@crypto512/jicon-mcp 0.7.1 → 1.0.1

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 (129) hide show
  1. package/README.md +128 -395
  2. package/TOOL_LIST.md +810 -120
  3. package/dist/config/constants.d.ts +1 -0
  4. package/dist/config/constants.d.ts.map +1 -1
  5. package/dist/config/constants.js +1 -0
  6. package/dist/config/constants.js.map +1 -1
  7. package/dist/config/loader.d.ts +1 -0
  8. package/dist/config/loader.d.ts.map +1 -1
  9. package/dist/config/loader.js +27 -1
  10. package/dist/config/loader.js.map +1 -1
  11. package/dist/config/types.d.ts +8 -0
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/config/types.js +2 -0
  14. package/dist/config/types.js.map +1 -1
  15. package/dist/confluence/client.d.ts +38 -0
  16. package/dist/confluence/client.d.ts.map +1 -1
  17. package/dist/confluence/client.js +117 -0
  18. package/dist/confluence/client.js.map +1 -1
  19. package/dist/confluence/tools.d.ts +102 -75
  20. package/dist/confluence/tools.d.ts.map +1 -1
  21. package/dist/confluence/tools.js +510 -151
  22. package/dist/confluence/tools.js.map +1 -1
  23. package/dist/confluence/types.d.ts +55 -1
  24. package/dist/confluence/types.d.ts.map +1 -1
  25. package/dist/index.js +88 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/jira/tools.d.ts +0 -5
  28. package/dist/jira/tools.d.ts.map +1 -1
  29. package/dist/jira/tools.js +40 -87
  30. package/dist/jira/tools.js.map +1 -1
  31. package/dist/permissions/filter.d.ts +5 -0
  32. package/dist/permissions/filter.d.ts.map +1 -1
  33. package/dist/permissions/filter.js +29 -12
  34. package/dist/permissions/filter.js.map +1 -1
  35. package/dist/permissions/tool-registry.d.ts +23 -25
  36. package/dist/permissions/tool-registry.d.ts.map +1 -1
  37. package/dist/permissions/tool-registry.js +41 -45
  38. package/dist/permissions/tool-registry.js.map +1 -1
  39. package/dist/permissions/write-home-validator.d.ts +35 -0
  40. package/dist/permissions/write-home-validator.d.ts.map +1 -0
  41. package/dist/permissions/write-home-validator.js +140 -0
  42. package/dist/permissions/write-home-validator.js.map +1 -0
  43. package/dist/tempo/tools.d.ts.map +1 -1
  44. package/dist/tempo/tools.js +43 -44
  45. package/dist/tempo/tools.js.map +1 -1
  46. package/dist/utils/buffer-tools.d.ts +119 -1
  47. package/dist/utils/buffer-tools.d.ts.map +1 -1
  48. package/dist/utils/buffer-tools.js +610 -3
  49. package/dist/utils/buffer-tools.js.map +1 -1
  50. package/dist/utils/content-buffer.d.ts +34 -0
  51. package/dist/utils/content-buffer.d.ts.map +1 -1
  52. package/dist/utils/content-buffer.js +79 -0
  53. package/dist/utils/content-buffer.js.map +1 -1
  54. package/dist/utils/http-client.d.ts.map +1 -1
  55. package/dist/utils/http-client.js +4 -4
  56. package/dist/utils/http-client.js.map +1 -1
  57. package/dist/utils/jicon-help.d.ts +29 -0
  58. package/dist/utils/jicon-help.d.ts.map +1 -0
  59. package/dist/utils/jicon-help.js +873 -0
  60. package/dist/utils/jicon-help.js.map +1 -0
  61. package/dist/utils/plantuml/client.d.ts +40 -0
  62. package/dist/utils/plantuml/client.d.ts.map +1 -0
  63. package/dist/utils/plantuml/client.js +306 -0
  64. package/dist/utils/plantuml/client.js.map +1 -0
  65. package/dist/utils/plantuml/docker-manager.d.ts +35 -0
  66. package/dist/utils/plantuml/docker-manager.d.ts.map +1 -0
  67. package/dist/utils/plantuml/docker-manager.js +280 -0
  68. package/dist/utils/plantuml/docker-manager.js.map +1 -0
  69. package/dist/utils/plantuml/index.d.ts +11 -0
  70. package/dist/utils/plantuml/index.d.ts.map +1 -0
  71. package/dist/utils/plantuml/index.js +16 -0
  72. package/dist/utils/plantuml/index.js.map +1 -0
  73. package/dist/utils/plantuml/service.d.ts +46 -0
  74. package/dist/utils/plantuml/service.d.ts.map +1 -0
  75. package/dist/utils/plantuml/service.js +96 -0
  76. package/dist/utils/plantuml/service.js.map +1 -0
  77. package/dist/utils/plantuml/tools.d.ts +65 -0
  78. package/dist/utils/plantuml/tools.d.ts.map +1 -0
  79. package/dist/utils/plantuml/tools.js +272 -0
  80. package/dist/utils/plantuml/tools.js.map +1 -0
  81. package/dist/utils/plantuml/types.d.ts +130 -0
  82. package/dist/utils/plantuml/types.d.ts.map +1 -0
  83. package/dist/utils/plantuml/types.js +66 -0
  84. package/dist/utils/plantuml/types.js.map +1 -0
  85. package/dist/utils/response-formatter.d.ts +14 -0
  86. package/dist/utils/response-formatter.d.ts.map +1 -1
  87. package/dist/utils/response-formatter.js +84 -1
  88. package/dist/utils/response-formatter.js.map +1 -1
  89. package/dist/utils/url-tools.d.ts +49 -0
  90. package/dist/utils/url-tools.d.ts.map +1 -0
  91. package/dist/utils/url-tools.js +141 -0
  92. package/dist/utils/url-tools.js.map +1 -0
  93. package/dist/utils/xhtml/confluence-schema.d.ts +55 -0
  94. package/dist/utils/xhtml/confluence-schema.d.ts.map +1 -0
  95. package/dist/utils/xhtml/confluence-schema.js +215 -0
  96. package/dist/utils/xhtml/confluence-schema.js.map +1 -0
  97. package/dist/utils/xhtml/index.d.ts +17 -0
  98. package/dist/utils/xhtml/index.d.ts.map +1 -0
  99. package/dist/utils/xhtml/index.js +21 -0
  100. package/dist/utils/xhtml/index.js.map +1 -0
  101. package/dist/utils/xhtml/operations.d.ts +100 -0
  102. package/dist/utils/xhtml/operations.d.ts.map +1 -0
  103. package/dist/utils/xhtml/operations.js +596 -0
  104. package/dist/utils/xhtml/operations.js.map +1 -0
  105. package/dist/utils/xhtml/parser.d.ts +64 -0
  106. package/dist/utils/xhtml/parser.d.ts.map +1 -0
  107. package/dist/utils/xhtml/parser.js +180 -0
  108. package/dist/utils/xhtml/parser.js.map +1 -0
  109. package/dist/utils/xhtml/plantuml.d.ts +112 -0
  110. package/dist/utils/xhtml/plantuml.d.ts.map +1 -0
  111. package/dist/utils/xhtml/plantuml.js +251 -0
  112. package/dist/utils/xhtml/plantuml.js.map +1 -0
  113. package/dist/utils/xhtml/selector.d.ts +35 -0
  114. package/dist/utils/xhtml/selector.d.ts.map +1 -0
  115. package/dist/utils/xhtml/selector.js +358 -0
  116. package/dist/utils/xhtml/selector.js.map +1 -0
  117. package/dist/utils/xhtml/serializer.d.ts +26 -0
  118. package/dist/utils/xhtml/serializer.d.ts.map +1 -0
  119. package/dist/utils/xhtml/serializer.js +170 -0
  120. package/dist/utils/xhtml/serializer.js.map +1 -0
  121. package/dist/utils/xhtml/types.d.ts +134 -0
  122. package/dist/utils/xhtml/types.d.ts.map +1 -0
  123. package/dist/utils/xhtml/types.js +65 -0
  124. package/dist/utils/xhtml/types.js.map +1 -0
  125. package/dist/utils/xhtml/validator.d.ts +67 -0
  126. package/dist/utils/xhtml/validator.d.ts.map +1 -0
  127. package/dist/utils/xhtml/validator.js +300 -0
  128. package/dist/utils/xhtml/validator.js.map +1 -0
  129. package/package.json +5 -1
