@redpanda-data/docs-extensions-and-macros 4.12.6 → 4.13.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.
Files changed (61) hide show
  1. package/README.adoc +33 -1064
  2. package/bin/doc-tools-mcp.js +720 -0
  3. package/bin/doc-tools.js +1050 -50
  4. package/bin/mcp-tools/antora.js +153 -0
  5. package/bin/mcp-tools/cache.js +89 -0
  6. package/bin/mcp-tools/cloud-regions.js +127 -0
  7. package/bin/mcp-tools/content-review.js +196 -0
  8. package/bin/mcp-tools/crd-docs.js +153 -0
  9. package/bin/mcp-tools/frontmatter.js +138 -0
  10. package/bin/mcp-tools/generated-docs-review.js +887 -0
  11. package/bin/mcp-tools/helm-docs.js +152 -0
  12. package/bin/mcp-tools/index.js +245 -0
  13. package/bin/mcp-tools/job-queue.js +468 -0
  14. package/bin/mcp-tools/mcp-validation.js +266 -0
  15. package/bin/mcp-tools/metrics-docs.js +146 -0
  16. package/bin/mcp-tools/openapi.js +174 -0
  17. package/bin/mcp-tools/prompt-discovery.js +283 -0
  18. package/bin/mcp-tools/property-docs.js +157 -0
  19. package/bin/mcp-tools/rpcn-docs.js +113 -0
  20. package/bin/mcp-tools/rpk-docs.js +141 -0
  21. package/bin/mcp-tools/telemetry.js +211 -0
  22. package/bin/mcp-tools/utils.js +131 -0
  23. package/bin/mcp-tools/versions.js +168 -0
  24. package/cli-utils/convert-doc-links.js +1 -1
  25. package/cli-utils/github-token.js +58 -0
  26. package/cli-utils/self-managed-docs-branch.js +2 -2
  27. package/cli-utils/setup-mcp.js +313 -0
  28. package/docker-compose/25.1/transactions.md +1 -1
  29. package/docker-compose/transactions.md +1 -1
  30. package/extensions/DEVELOPMENT.adoc +464 -0
  31. package/extensions/README.adoc +124 -0
  32. package/extensions/REFERENCE.adoc +768 -0
  33. package/extensions/USER_GUIDE.adoc +339 -0
  34. package/extensions/generate-rp-connect-info.js +3 -4
  35. package/extensions/version-fetcher/get-latest-console-version.js +38 -27
  36. package/extensions/version-fetcher/get-latest-redpanda-version.js +65 -54
  37. package/extensions/version-fetcher/retry-util.js +88 -0
  38. package/extensions/version-fetcher/set-latest-version.js +6 -3
  39. package/macros/DEVELOPMENT.adoc +377 -0
  40. package/macros/README.adoc +105 -0
  41. package/macros/REFERENCE.adoc +222 -0
  42. package/macros/USER_GUIDE.adoc +220 -0
  43. package/macros/rp-connect-components.js +6 -6
  44. package/package.json +12 -3
  45. package/tools/bundle-openapi.js +20 -10
  46. package/tools/cloud-regions/generate-cloud-regions.js +1 -1
  47. package/tools/fetch-from-github.js +18 -4
  48. package/tools/gen-rpk-ascii.py +3 -1
  49. package/tools/generate-cli-docs.js +325 -0
  50. package/tools/get-console-version.js +4 -2
  51. package/tools/get-redpanda-version.js +4 -2
  52. package/tools/metrics/metrics.py +19 -7
  53. package/tools/property-extractor/Makefile +7 -1
  54. package/tools/property-extractor/cloud_config.py +4 -4
  55. package/tools/property-extractor/constant_resolver.py +11 -11
  56. package/tools/property-extractor/property_extractor.py +18 -16
  57. package/tools/property-extractor/topic_property_extractor.py +2 -2
  58. package/tools/property-extractor/transformers.py +7 -7
  59. package/tools/property-extractor/type_definition_extractor.py +4 -4
  60. package/tools/redpanda-connect/README.adoc +1 -1
  61. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +5 -3
