@velt-js/mcp-installer 0.1.0

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.
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Velt CLI Execution Utilities
3
+ *
4
+ * Handles running the Velt CLI via npx @velt-js/add-velt.
5
+ * This module spawns npx to execute the published npm package.
6
+ */
7
+
8
+ import { spawn } from 'child_process';
9
+
10
+ /**
11
+ * Maps MCP feature selections to CLI flags
12
+ *
13
+ * CLI Flags supported by @velt-js/add-velt:
14
+ * --presence Add presence (VeltPresence - shows online users)
15
+ * --cursors Add cursors (VeltCursor - shows live cursor positions)
16
+ * --comments Add comments (VeltComments, VeltCommentsSidebar)
17
+ * --notifications Add notifications (VeltNotificationsTool)
18
+ * --reactflow-crdt Add ReactFlow CRDT
19
+ * --tiptap-crdt Add Tiptap CRDT
20
+ * --codemirror-crdt Add CodeMirror CRDT
21
+ * --all Enable presence + cursors + comments + notifications + CRDT (REQUIRES a CRDT flag!)
22
+ * --force, -f Force overwrite existing files
23
+ * --legacy-peer-deps Use legacy peer deps (npm only)
24
+ *
25
+ * @param {Object} params
26
+ * @param {string[]} [params.features=[]] - Features to install: 'presence', 'cursors', 'comments', 'notifications', 'crdt'
27
+ * @param {string} [params.crdtType=null] - CRDT type: 'tiptap', 'codemirror', 'reactflow'
28
+ * @param {boolean} [params.force=false] - Force overwrite files
29
+ * @param {boolean} [params.legacyPeerDeps=false] - Use legacy peer deps
30
+ * @returns {string[]} Array of CLI flags
31
+ */
32
+ export function mapFeaturesToCliFlags({
33
+ features = [],
34
+ crdtType = null,
35
+ force = false,
36
+ legacyPeerDeps = false,
37
+ }) {
38
+ const flags = [];
39
+
40
+ // Normalize features to lowercase
41
+ const normalizedFeatures = features.map(f => f.toLowerCase());
42
+
43
+ const hasPresence = normalizedFeatures.includes('presence');
44
+ const hasCursors = normalizedFeatures.includes('cursors');
45
+ const hasComments = normalizedFeatures.includes('comments');
46
+ const hasNotifications = normalizedFeatures.includes('notifications');
47
+ const hasCrdt = normalizedFeatures.includes('crdt') && crdtType;
48
+
49
+ // Validate CRDT type if provided
50
+ // Recognized types (accepted by MCP): includes blocknote which is handled via docs, not CLI flag
51
+ const recognizedCrdtTypes = ['tiptap', 'codemirror', 'reactflow', 'blocknote'];
52
+ // CLI-supported types (have a corresponding --X-crdt flag)
53
+ const cliFlagCrdtTypes = ['tiptap', 'codemirror', 'reactflow'];
54
+ const isCrdtRecognized = hasCrdt && recognizedCrdtTypes.includes(crdtType.toLowerCase());
55
+ const isCrdtCliSupported = hasCrdt && cliFlagCrdtTypes.includes(crdtType.toLowerCase());
56
+ if (hasCrdt && !isCrdtRecognized) {
57
+ console.error(` ⚠️ Unknown CRDT type "${crdtType}", skipping CRDT flag`);
58
+ } else if (hasCrdt && !isCrdtCliSupported) {
59
+ console.error(` ℹ️ CRDT type "${crdtType}" is handled via docs/plan, no CLI flag generated`);
60
+ }
61
+
62
+ // Determine flag strategy
63
+ // --all requires a CLI-supported CRDT flag, so only use it when we have all features with a supported CRDT type
64
+ const useAllFlag = hasPresence && hasCursors && hasComments && hasNotifications && isCrdtCliSupported;
65
+
66
+ if (useAllFlag) {
67
+ // Use --all with CRDT type
68
+ flags.push('--all');
69
+ flags.push(`--${crdtType.toLowerCase()}-crdt`);
70
+ } else {
71
+ // Build individual flags
72
+ if (hasPresence) {
73
+ flags.push('--presence');
74
+ }
75
+ if (hasCursors) {
76
+ flags.push('--cursors');
77
+ }
78
+ if (hasComments) {
79
+ flags.push('--comments');
80
+ }
81
+ if (hasNotifications) {
82
+ flags.push('--notifications');
83
+ }
84
+ if (isCrdtCliSupported) {
85
+ flags.push(`--${crdtType.toLowerCase()}-crdt`);
86
+ }
87
+ }
88
+
89
+ // Add installation flags
90
+ if (force) {
91
+ flags.push('--force');
92
+ }
93
+ if (legacyPeerDeps) {
94
+ flags.push('--legacy-peer-deps');
95
+ }
96
+
97
+ return flags;
98
+ }
99
+
100
+ /**
101
+ * Runs the Velt CLI via npx @velt-js/add-velt
102
+ *
103
+ * @param {Object} params
104
+ * @param {string} params.installDir - Directory to install Velt in
105
+ * @param {string} params.apiKey - Velt API key
106
+ * @param {string} [params.authToken] - Optional auth token
107
+ * @param {string[]} [params.features=[]] - Features to install (for guided mode)
108
+ * @param {string} [params.crdtType=null] - CRDT editor type if 'crdt' feature selected
109
+ * @param {boolean} [params.force=false] - Force overwrite existing files
110
+ * @param {boolean} [params.legacyPeerDeps=false] - Use legacy peer deps (npm only)
111
+ * @returns {Promise<Object>} CLI execution result
112
+ */
113
+ export async function runVeltCli({
114
+ installDir,
115
+ apiKey,
116
+ authToken = null,
117
+ features = [],
118
+ crdtType = null,
119
+ force = false,
120
+ legacyPeerDeps = false,
121
+ }) {
122
+ try {
123
+ // Log what we're about to do
124
+ console.error('\n📦 Velt CLI Execution');
125
+ console.error(` Directory: ${installDir}`);
126
+ console.error(` API Key: ${apiKey ? `${apiKey.substring(0, 8)}...` : '(not provided)'}`);
127
+
128
+ // Map features to CLI flags
129
+ const flags = mapFeaturesToCliFlags({
130
+ features,
131
+ crdtType,
132
+ force,
133
+ legacyPeerDeps,
134
+ });
135
+
136
+ console.error(` 🏷️ Features: ${features.length > 0 ? features.join(', ') : '(core only)'}`);
137
+ console.error(` 🚩 Flags: ${flags.length > 0 ? flags.join(' ') : '(none)'}`);
138
+
139
+ // Build environment with API credentials
140
+ const env = {
141
+ ...process.env,
142
+ VELT_API_KEY: apiKey,
143
+ NEXT_PUBLIC_VELT_API_KEY: apiKey,
144
+ };
145
+
146
+ if (authToken) {
147
+ env.VELT_AUTH_TOKEN = authToken;
148
+ }
149
+
150
+ // Build npx command
151
+ const npxArgs = ['@velt-js/add-velt', ...flags];
152
+ const fullCommand = `npx ${npxArgs.join(' ')}`;
153
+
154
+ console.error('\n ═══════════════════════════════════════════════════════════');
155
+ console.error(` 📋 EXACT CLI COMMAND: ${fullCommand}`);
156
+ console.error(' ═══════════════════════════════════════════════════════════\n');
157
+
158
+ // Execute via npx
159
+ const result = await new Promise((resolve) => {
160
+ let stdout = '';
161
+ let stderr = '';
162
+ const timeout = 120000;
163
+
164
+ const proc = spawn('npx', npxArgs, {
165
+ cwd: installDir,
166
+ env,
167
+ stdio: ['inherit', 'pipe', 'pipe'],
168
+ shell: process.platform === 'win32',
169
+ });
170
+
171
+ const timeoutId = setTimeout(() => {
172
+ proc.kill('SIGTERM');
173
+ resolve({
174
+ success: false,
175
+ error: `CLI execution timed out after ${timeout}ms`,
176
+ method: 'npx',
177
+ command: fullCommand,
178
+ exitCode: -1,
179
+ stdout,
180
+ stderr,
181
+ });
182
+ }, timeout);
183
+
184
+ proc.stdout?.on('data', (data) => {
185
+ stdout += data.toString();
186
+ process.stderr.write(data);
187
+ });
188
+
189
+ proc.stderr?.on('data', (data) => {
190
+ stderr += data.toString();
191
+ process.stderr.write(data);
192
+ });
193
+
194
+ proc.on('error', (err) => {
195
+ clearTimeout(timeoutId);
196
+ resolve({
197
+ success: false,
198
+ error: err.message,
199
+ method: 'npx',
200
+ command: fullCommand,
201
+ exitCode: 1,
202
+ stdout,
203
+ stderr,
204
+ });
205
+ });
206
+
207
+ proc.on('close', (code) => {
208
+ clearTimeout(timeoutId);
209
+ resolve({
210
+ success: code === 0,
211
+ exitCode: code,
212
+ method: 'npx',
213
+ command: fullCommand,
214
+ stdout,
215
+ stderr,
216
+ output: stdout + stderr,
217
+ });
218
+ });
219
+ });
220
+
221
+ // Log result summary
222
+ if (result.success) {
223
+ console.error(`\n ✅ CLI completed successfully (method: ${result.method})`);
224
+ } else {
225
+ console.error(`\n ⚠️ CLI exited with code ${result.exitCode} (method: ${result.method})`);
226
+ if (result.error) {
227
+ console.error(` Error: ${result.error}`);
228
+ }
229
+ }
230
+
231
+ return {
232
+ success: result.success,
233
+ exitCode: result.exitCode,
234
+ output: result.output,
235
+ command: result.command,
236
+ method: result.method,
237
+ error: result.error,
238
+ };
239
+ } catch (error) {
240
+ console.error(` ❌ CLI execution error: ${error.message}`);
241
+ return {
242
+ success: false,
243
+ error: error.message,
244
+ exitCode: 1,
245
+ method: 'error',
246
+ };
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Runs CLI with feature-specific flags (for guided mode)
252
+ *
253
+ * This is the recommended function for the guided installation path
254
+ * where users have selected specific features.
255
+ *
256
+ * @param {Object} params
257
+ * @param {string} params.projectPath - Target project directory
258
+ * @param {string} params.apiKey - Velt API key
259
+ * @param {string} [params.authToken] - Auth token
260
+ * @param {string[]} params.features - Features: 'comments', 'notifications', 'crdt', 'presence', 'cursors', 'recorder'
261
+ * @param {string} [params.commentType] - Comment type (not used by CLI, for plan only)
262
+ * @param {string} [params.crdtEditorType] - CRDT editor: 'tiptap', 'codemirror', 'reactflow'
263
+ * @param {boolean} [params.force=false] - Force overwrite
264
+ * @returns {Promise<Object>} CLI execution result
265
+ */
266
+ export async function runVeltCliWithFeatures({
267
+ projectPath,
268
+ apiKey,
269
+ authToken = null,
270
+ features = [],
271
+ commentType = null,
272
+ crdtEditorType = null,
273
+ force = false,
274
+ }) {
275
+ console.error('\n🎯 Running Velt CLI with feature flags');
276
+
277
+ // Map MCP features to CLI-compatible features
278
+ // The CLI supports: comments, notifications, presence, cursors, and CRDT types
279
+ // Only 'recorder' is handled by the MCP guided plan (not supported by CLI)
280
+ const cliFeatures = [];
281
+
282
+ if (features.includes('comments')) {
283
+ cliFeatures.push('comments');
284
+ }
285
+ if (features.includes('notifications')) {
286
+ cliFeatures.push('notifications');
287
+ }
288
+ if (features.includes('presence')) {
289
+ cliFeatures.push('presence');
290
+ }
291
+ if (features.includes('cursors')) {
292
+ cliFeatures.push('cursors');
293
+ }
294
+ if (features.includes('crdt')) {
295
+ if (crdtEditorType) {
296
+ cliFeatures.push('crdt');
297
+ } else {
298
+ console.error(' ⚠️ CRDT feature selected but no crdtEditorType provided — CRDT will be skipped');
299
+ }
300
+ }
301
+
302
+ console.error(` Requested features: ${features.join(', ')}`);
303
+ console.error(` CLI features to forward: ${cliFeatures.length > 0 ? cliFeatures.join(', ') : '(core only)'}`);
304
+
305
+ if (crdtEditorType) {
306
+ console.error(` CRDT Editor: ${crdtEditorType}`);
307
+ }
308
+
309
+ return runVeltCli({
310
+ installDir: projectPath,
311
+ apiKey,
312
+ authToken,
313
+ features: cliFeatures,
314
+ crdtType: crdtEditorType,
315
+ force,
316
+ });
317
+ }
318
+
319
+ /**
320
+ * Runs CLI in core-only mode (for SKIP/CLI-only path)
321
+ *
322
+ * This runs the CLI without any feature flags, generating only
323
+ * the core scaffold files (VeltInitializeDocument, VeltInitializeUser, VeltCollaboration).
324
+ *
325
+ * @param {Object} params
326
+ * @param {string} params.projectPath - Target project directory
327
+ * @param {string} params.apiKey - Velt API key
328
+ * @param {string} [params.authToken] - Auth token
329
+ * @param {boolean} [params.force=false] - Force overwrite
330
+ * @returns {Promise<Object>} CLI execution result
331
+ */
332
+ export async function runVeltCliCoreOnly({
333
+ projectPath,
334
+ apiKey,
335
+ authToken = null,
336
+ force = false,
337
+ }) {
338
+ console.error('\n📦 Running Velt CLI (core only, no feature flags)');
339
+
340
+ return runVeltCli({
341
+ installDir: projectPath,
342
+ apiKey,
343
+ authToken,
344
+ features: [], // No features = core scaffold only
345
+ crdtType: null,
346
+ force,
347
+ });
348
+ }
349
+
350
+ /**
351
+ * Gets CLI resolution info (for diagnostics/validation)
352
+ *
353
+ * @returns {Object} CLI resolution result
354
+ */
355
+ export function getCliResolutionInfo() {
356
+ return {
357
+ method: 'npx',
358
+ command: 'npx @velt-js/add-velt',
359
+ path: null,
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Gets the CLI flags that would be used for given features
365
+ *
366
+ * @param {Object} params - Same as mapFeaturesToCliFlags
367
+ * @returns {string[]} Array of CLI flags
368
+ */
369
+ export function previewCliFlags(params) {
370
+ return mapFeaturesToCliFlags(params);
371
+ }
372
+
373
+ export default {
374
+ runVeltCli,
375
+ runVeltCliWithFeatures,
376
+ runVeltCliCoreOnly,
377
+ getCliResolutionInfo,
378
+ previewCliFlags,
379
+ mapFeaturesToCliFlags,
380
+ };
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Comment Placement Detector
3
+ *
4
+ * Analyzes project structure to determine the best files and locations
5
+ * for placing Velt comments (Freestyle or Popover).
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+
11
+ /**
12
+ * @typedef {Object} CommentPlacement
13
+ * @property {string} file - File path where comment should be added
14
+ * @property {string} componentName - Name of the component/file
15
+ * @property {string} reason - Why this location was chosen
16
+ * @property {string} implementationGuide - How to implement the comment type
17
+ * @property {number} confidence - Confidence score (0-100)
18
+ */
19
+
20
+ /**
21
+ * Detects where to place comments based on user's description and comment type
22
+ *
23
+ * @param {Object} options - Detection options
24
+ * @param {string} options.projectPath - Path to the Next.js project
25
+ * @param {string} options.commentType - 'freestyle' or 'popover'
26
+ * @param {string} [options.targetDescription] - User's description of where to place comments (e.g., "header", "sidebar")
27
+ * @param {string} [options.targetComponent] - Specific component name if known
28
+ * @returns {Promise<Object>} Detection results
29
+ */
30
+ export async function detectCommentPlacement(options) {
31
+ const {
32
+ projectPath,
33
+ commentType,
34
+ targetDescription = '',
35
+ targetComponent = '',
36
+ } = options;
37
+
38
+ try {
39
+ console.error(`🔍 Analyzing project for ${commentType} comment placement...`);
40
+ if (targetDescription) {
41
+ console.error(` 🎯 Target: ${targetDescription}`);
42
+ }
43
+
44
+ // Step 1: Detect project structure
45
+ const structure = detectProjectStructure(projectPath);
46
+
47
+ // Step 2: Find candidate files
48
+ const candidates = await findCandidateFiles({
49
+ structure,
50
+ targetDescription,
51
+ targetComponent,
52
+ commentType,
53
+ });
54
+
55
+ // Step 3: Rank candidates by confidence
56
+ const rankedCandidates = rankCandidates(candidates, {
57
+ commentType,
58
+ targetDescription,
59
+ targetComponent,
60
+ });
61
+
62
+ console.error(` ✅ Found ${rankedCandidates.length} potential placement(s)`);
63
+
64
+ return {
65
+ success: true,
66
+ data: {
67
+ commentType,
68
+ targetDescription,
69
+ placements: rankedCandidates,
70
+ recommendedPlacement: rankedCandidates[0] || null,
71
+ },
72
+ };
73
+ } catch (error) {
74
+ console.error(` ❌ Error detecting comment placement: ${error.message}`);
75
+
76
+ return {
77
+ success: false,
78
+ error: error.message,
79
+ stack: error.stack,
80
+ };
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Detects project structure
86
+ */
87
+ function detectProjectStructure(projectPath) {
88
+ const srcAppPath = path.join(projectPath, 'src', 'app');
89
+ const hasSrcApp = fs.existsSync(srcAppPath);
90
+
91
+ const appRoot = hasSrcApp
92
+ ? path.join(projectPath, 'src', 'app')
93
+ : path.join(projectPath, 'app');
94
+
95
+ const componentsRoot = hasSrcApp
96
+ ? path.join(projectPath, 'src', 'components')
97
+ : path.join(projectPath, 'components');
98
+
99
+ return {
100
+ hasSrcApp,
101
+ appRoot,
102
+ componentsRoot,
103
+ projectPath,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Finds candidate files for comment placement
109
+ */
110
+ async function findCandidateFiles(options) {
111
+ const { structure, targetDescription, targetComponent, commentType } = options;
112
+ const candidates = [];
113
+
114
+ // Helper to search directory recursively
115
+ function searchDirectory(dir, relativePath = '') {
116
+ if (!fs.existsSync(dir)) return;
117
+
118
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
119
+
120
+ for (const entry of entries) {
121
+ const fullPath = path.join(dir, entry.name);
122
+ const relPath = path.join(relativePath, entry.name);
123
+
124
+ if (entry.isDirectory()) {
125
+ // Skip node_modules and .next
126
+ if (entry.name === 'node_modules' || entry.name === '.next') continue;
127
+ searchDirectory(fullPath, relPath);
128
+ } else if (entry.isFile() && /\.(tsx|jsx)$/.test(entry.name)) {
129
+ // Analyze this file
130
+ const analysis = analyzeFile(fullPath, relPath, targetDescription, targetComponent, commentType);
131
+ if (analysis) {
132
+ candidates.push(analysis);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // Search app directory
139
+ searchDirectory(structure.appRoot, 'app');
140
+
141
+ // Search components directory
142
+ if (fs.existsSync(structure.componentsRoot)) {
143
+ searchDirectory(structure.componentsRoot, 'components');
144
+ }
145
+
146
+ return candidates;
147
+ }
148
+
149
+ /**
150
+ * Analyzes a file to determine if it's a good candidate
151
+ */
152
+ function analyzeFile(filePath, relativePath, targetDescription, targetComponent, commentType) {
153
+ try {
154
+ const content = fs.readFileSync(filePath, 'utf-8');
155
+ const fileName = path.basename(filePath);
156
+ const componentName = fileName.replace(/\.(tsx|jsx)$/, '');
157
+
158
+ // Calculate match score
159
+ let matchScore = 0;
160
+ let matchReasons = [];
161
+
162
+ // Check if filename matches target description
163
+ const normalizedTarget = targetDescription.toLowerCase();
164
+ const normalizedFileName = fileName.toLowerCase();
165
+ const normalizedRelPath = relativePath.toLowerCase();
166
+
167
+ // Keyword matching
168
+ const keywords = {
169
+ header: ['header', 'navbar', 'nav', 'topbar', 'menu'],
170
+ sidebar: ['sidebar', 'drawer', 'aside', 'panel'],
171
+ footer: ['footer', 'bottom'],
172
+ main: ['main', 'content', 'body', 'page'],
173
+ card: ['card', 'item', 'post', 'article'],
174
+ list: ['list', 'grid', 'table'],
175
+ form: ['form', 'input', 'field'],
176
+ button: ['button', 'btn', 'action'],
177
+ };
178
+
179
+ // Match against keywords
180
+ for (const [category, words] of Object.entries(keywords)) {
181
+ if (normalizedTarget.includes(category)) {
182
+ for (const word of words) {
183
+ if (normalizedFileName.includes(word) || normalizedRelPath.includes(word)) {
184
+ matchScore += 30;
185
+ matchReasons.push(`Matches "${word}" in filename/path`);
186
+ break;
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // Check if specific component name matches
193
+ if (targetComponent && normalizedFileName.includes(targetComponent.toLowerCase())) {
194
+ matchScore += 50;
195
+ matchReasons.push(`Matches specified component name "${targetComponent}"`);
196
+ }
197
+
198
+ // Check for React component patterns
199
+ if (content.includes('export default') || content.includes('export function') || content.includes('export const')) {
200
+ matchScore += 10;
201
+ matchReasons.push('Contains React component export');
202
+ }
203
+
204
+ // Freestyle comments work best on page-level components
205
+ if (commentType === 'freestyle') {
206
+ if (fileName === 'page.tsx' || fileName === 'page.jsx') {
207
+ matchScore += 20;
208
+ matchReasons.push('Page component is ideal for freestyle comments');
209
+ }
210
+ if (content.includes('VeltComments') || content.includes('VeltProvider')) {
211
+ matchScore += 15;
212
+ matchReasons.push('Already has Velt integration');
213
+ }
214
+ }
215
+
216
+ // Popover comments work best on interactive components
217
+ if (commentType === 'popover') {
218
+ if (content.includes('onClick') || content.includes('button') || content.includes('Button')) {
219
+ matchScore += 15;
220
+ matchReasons.push('Has interactive elements (good for popover)');
221
+ }
222
+ if (content.includes('card') || content.includes('Card') || content.includes('item')) {
223
+ matchScore += 15;
224
+ matchReasons.push('Has card/item components (good for popover)');
225
+ }
226
+ }
227
+
228
+ // If no matches, skip this file
229
+ if (matchScore === 0) {
230
+ return null;
231
+ }
232
+
233
+ // Build implementation guide
234
+ const implementationGuide = buildImplementationGuide(commentType, componentName, content);
235
+
236
+ return {
237
+ file: relativePath,
238
+ fullPath: filePath,
239
+ componentName,
240
+ reason: matchReasons.join('; '),
241
+ implementationGuide,
242
+ confidence: Math.min(matchScore, 100),
243
+ commentType,
244
+ };
245
+ } catch (error) {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Builds implementation guide for the comment type
252
+ */
253
+ function buildImplementationGuide(commentType, componentName, fileContent) {
254
+ if (commentType === 'freestyle') {
255
+ return `
256
+ To add Freestyle Comments to ${componentName}:
257
+
258
+ 1. Import: import { VeltComments } from '@veltdev/react';
259
+ 2. Add component: <VeltComments />
260
+ 3. Freestyle comments allow users to click anywhere on the page to add comments
261
+ 4. No additional props required for basic setup
262
+
263
+ Example:
264
+ <div>
265
+ <VeltComments />
266
+ {/* Your existing content */}
267
+ </div>
268
+
269
+ Docs: https://docs.velt.dev/async-collaboration/comments/setup/freestyle
270
+ `;
271
+ }
272
+
273
+ if (commentType === 'popover') {
274
+ return `
275
+ To add Popover Comments to ${componentName}:
276
+
277
+ 1. Import: import { VeltCommentTool } from '@veltdev/react';
278
+ 2. Add data attribute to target elements: data-velt-comment-target="{uniqueId}"
279
+ 3. Add the comment tool component: <VeltCommentTool commentTargetId="{uniqueId}" />
280
+
281
+ Example:
282
+ <div data-velt-comment-target="component-${componentName.toLowerCase()}">
283
+ <VeltCommentTool commentTargetId="component-${componentName.toLowerCase()}" />
284
+ {/* Your existing content */}
285
+ </div>
286
+
287
+ Docs: https://docs.velt.dev/async-collaboration/comments/setup/popover
288
+ `;
289
+ }
290
+
291
+ return `See Velt documentation for ${commentType} comments implementation.`;
292
+ }
293
+
294
+ /**
295
+ * Ranks candidates by confidence score
296
+ */
297
+ function rankCandidates(candidates, options) {
298
+ return candidates
299
+ .sort((a, b) => b.confidence - a.confidence)
300
+ .slice(0, 5); // Return top 5 candidates
301
+ }
302
+
303
+ export default {
304
+ detectCommentPlacement,
305
+ };