@@ -2,11 +2,16 @@
2
2
  * Buffer management tools for MCP Server
3
3
  *
4
4
  * Provides tools to retrieve chunks from buffered content,
5
- * list active buffers, clear buffers, search content, and edit content.
5
+ * list active buffers, clear buffers, search content, edit content,
6
+ * and save content to files.
6
7
  */
7
8
  import { z } from "zod";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
8
11
  import { contentBuffer } from "./content-buffer.js";
9
12
  import { formatSuccess, formatError, getMaxOutputSize } from "./response-formatter.js";
13
+ import { BufferEditXhtmlSchema, parseXhtml, serializeXhtml, executeOperation, validateXhtml, validateXhtmlAsync, validatePlantUml, buildPlantUmlMacro, querySelector, updatePlantUmlInMacro, resolveSemanticPosition, findUnsupportedPseudo, } from "./xhtml/index.js";
14
+ import { validate as validatePlantUmlWithDocker, isAvailable as isPlantUmlServiceAvailable } from "./plantuml/service.js";
10
15
  /**
11
16
  * Format grep results as text output similar to grep CLI
12
17
  */
@@ -48,8 +53,65 @@ function formatGrepOutput(result, showLineNumbers) {
48
53
  }
49
54
  return lines.join("\n");
50
55
  }