@@ -0,0 +1,468 @@
1
+ /**
2
+ * MCP Tools - Background Job Queue
3
+ *
4
+ * Manages long-running documentation generation jobs with progress tracking
5
+ * and streaming updates via MCP notifications.
6
+ */
7
+
8
+ const { spawn } = require('child_process');
9
+ const { randomUUID } = require('crypto');
10
+ const { DEFAULT_COMMAND_TIMEOUT } = require('./utils');
11
+
12
+ // Job status enum
13
+ const JobStatus = {
14
+ PENDING: 'pending',
15
+ RUNNING: 'running',
16
+ COMPLETED: 'completed',
17
+ FAILED: 'failed'
18
+ };
19
+
20
+ // In-memory job storage
21
+ const jobs = new Map();
22
+
23
+ // Reference to MCP server for sending notifications
24
+ let mcpServer = null;
25
+
26
+ /**
27
+ * Initialize the job queue with MCP server reference
28
+ * @param {Object} server - MCP server instance
29
+ */
30
+ function initializeJobQueue(server) {
31
+ mcpServer = server;
32
+ }
33
+
34
+ /**
35
+ * Create a new background job
36
+ * @param {string} toolName - Name of the tool
37
+ * @param {string|Array} command - Command to execute (array format preferred for security)
38
+ * @param {Object} options - Execution options
39
+ * @returns {string} Job ID
40
+ */
41
+ function createJob(toolName, command, options = {}) {
42
+ const jobId = randomUUID();
43
+
44
+ const job = {
45
+ id: jobId,
46
+ tool: toolName,
47
+ command,
48
+ status: JobStatus.PENDING,
49
+ createdAt: new Date().toISOString(),
50
+ startedAt: null,
51
+ completedAt: null,
52
+ progress: 0,
53
+ progressMessage: 'Job queued',
54
+ output: '',
55
+ error: null,
56
+ result: null
57
+ };
58
+
59
+ jobs.set(jobId, job);
60
+
61
+ // Start job execution immediately (can be changed to queue-based if needed)
62
+ executeJob(jobId, command, options);
63
+
64
+ return jobId;
65
+ }
66
+
67
+ /**
68
+ * Parse a command string into executable and arguments
69
+ * Handles basic quoted arguments safely
70
+ */
71
+ function parseCommand(command) {
72
+ const args = [];
73
+ let current = '';
74
+ let inQuotes = false;
75
+ let quoteChar = '';
76
+
77
+ for (let i = 0; i < command.length; i++) {
78
+ const char = command[i];
79
+
80
+ if (!inQuotes && (char === '"' || char === "'")) {
81
+ inQuotes = true;
82
+ quoteChar = char;
83
+ } else if (inQuotes && char === quoteChar) {
84
+ inQuotes = false;
85
+ quoteChar = '';
86
+ } else if (!inQuotes && char === ' ') {
87
+ if (current.trim()) {
88
+ args.push(current.trim());
89
+ current = '';
90
+ }
91
+ } else {
92
+ current += char;
93
+ }
94
+ }
95
+
96
+ if (current.trim()) {
97
+ args.push(current.trim());
98
+ }
99
+
100
+ return args;
101
+ }
102
+
103
+ /**
104
+ * Validate that an executable is safe to run
105
+ * Whitelist known safe executables
106
+ */
107
+ function isValidExecutable(executable) {
108
+ const allowedExecutables = [
109
+ 'npx',
110
+ 'node',
111
+ 'npm',
112
+ 'yarn',
113
+ 'doc-tools',
114
+ 'helm-docs',
115
+ 'crd-ref-docs',
116
+ 'git',
117
+ 'make',
118
+ 'docker',
119
+ 'timeout'
120
+ ];
121
+
122
+ // Allow absolute paths to known tools in common locations
123
+ const allowedPaths = [
124
+ '/usr/bin/',
125
+ '/usr/local/bin/',
126
+ '/bin/',
127
+ '/opt/homebrew/bin/'
128
+ ];
129
+
130
+ // Check if it's a whitelisted executable
131
+ if (allowedExecutables.includes(executable)) {
132
+ return true;
133
+ }
134
+
135
+ // Check if it's an absolute path to a whitelisted location
136
+ if (executable.startsWith('/')) {
137
+ return allowedPaths.some(path =>
138
+ executable.startsWith(path) &&
139
+ allowedExecutables.some(exe => executable.endsWith(`/${exe}`) || executable.endsWith(`/${exe}.exe`))
140
+ );
141
+ }
142
+
143
+ return false;
144
+ }
145
+
146
+ /**
147
+ * Execute a job in the background
148
+ * @param {string} jobId - Job ID
149
+ * @param {string|Array} command - Command to execute (string will be parsed, array is preferred)
150
+ * @param {Object} options - Execution options
151
+ */
152
+ async function executeJob(jobId, command, options = {}) {
153
+ const job = jobs.get(jobId);
154
+ if (!job) return;
155
+
156
+ try {
157
+ job.status = JobStatus.RUNNING;
158
+ job.startedAt = new Date().toISOString();
159
+ updateJobProgress(jobId, 10, 'Starting execution...');
160
+
161
+ const cwd = options.cwd || process.cwd();
162
+ const timeout = options.timeout || DEFAULT_COMMAND_TIMEOUT;
163
+
164
+ let executable, args;
165
+
166
+ if (Array.isArray(command)) {
167
+ // Preferred: pre-parsed array [executable, ...args]
168
+ [executable, ...args] = command;
169
+ } else if (typeof command === 'string') {
170
+ // Legacy string command - use safer parsing
171
+ // Basic parsing that handles simple quoted arguments
172
+ const parsedArgs = parseCommand(command);
173
+ [executable, ...args] = parsedArgs;
174
+ } else {
175
+ throw new Error('Command must be a string or array');
176
+ }
177
+
178
+ // Validate executable to prevent injection
179
+ if (!isValidExecutable(executable)) {
180
+ throw new Error(`Invalid executable: ${executable}`);
181
+ }
182
+
183
+ const childProcess = spawn(executable, args, {
184
+ cwd,
185
+ shell: false, // Explicitly disable shell to prevent injection
186
+ stdio: ['ignore', 'pipe', 'pipe']
187
+ });
188
+
189
+ let stdout = '';
190
+ let stderr = '';
191
+ let timedOut = false;
192
+
193
+ // Set up timeout
194
+ const timeoutHandle = setTimeout(() => {
195
+ timedOut = true;
196
+ childProcess.kill('SIGTERM');
197
+ job.error = `Job timed out after ${timeout}ms`;
198
+ job.status = JobStatus.FAILED;
199
+ job.completedAt = new Date().toISOString();
200
+ job.result = {
201
+ success: false,
202
+ error: job.error,
203
+ stdout: stdout.trim(),
204
+ stderr: stderr.trim()
205
+ };
206
+ updateJobProgress(jobId, 100, 'Job timed out');
207
+ }, timeout);
208
+
209
+ // Capture stdout
210
+ childProcess.stdout.on('data', (data) => {
211
+ const chunk = data.toString();
212
+ stdout += chunk;
213
+ job.output = stdout;
214
+
215
+ // Parse progress from output if available
216
+ parseProgressFromOutput(jobId, chunk);
217
+ });
218
+
219
+ // Capture stderr
220
+ childProcess.stderr.on('data', (data) => {
221
+ stderr += data.toString();
222
+ });
223
+
224
+ // Handle completion
225
+ childProcess.on('close', (code) => {
226
+ clearTimeout(timeoutHandle);
227
+
228
+ // If job already timed out, don't overwrite the timeout error
229
+ if (timedOut) {
230
+ return;
231
+ }
232
+
233
+ job.completedAt = new Date().toISOString();
234
+
235
+ if (code === 0) {
236
+ job.status = JobStatus.COMPLETED;
237
+ job.progress = 100;
238
+ job.progressMessage = 'Completed successfully';
239
+ job.result = {
240
+ success: true,
241
+ output: stdout.trim(),
242
+ command
243
+ };
244
+ updateJobProgress(jobId, 100, 'Completed successfully');
245
+ } else {
246
+ job.status = JobStatus.FAILED;
247
+ job.error = stderr || `Command exited with code ${code}`;
248
+ job.result = {
249
+ success: false,
250
+ error: job.error,
251
+ stdout: stdout.trim(),
252
+ stderr: stderr.trim(),
253
+ exitCode: code
254
+ };
255
+ updateJobProgress(jobId, 100, `Failed with exit code ${code}`);
256
+ }
257
+ });
258
+
259
+ // Handle errors
260
+ childProcess.on('error', (err) => {
261
+ clearTimeout(timeoutHandle);
262
+
263
+ // If job already timed out, don't overwrite the timeout error
264
+ if (timedOut) {
265
+ return;
266
+ }
267
+
268
+ job.status = JobStatus.FAILED;
269
+ job.error = err.message;
270
+ job.completedAt = new Date().toISOString();
271
+ job.result = {
272
+ success: false,
273
+ error: err.message
274
+ };
275
+ updateJobProgress(jobId, 100, `Error: ${err.message}`);
276
+ });
277
+ } catch (err) {
278
+ // Catch synchronous errors (validation failures, etc.)
279
+ // Record them on the job instead of throwing
280
+ job.status = JobStatus.FAILED;
281
+ job.error = err.message;
282
+ job.completedAt = new Date().toISOString();
283
+ job.result = {
284
+ success: false,
285
+ error: err.message
286
+ };
287
+ updateJobProgress(jobId, 100, `Error: ${err.message}`);
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Parse progress information from command output
293
+ * @param {string} jobId - Job ID
294
+ * @param {string} output - Output chunk
295
+ */
296
+ function parseProgressFromOutput(jobId, output) {
297
+ const job = jobs.get(jobId);
298
+ if (!job) return;
299
+
300
+ // Look for common progress patterns
301
+ const patterns = [
302
+ // Percentage: "Progress: 45%", "45%", "[45%]"
303
+ /(?:progress[:\s]*)?(\d+)%/i,
304
+ // Step indicators: "Step 3/5", "3 of 5"
305
+ /(?:step\s+)?(\d+)\s*(?:\/|of)\s*(\d+)/i,
306
+ // Processing indicators: "Processing file 3 of 10"
307
+ /processing.*?(\d+)\s*of\s*(\d+)/i,
308
+ // Cloning/downloading indicators
309
+ /(?:cloning|downloading|fetching)/i,
310
+ // Building indicators
311
+ /(?:building|compiling|generating)/i,
312
+ // Analyzing indicators
313
+ /(?:analyzing|parsing|extracting)/i
314
+ ];
315
+
316
+ for (const pattern of patterns) {
317
+ const match = output.match(pattern);
318
+ if (match) {
319
+ if (match.length === 2) {
320
+ // Percentage match
321
+ const percentage = parseInt(match[1]);
322
+ if (percentage >= 0 && percentage <= 100) {
323
+ updateJobProgress(jobId, percentage, output.trim().split('\n').pop());
324
+ return;
325
+ }
326
+ } else if (match.length === 3) {
327
+ // Step match (for example, "3/5")
328
+ const current = parseInt(match[1]);
329
+ const total = parseInt(match[2]);
330
+ const percentage = Math.round((current / total) * 100);
331
+ updateJobProgress(jobId, percentage, output.trim().split('\n').pop());
332
+ return;
333
+ }
334
+ }
335
+ }
336
+
337
+ // If we find action keywords but no percentage, estimate progress based on job runtime
338
+ const actionKeywords = ['cloning', 'downloading', 'fetching', 'building', 'compiling', 'generating', 'analyzing', 'parsing', 'extracting'];
339
+ const hasAction = actionKeywords.some(keyword => output.toLowerCase().includes(keyword));
340
+
341
+ if (hasAction && job.progress < 90) {
342
+ // Gradually increase progress for long-running jobs
343
+ const elapsed = new Date() - new Date(job.startedAt);
344
+ const estimatedTotal = DEFAULT_COMMAND_TIMEOUT;
345
+ const estimatedProgress = Math.min(90, Math.round((elapsed / estimatedTotal) * 100));
346
+
347
+ if (estimatedProgress > job.progress) {
348
+ updateJobProgress(jobId, estimatedProgress, output.trim().split('\n').pop());
349
+ }
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Update job progress and send notification
355
+ * @param {string} jobId - Job ID
356
+ * @param {number} progress - Progress percentage (0-100)
357
+ * @param {string} message - Progress message
358
+ */
359
+ function updateJobProgress(jobId, progress, message) {
360
+ const job = jobs.get(jobId);
361
+ if (!job) return;
362
+
363
+ job.progress = Math.min(100, Math.max(0, progress));
364
+ job.progressMessage = message;
365
+
366
+ // Send MCP notification if server is initialized
367
+ if (mcpServer) {
368
+ try {
369
+ mcpServer.notification({
370
+ method: 'notifications/progress',
371
+ params: {
372
+ progressToken: jobId,
373
+ progress: job.progress,
374
+ total: 100,
375
+ message: message
376
+ }
377
+ });
378
+ } catch (err) {
379
+ // Ignore notification errors - they shouldn't stop the job
380
+ console.error(`Failed to send progress notification: ${err.message}`);
381
+ }
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Get job status
387
+ * @param {string} jobId - Job ID
388
+ * @returns {Object|null} Job status or null if not found
389
+ */
390
+ function getJob(jobId) {
391
+ const job = jobs.get(jobId);
392
+ if (!job) return null;
393
+
394
+ return {
395
+ id: job.id,
396
+ tool: job.tool,
397
+ status: job.status,
398
+ progress: job.progress,
399
+ progressMessage: job.progressMessage,
400
+ createdAt: job.createdAt,
401
+ startedAt: job.startedAt,
402
+ completedAt: job.completedAt,
403
+ error: job.error,
404
+ result: job.result
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Get all jobs
410
+ * @param {Object} filter - Optional filter
411
+ * @returns {Array} List of jobs
412
+ */
413
+ function listJobs(filter = {}) {
414
+ const jobList = Array.from(jobs.values());
415
+
416
+ return jobList
417
+ .filter(job => {
418
+ if (filter.status && job.status !== filter.status) return false;
419
+ if (filter.tool && job.tool !== filter.tool) return false;
420
+ return true;
421
+ })
422
+ .map(job => ({
423
+ id: job.id,
424
+ tool: job.tool,
425
+ status: job.status,
426
+ progress: job.progress,
427
+ progressMessage: job.progressMessage,
428
+ createdAt: job.createdAt,
429
+ startedAt: job.startedAt,
430
+ completedAt: job.completedAt
431
+ }))
432
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
433
+ }
434
+
435
+ /**
436
+ * Clean up old completed/failed jobs
437
+ * @param {number} maxAge - Maximum age in milliseconds (default: 1 hour)
438
+ */
439
+ function cleanupOldJobs(maxAge = 60 * 60 * 1000) {
440
+ const now = Date.now();
441
+ let removed = 0;
442
+
443
+ for (const [jobId, job] of jobs.entries()) {
444
+ if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) {
445
+ const jobTime = new Date(job.completedAt || job.createdAt).getTime();
446
+ if (now - jobTime > maxAge) {
447
+ jobs.delete(jobId);
448
+ removed++;
449
+ }
450
+ }
451
+ }
452
+
453
+ return removed;
454
+ }
455
+
456
+ // Clean up old jobs every 10 minutes
457
+ setInterval(() => {
458
+ cleanupOldJobs();
459
+ }, 10 * 60 * 1000);
460
+
461
+ module.exports = {
462
+ JobStatus,
463
+ initializeJobQueue,
464
+ createJob,
465
+ getJob,
466
+ listJobs,
467
+ cleanupOldJobs
468
+ };