56
+ /**
57
+ * Find project root by walking up directory tree looking for .jicon.json
58
+ */
59
+ function findProjectRoot() {
60
+ let dir = process.cwd();
61
+ const root = path.parse(dir).root;
62
+ while (dir !== root) {
63
+ if (fs.existsSync(path.join(dir, ".jicon.json"))) {
64
+ return dir;
65
+ }
66
+ dir = path.dirname(dir);
67
+ }
68
+ // Fallback to cwd if no marker found
69
+ return process.cwd();
70
+ }
71
+ /**
72
+ * Check if content type indicates binary data that needs base64 decoding
73
+ */
74
+ function isBinaryContentType(contentType) {
75
+ if (typeof contentType !== "string")
76
+ return false;
77
+ return [
78
+ "image/png",
79
+ "image/jpeg",
80
+ "image/gif",
81
+ "image/webp",
82
+ "application/postscript",
83
+ "application/octet-stream",
84
+ ].some((type) => contentType.includes(type));
85
+ }
51
86
  export function createBufferTools() {
52
87
  return {
88
+ buffer_create: {
89
+ description: `Create a new buffer with initial content. Use this to draft new content (e.g., for new Confluence pages) before persisting. Returns bufferId for use with buffer_edit and other buffer tools.`,
90
+ inputSchema: z.object({
91
+ content: z.string().describe("Initial content for the buffer"),
92
+ metadata: z
93
+ .record(z.unknown())
94
+ .optional()
95
+ .describe("Optional metadata to attach to the buffer"),
96
+ }),
97
+ handler: async (args) => {
98
+ try {
99
+ const bufferId = contentBuffer.store(args.content, args.metadata);
100
+ const info = contentBuffer.getInfo(bufferId);
101
+ return formatSuccess({
102
+ bufferId,
103
+ totalSize: info?.totalSize ?? args.content.length,
104
+ createdAt: info ? new Date(info.createdAt).toISOString() : new Date().toISOString(),
105
+ expiresAt: info ? new Date(info.expiresAt).toISOString() : undefined,
106
+ metadata: args.metadata,
107
+ message: "Buffer created. Use buffer_edit to modify, buffer_get_chunk to read.",
108
+ });
109
+ }
110
+ catch (error) {
111
+ return formatError(error instanceof Error ? error : new Error(String(error)));
112
+ }
113
+ },
114
+ },
53
115
  buffer_get_chunk: {
54
116
  description: `Retrieve a chunk of buffered content by buffer ID. Use after receiving a bufferId from tools. Returns content, offset, totalSize, and hasMore flag.`,
55
117
  inputSchema: z.object({
@@ -91,7 +153,18 @@ export function createBufferTools() {
91
153
  },
92
154
  },
93
155
  buffer_list: {
94
- description: `List all active buffers with metadata. Buffers expire after 10 minutes.`,
156
+ description: `List all active buffers with metadata. Buffers expire after 10 minutes.
157
+
158
+ Use to recover lost buffer IDs or find buffers associated with Confluence drafts.
159
+
160
+ Metadata includes:
161
+ - resourceType: "confluence_page" for Confluence content
162
+ - resourceId: page/draft ID
163
+ - isDraft: true for unsaved drafts
164
+ - title, spaceKey: page info
165
+ - contentType: "xhtml", "plain", or "json"
166
+
167
+ Example: Find buffer for draft 12345 → look for metadata.resourceId === "12345"`,
95
168
  inputSchema: z.object({}),
96
169
  handler: async () => {
97
170
  try {
@@ -270,7 +343,9 @@ export function createBufferTools() {
270
343
  },
271
344
  },
272
345
  buffer_edit: {
273
- description: `Exact string replacement in buffered content. Fails if old_string is not unique (use replace_all=true for all occurrences). Changes are in-memory only - call update API to persist.`,
346
+ description: `Exact string replacement in buffered content. Fails if old_string is not unique (use replace_all=true for all occurrences). Changes are in-memory only - call update API to persist.
347
+
348
+ For Confluence content: Use buffer_edit_xhtml instead for structure-aware editing (tables, macros, layouts). Call help(topic="storage") for guide.`,
274
349
  inputSchema: z.object({
275
350
  bufferId: z.string().describe("Buffer ID to modify"),
276
351
  old_string: z.string().describe("Exact text to replace"),
@@ -283,6 +358,19 @@ export function createBufferTools() {
283
358
  }),
284
359
  handler: async (args) => {
285
360
  try {
361
+ // Check if buffer contains XHTML content - reject to force use of buffer_edit_xhtml
362
+ const bufferInfo = contentBuffer.getInfo(args.bufferId);
363
+ if (bufferInfo?.metadata?.contentType === "xhtml") {
364
+ return formatError({
365
+ error: true,
366
+ message: "This buffer contains Confluence XHTML content. Use buffer_edit_xhtml instead for proper structure-aware editing.",
367
+ statusCode: 400,
368
+ details: {
369
+ hint: "For PlantUML diagrams, use: buffer_edit_xhtml(bufferId, operation='insert-plantuml', ...)",
370
+ contentType: "xhtml",
371
+ },
372
+ });
373
+ }
286
374
  const result = contentBuffer.edit(args.bufferId, args.old_string, args.new_string, args.replace_all ?? false);
287
375
  if (!result) {
288
376
  return formatError({
@@ -307,6 +395,525 @@ export function createBufferTools() {
307
395
  }
308
396
  },
309
397
  },
398
+ buffer_edit_xhtml: {
399
+ description: `Structure-aware XHTML editing for Confluence storage format. Supports CSS-like selectors to target elements, with operations: insert, insert-plantuml, update, update-plantuml, remove, move, wrap.
400
+
401
+ Selector syntax:
402
+ - Tag: 'p', 'h1', 'table'
403
+ - Namespaced: 'ac:structured-macro', 'ri:page'
404
+ - Attributes: '[ac:name="plantuml"]', 'ac:structured-macro[ac:name="code"]'
405
+ - Pseudo: ':nth-child(2)', ':first-child', ':last-child'
406
+ - Descendants: 'table tbody tr'
407
+ - Direct child: 'ul > li'
408
+
409
+ Semantic positions (for insert-plantuml, alternative to selector):
410
+ - 'after-title': After first h1 or h2
411
+ - 'after-heading': After first heading (h1-h6)
412
+ - 'before-content': At document start
413
+ - 'end': At document end
414
+ - 'after-toc': After table of contents macro
415
+
416
+ PlantUML: Use plantuml_validate first for full Docker-based validation. This tool uses basic sync validation.`,
417
+ inputSchema: BufferEditXhtmlSchema,
418
+ handler: async (args) => {
419
+ try {
420
+ // Get buffer content
421
+ const bufferInfo = contentBuffer.getInfo(args.bufferId);
422
+ if (!bufferInfo) {
423
+ return formatError({
424
+ error: true,
425
+ message: `Buffer not found or expired: ${args.bufferId}`,
426
+ statusCode: 404,
427
+ });
428
+ }
429
+ const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
430
+ if (!chunk) {
431
+ return formatError({
432
+ error: true,
433
+ message: `Failed to read buffer: ${args.bufferId}`,
434
+ statusCode: 500,
435
+ });
436
+ }
437
+ const originalContent = chunk.chunk;
438
+ const oldSize = originalContent.length;
439
+ // Parse XHTML
440
+ const parseResult = parseXhtml(originalContent);
441
+ if (!parseResult.document) {
442
+ return formatError({
443
+ error: true,
444
+ message: `Failed to parse XHTML: ${parseResult.error?.message}`,
445
+ statusCode: 400,
446
+ details: {
447
+ parseError: parseResult.error?.message,
448
+ line: parseResult.error?.line,
449
+ column: parseResult.error?.column,
450
+ context: parseResult.error?.context,
451
+ hint: parseResult.error?.context
452
+ ? `Check near: "${parseResult.error.context.substring(0, 60)}${parseResult.error.context.length > 60 ? "..." : ""}"`
453
+ : undefined,
454
+ },
455
+ });
456
+ }
457
+ // Handle PlantUML operations specially
458
+ let contentToInsert = args.content;
459
+ const operation = args.operation;
460
+ let resolvedSelector = args.selector;
461
+ let resolvedPosition = args.position;
462
+ let insertDiagramType; // Track diagram type for insert-plantuml
463
+ // Check if we need to derive selector from semanticPosition
464
+ if (!resolvedSelector && args.semanticPosition) {
465
+ const resolved = resolveSemanticPosition(parseResult.document, args.semanticPosition);
466
+ if (resolved) {
467
+ resolvedSelector = resolved.selector;
468
+ resolvedPosition = resolved.position;
469
+ }
470
+ else {
471
+ return formatError({
472
+ error: true,
473
+ message: `Could not resolve semantic position: ${args.semanticPosition}`,
474
+ statusCode: 400,
475
+ details: {
476
+ semanticPosition: args.semanticPosition,
477
+ hint: "Document may be empty or missing required elements",
478
+ },
479
+ });
480
+ }
481
+ }
482
+ // Check that we have a selector (either provided or derived from semanticPosition)
483
+ if (!resolvedSelector) {
484
+ return formatError({
485
+ error: true,
486
+ message: "Either selector or semanticPosition is required",
487
+ statusCode: 400,
488
+ details: {
489
+ hint: "Provide selector (e.g., 'h1', 'table', 'ac:structured-macro[ac:name=\"plantuml\"]') or semanticPosition (e.g., 'after-title', 'end')",
490
+ },
491
+ });
492
+ }
493
+ if (operation === "insert-plantuml") {
494
+ if (!args.plantuml) {
495
+ return formatError({
496
+ error: true,
497
+ message: "plantuml parameter is required for insert-plantuml operation",
498
+ statusCode: 400,
499
+ });
500
+ }
501
+ // Validate PlantUML - use Docker service if available, else fallback to basic validation
502
+ let normalizedCode;
503
+ if (isPlantUmlServiceAvailable()) {
504
+ // Use Docker-based validation
505
+ try {
506
+ const dockerValidation = await validatePlantUmlWithDocker(args.plantuml);
507
+ if (!dockerValidation.valid) {
508
+ return formatError({
509
+ error: true,
510
+ message: "PlantUML syntax error",
511
+ statusCode: 400,
512
+ details: {
513
+ plantumlErrors: dockerValidation.errors,
514
+ diagramType: dockerValidation.diagramType,
515
+ hint: "Check plantuml_validate for detailed error info",
516
+ },
517
+ });
518
+ }
519
+ normalizedCode = dockerValidation.normalizedCode;
520
+ insertDiagramType = dockerValidation.diagramType;
521
+ }
522
+ catch (error) {
523
+ // Docker error - fallback to basic validation
524
+ const basicValidation = validatePlantUml(args.plantuml);
525
+ if (!basicValidation.valid) {
526
+ return formatError({
527
+ error: true,
528
+ message: "PlantUML syntax error",
529
+ statusCode: 400,
530
+ details: {
531
+ plantumlError: basicValidation.error,
532
+ },
533
+ });
534
+ }
535
+ normalizedCode = basicValidation.normalizedCode;
536
+ insertDiagramType = basicValidation.diagramType;
537
+ }
538
+ }
539
+ else {
540
+ // Use basic synchronous validation
541
+ const basicValidation = validatePlantUml(args.plantuml);
542
+ if (!basicValidation.valid) {
543
+ return formatError({
544
+ error: true,
545
+ message: "PlantUML syntax error",
546
+ statusCode: 400,
547
+ details: {
548
+ plantumlError: basicValidation.error,
549
+ hint: "Use plantuml_validate for full Docker-based validation",
550
+ },
551
+ });
552
+ }
553
+ normalizedCode = basicValidation.normalizedCode;
554
+ insertDiagramType = basicValidation.diagramType;
555
+ }
556
+ // Build the macro
557
+ contentToInsert = buildPlantUmlMacro(normalizedCode, args.macroId);
558
+ }
559
+ else if (operation === "update-plantuml") {
560
+ if (!args.plantuml) {
561
+ return formatError({
562
+ error: true,
563
+ message: "plantuml parameter is required for update-plantuml operation",
564
+ statusCode: 400,
565
+ });
566
+ }
567
+ // Validate PlantUML - use Docker service if available, else fallback to basic validation
568
+ let normalizedCode;
569
+ let updateDiagramType;
570
+ if (isPlantUmlServiceAvailable()) {
571
+ // Use Docker-based validation
572
+ try {
573
+ const dockerValidation = await validatePlantUmlWithDocker(args.plantuml);
574
+ if (!dockerValidation.valid) {
575
+ return formatError({
576
+ error: true,
577
+ message: "PlantUML syntax error",
578
+ statusCode: 400,
579
+ details: {
580
+ plantumlErrors: dockerValidation.errors,
581
+ diagramType: dockerValidation.diagramType,
582
+ hint: "Check plantuml_validate for detailed error info",
583
+ },
584
+ });
585
+ }
586
+ normalizedCode = dockerValidation.normalizedCode;
587
+ updateDiagramType = dockerValidation.diagramType;
588
+ }
589
+ catch (error) {
590
+ // Docker error - fallback to basic validation
591
+ const basicValidation = validatePlantUml(args.plantuml);
592
+ if (!basicValidation.valid) {
593
+ return formatError({
594
+ error: true,
595
+ message: "PlantUML syntax error",
596
+ statusCode: 400,
597
+ details: {
598
+ plantumlError: basicValidation.error,
599
+ },
600
+ });
601
+ }
602
+ normalizedCode = basicValidation.normalizedCode;
603
+ updateDiagramType = basicValidation.diagramType;
604
+ }
605
+ }
606
+ else {
607
+ // Use basic synchronous validation
608
+ const basicValidation = validatePlantUml(args.plantuml);
609
+ if (!basicValidation.valid) {
610
+ return formatError({
611
+ error: true,
612
+ message: "PlantUML syntax error",
613
+ statusCode: 400,
614
+ details: {
615
+ plantumlError: basicValidation.error,
616
+ hint: "Use plantuml_validate for full Docker-based validation",
617
+ },
618
+ });
619
+ }
620
+ normalizedCode = basicValidation.normalizedCode;
621
+ updateDiagramType = basicValidation.diagramType;
622
+ }
623
+ // Find the macro element and update it directly
624
+ const selectorResult = querySelector(parseResult.document, args.selector);
625
+ if (selectorResult.matches.length === 0) {
626
+ const unsupportedPseudo = findUnsupportedPseudo(args.selector);
627
+ const hint = unsupportedPseudo
628
+ ? `Pseudo-selector ':${unsupportedPseudo}' is not supported. Use ':contains("text")' to find elements by text content, or use 'ac:structured-macro[ac:name="plantuml"]' with matchIndex.`
629
+ : "Use 'ac:structured-macro[ac:name=\"plantuml\"]' to find PlantUML macros, or use matchIndex if multiple exist.";
630
+ return formatError({
631
+ error: true,
632
+ message: `No elements match selector: ${args.selector}`,
633
+ statusCode: 400,
634
+ details: { selector: args.selector, matchCount: 0, hint },
635
+ });
636
+ }
637
+ const targetIndex = args.matchIndex ?? 0;
638
+ if (targetIndex >= selectorResult.matches.length) {
639
+ return formatError({
640
+ error: true,
641
+ message: `matchIndex ${targetIndex} out of range (${selectorResult.matches.length} matches)`,
642
+ statusCode: 400,
643
+ });
644
+ }
645
+ const macroElement = selectorResult.matches[targetIndex].element;
646
+ // Verify it's a plantuml macro
647
+ if (macroElement.tagName.toLowerCase() !== "ac:structured-macro" ||
648
+ macroElement.getAttribute("ac:name") !== "plantuml") {
649
+ return formatError({
650
+ error: true,
651
+ message: "Selected element is not a PlantUML macro",
652
+ statusCode: 400,
653
+ });
654
+ }
655
+ // Update the macro content
656
+ const updated = updatePlantUmlInMacro(macroElement, normalizedCode);
657
+ if (!updated) {
658
+ return formatError({
659
+ error: true,
660
+ message: "Failed to update PlantUML macro content",
661
+ statusCode: 500,
662
+ });
663
+ }
664
+ // Serialize and update buffer
665
+ const newContent = serializeXhtml(parseResult.document);
666
+ // Validate if requested
667
+ if (args.validate !== false) {
668
+ const validationResult = validateXhtml(newContent);
669
+ if (!validationResult.valid) {
670
+ const firstError = validationResult.errors[0];
671
+ return formatError({
672
+ error: true,
673
+ message: `Resulting XHTML is invalid: ${firstError?.message || "Unknown error"}`,
674
+ statusCode: 400,
675
+ details: {
676
+ validationErrors: validationResult.errors,
677
+ hint: firstError?.location?.context
678
+ ? `Check near: "${firstError.location.context.substring(0, 60)}${firstError.location.context.length > 60 ? "..." : ""}"`
679
+ : undefined,
680
+ },
681
+ });
682
+ }
683
+ }
684
+ // Update buffer in place (keeps same buffer ID)
685
+ contentBuffer.update(args.bufferId, newContent, { contentType: "xhtml" });
686
+ return formatSuccess({
687
+ bufferId: args.bufferId,
688
+ success: true,
689
+ operation: "update-plantuml",
690
+ matchCount: 1,
691
+ oldSize,
692
+ newSize: newContent.length,
693
+ affectedElements: ["ac:structured-macro"],
694
+ diagramType: updateDiagramType,
695
+ });
696
+ }
697
+ // Execute the operation
698
+ const result = executeOperation(parseResult.document, operation, resolvedSelector, {
699
+ position: resolvedPosition,
700
+ content: contentToInsert,
701
+ attributes: args.attributes,
702
+ targetSelector: args.targetSelector,
703
+ matchIndex: args.matchIndex,
704
+ matchAll: args.matchAll,
705
+ });
706
+ if ("error" in result && result.error) {
707
+ return formatError({
708
+ error: true,
709
+ message: result.message,
710
+ statusCode: 400,
711
+ details: result.details,
712
+ });
713
+ }
714
+ // Serialize back to XHTML
715
+ const newContent = serializeXhtml(parseResult.document);
716
+ // Validate if requested
717
+ if (args.validate !== false) {
718
+ const validationResult = validateXhtml(newContent);
719
+ if (!validationResult.valid) {
720
+ const firstError = validationResult.errors[0];
721
+ return formatError({
722
+ error: true,
723
+ message: `Resulting XHTML is invalid: ${firstError?.message || "Unknown error"}`,
724
+ statusCode: 400,
725
+ details: {
726
+ validationErrors: validationResult.errors,
727
+ hint: firstError?.location?.context
728
+ ? `Check near: "${firstError.location.context.substring(0, 60)}${firstError.location.context.length > 60 ? "..." : ""}"`
729
+ : undefined,
730
+ },
731
+ });
732
+ }
733
+ }
734
+ // Update buffer in place (keeps same buffer ID)
735
+ contentBuffer.update(args.bufferId, newContent, { contentType: "xhtml" });
736
+ // Check if buffer is associated with a Confluence draft and add nextStep hint
737
+ const updatedBufferInfo = contentBuffer.getInfo(args.bufferId);
738
+ const isDraft = updatedBufferInfo?.metadata?.isDraft === true;
739
+ const isConfluencePage = updatedBufferInfo?.metadata?.resourceType === "confluence_page";
740
+ return formatSuccess({
741
+ ...result,
742
+ bufferId: args.bufferId,
743
+ oldSize,
744
+ newSize: newContent.length,
745
+ ...(insertDiagramType && { diagramType: insertDiagramType }),
746
+ ...((isDraft || isConfluencePage) && {
747
+ nextStep: "Call confluence_draft_save(bufferId) to persist changes to Confluence",
748
+ }),
749
+ });
750
+ }
751
+ catch (error) {
752
+ return formatError(error instanceof Error ? error : new Error(String(error)));
753
+ }
754
+ },
755
+ },
756
+ buffer_validate_xhtml: {
757
+ description: `Validate buffered content as Confluence storage format (XHTML).
758
+
759
+ Checks:
760
+ - XML well-formedness (balanced tags, proper nesting)
761
+ - Required attributes on Confluence elements (ac:name, ri:space-key, etc.)
762
+ - Valid layout section types (single, two_equal, etc.)
763
+ - Known macro names (warns for unknown macros)
764
+ - PlantUML syntax (via Docker service, if running)
765
+
766
+ PlantUML validation:
767
+ - If Docker service is running, validates all PlantUML macros
768
+ - If not running, adds warning (use plantuml_validate to start the service)
769
+
770
+ Use this to validate content before calling confluence_update_page or confluence_create_page.`,
771
+ inputSchema: z.object({
772
+ bufferId: z.string().describe("Buffer ID containing XHTML content"),
773
+ validatePlantUml: z
774
+ .boolean()
775
+ .optional()
776
+ .default(true)
777
+ .describe("Validate PlantUML syntax via Docker service (default: true)"),
778
+ }),
779
+ handler: async (args) => {
780
+ try {
781
+ // Get buffer content
782
+ const bufferInfo = contentBuffer.getInfo(args.bufferId);
783
+ if (!bufferInfo) {
784
+ return formatError({
785
+ error: true,
786
+ message: `Buffer not found or expired: ${args.bufferId}`,
787
+ statusCode: 404,
788
+ });
789
+ }
790
+ const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
791
+ if (!chunk) {
792
+ return formatError({
793
+ error: true,
794
+ message: `Failed to read buffer: ${args.bufferId}`,
795
+ statusCode: 500,
796
+ });
797
+ }
798
+ // Validate XHTML with async PlantUML validation
799
+ const validationResult = await validateXhtmlAsync(chunk.chunk, {
800
+ validatePlantUml: args.validatePlantUml !== false,
801
+ });
802
+ return formatSuccess({
803
+ bufferId: args.bufferId,
804
+ valid: validationResult.valid,
805
+ errorCount: validationResult.errors.length,
806
+ warningCount: validationResult.warnings.length,
807
+ errors: validationResult.errors.map(e => ({
808
+ type: e.type,
809
+ message: e.message,
810
+ location: e.location,
811
+ })),
812
+ warnings: validationResult.warnings.map(w => ({
813
+ type: w.type,
814
+ message: w.message,
815
+ location: w.location,
816
+ })),
817
+ plantuml: validationResult.plantuml,
818
+ });
819
+ }
820
+ catch (error) {
821
+ return formatError(error instanceof Error ? error : new Error(String(error)));
822
+ }
823
+ },
824
+ },
825
+ buffer_save_to_file: {
826
+ description: `Save buffer content to a file within the project directory.
827
+
828
+ SECURITY: Files can only be saved within the project root (directory containing .jicon.json).
829
+ Attempts to write outside this directory will be rejected.
830
+
831
+ Binary handling:
832
+ - If buffer metadata indicates binary content (image/png, application/postscript, etc.),
833
+ content is automatically decoded from base64
834
+ - Use decodeBase64=true to force base64 decoding
835
+ - PNG and EPS from plantuml_render are base64 encoded and will be auto-decoded
836
+
837
+ Example:
838
+ buffer_save_to_file(bufferId="buf_xxx", outputPath="./diagrams/sequence.png")
839
+ buffer_save_to_file(bufferId="buf_yyy", outputPath="output/diagram.svg")`,
840
+ inputSchema: z.object({
841
+ bufferId: z.string().describe("Buffer ID containing content to save"),
842
+ outputPath: z
843
+ .string()
844
+ .describe("Output file path relative to project root, or absolute path within project"),
845
+ decodeBase64: z
846
+ .boolean()
847
+ .optional()
848
+ .describe("Force base64 decoding (auto-detected for binary content types like PNG/EPS)"),
849
+ }),
850
+ handler: async (args) => {
851
+ try {
852
+ // 1. Validate buffer exists
853
+ const info = contentBuffer.getInfo(args.bufferId);
854
+ if (!info) {
855
+ return formatError({
856
+ error: true,
857
+ message: `Buffer not found: ${args.bufferId}`,
858
+ statusCode: 404,
859
+ });
860
+ }
861
+ // 2. Find project root
862
+ const projectRoot = findProjectRoot();
863
+ // 3. Resolve and validate output path
864
+ const resolvedPath = path.resolve(projectRoot, args.outputPath);
865
+ const normalizedProjectRoot = path.normalize(projectRoot);
866
+ const normalizedOutputPath = path.normalize(resolvedPath);
867
+ // Security check: ensure path is within project directory
868
+ // Must start with projectRoot + separator, or be exactly projectRoot
869
+ if (!normalizedOutputPath.startsWith(normalizedProjectRoot + path.sep) &&
870
+ normalizedOutputPath !== normalizedProjectRoot) {
871
+ return formatError({
872
+ error: true,
873
+ message: "Security: output path must be within project directory",
874
+ statusCode: 403,
875
+ details: {
876
+ projectRoot: normalizedProjectRoot,
877
+ requestedPath: normalizedOutputPath,
878
+ hint: "Use a path relative to project root (e.g., './output/file.png')",
879
+ },
880
+ });
881
+ }
882
+ // 4. Get buffer content
883
+ const chunk = contentBuffer.getChunk(args.bufferId, 0, info.totalSize);
884
+ if (!chunk) {
885
+ return formatError({
886
+ error: true,
887
+ message: `Failed to read buffer: ${args.bufferId}`,
888
+ statusCode: 500,
889
+ });
890
+ }
891
+ // 5. Determine if base64 decoding needed
892
+ const shouldDecode = args.decodeBase64 === true ||
893
+ (args.decodeBase64 !== false &&
894
+ isBinaryContentType(info.metadata?.contentType));
895
+ // 6. Prepare content
896
+ const content = shouldDecode
897
+ ? Buffer.from(chunk.chunk, "base64")
898
+ : Buffer.from(chunk.chunk, "utf-8");
899
+ // 7. Ensure parent directory exists
900
+ const parentDir = path.dirname(resolvedPath);
901
+ await fs.promises.mkdir(parentDir, { recursive: true });
902
+ // 8. Write file
903
+ await fs.promises.writeFile(resolvedPath, content);
904
+ return formatSuccess({
905
+ success: true,
906
+ path: resolvedPath,
907
+ size: content.length,
908
+ decoded: shouldDecode,
909
+ message: `File saved successfully to ${resolvedPath}`,
910
+ });
911
+ }
912
+ catch (error) {
913
+ return formatError(error instanceof Error ? error : new Error(String(error)));
914
+ }
915
+ },
916
+ },
310
917
  };
311
918
  }
312
919
  //# sourceMappingURL=buffer-tools.js.map