@crypto512/jicon-mcp 0.7.1 → 1.0.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.
- package/README.md +128 -395
- package/TOOL_LIST.md +810 -120
- package/dist/config/constants.d.ts +1 -0
- package/dist/config/constants.d.ts.map +1 -1
- package/dist/config/constants.js +1 -0
- package/dist/config/constants.js.map +1 -1
- package/dist/config/loader.d.ts +1 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +27 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +8 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -1
- package/dist/confluence/client.d.ts +38 -0
- package/dist/confluence/client.d.ts.map +1 -1
- package/dist/confluence/client.js +117 -0
- package/dist/confluence/client.js.map +1 -1
- package/dist/confluence/tools.d.ts +102 -75
- package/dist/confluence/tools.d.ts.map +1 -1
- package/dist/confluence/tools.js +510 -151
- package/dist/confluence/tools.js.map +1 -1
- package/dist/confluence/types.d.ts +55 -1
- package/dist/confluence/types.d.ts.map +1 -1
- package/dist/index.js +88 -2
- package/dist/index.js.map +1 -1
- package/dist/jira/tools.d.ts +0 -5
- package/dist/jira/tools.d.ts.map +1 -1
- package/dist/jira/tools.js +40 -87
- package/dist/jira/tools.js.map +1 -1
- package/dist/permissions/filter.d.ts +5 -0
- package/dist/permissions/filter.d.ts.map +1 -1
- package/dist/permissions/filter.js +29 -12
- package/dist/permissions/filter.js.map +1 -1
- package/dist/permissions/tool-registry.d.ts +23 -25
- package/dist/permissions/tool-registry.d.ts.map +1 -1
- package/dist/permissions/tool-registry.js +41 -45
- package/dist/permissions/tool-registry.js.map +1 -1
- package/dist/permissions/write-home-validator.d.ts +35 -0
- package/dist/permissions/write-home-validator.d.ts.map +1 -0
- package/dist/permissions/write-home-validator.js +140 -0
- package/dist/permissions/write-home-validator.js.map +1 -0
- package/dist/tempo/tools.d.ts.map +1 -1
- package/dist/tempo/tools.js +43 -44
- package/dist/tempo/tools.js.map +1 -1
- package/dist/utils/buffer-tools.d.ts +119 -1
- package/dist/utils/buffer-tools.d.ts.map +1 -1
- package/dist/utils/buffer-tools.js +610 -3
- package/dist/utils/buffer-tools.js.map +1 -1
- package/dist/utils/content-buffer.d.ts +34 -0
- package/dist/utils/content-buffer.d.ts.map +1 -1
- package/dist/utils/content-buffer.js +79 -0
- package/dist/utils/content-buffer.js.map +1 -1
- package/dist/utils/http-client.d.ts.map +1 -1
- package/dist/utils/http-client.js +4 -4
- package/dist/utils/http-client.js.map +1 -1
- package/dist/utils/jicon-help.d.ts +29 -0
- package/dist/utils/jicon-help.d.ts.map +1 -0
- package/dist/utils/jicon-help.js +873 -0
- package/dist/utils/jicon-help.js.map +1 -0
- package/dist/utils/plantuml/client.d.ts +40 -0
- package/dist/utils/plantuml/client.d.ts.map +1 -0
- package/dist/utils/plantuml/client.js +306 -0
- package/dist/utils/plantuml/client.js.map +1 -0
- package/dist/utils/plantuml/docker-manager.d.ts +35 -0
- package/dist/utils/plantuml/docker-manager.d.ts.map +1 -0
- package/dist/utils/plantuml/docker-manager.js +280 -0
- package/dist/utils/plantuml/docker-manager.js.map +1 -0
- package/dist/utils/plantuml/index.d.ts +11 -0
- package/dist/utils/plantuml/index.d.ts.map +1 -0
- package/dist/utils/plantuml/index.js +16 -0
- package/dist/utils/plantuml/index.js.map +1 -0
- package/dist/utils/plantuml/service.d.ts +46 -0
- package/dist/utils/plantuml/service.d.ts.map +1 -0
- package/dist/utils/plantuml/service.js +96 -0
- package/dist/utils/plantuml/service.js.map +1 -0
- package/dist/utils/plantuml/tools.d.ts +65 -0
- package/dist/utils/plantuml/tools.d.ts.map +1 -0
- package/dist/utils/plantuml/tools.js +272 -0
- package/dist/utils/plantuml/tools.js.map +1 -0
- package/dist/utils/plantuml/types.d.ts +130 -0
- package/dist/utils/plantuml/types.d.ts.map +1 -0
- package/dist/utils/plantuml/types.js +66 -0
- package/dist/utils/plantuml/types.js.map +1 -0
- package/dist/utils/response-formatter.d.ts +14 -0
- package/dist/utils/response-formatter.d.ts.map +1 -1
- package/dist/utils/response-formatter.js +84 -1
- package/dist/utils/response-formatter.js.map +1 -1
- package/dist/utils/url-tools.d.ts +49 -0
- package/dist/utils/url-tools.d.ts.map +1 -0
- package/dist/utils/url-tools.js +141 -0
- package/dist/utils/url-tools.js.map +1 -0
- package/dist/utils/xhtml/confluence-schema.d.ts +55 -0
- package/dist/utils/xhtml/confluence-schema.d.ts.map +1 -0
- package/dist/utils/xhtml/confluence-schema.js +215 -0
- package/dist/utils/xhtml/confluence-schema.js.map +1 -0
- package/dist/utils/xhtml/index.d.ts +17 -0
- package/dist/utils/xhtml/index.d.ts.map +1 -0
- package/dist/utils/xhtml/index.js +21 -0
- package/dist/utils/xhtml/index.js.map +1 -0
- package/dist/utils/xhtml/operations.d.ts +100 -0
- package/dist/utils/xhtml/operations.d.ts.map +1 -0
- package/dist/utils/xhtml/operations.js +596 -0
- package/dist/utils/xhtml/operations.js.map +1 -0
- package/dist/utils/xhtml/parser.d.ts +64 -0
- package/dist/utils/xhtml/parser.d.ts.map +1 -0
- package/dist/utils/xhtml/parser.js +180 -0
- package/dist/utils/xhtml/parser.js.map +1 -0
- package/dist/utils/xhtml/plantuml.d.ts +112 -0
- package/dist/utils/xhtml/plantuml.d.ts.map +1 -0
- package/dist/utils/xhtml/plantuml.js +251 -0
- package/dist/utils/xhtml/plantuml.js.map +1 -0
- package/dist/utils/xhtml/selector.d.ts +35 -0
- package/dist/utils/xhtml/selector.d.ts.map +1 -0
- package/dist/utils/xhtml/selector.js +358 -0
- package/dist/utils/xhtml/selector.js.map +1 -0
- package/dist/utils/xhtml/serializer.d.ts +26 -0
- package/dist/utils/xhtml/serializer.d.ts.map +1 -0
- package/dist/utils/xhtml/serializer.js +170 -0
- package/dist/utils/xhtml/serializer.js.map +1 -0
- package/dist/utils/xhtml/types.d.ts +134 -0
- package/dist/utils/xhtml/types.d.ts.map +1 -0
- package/dist/utils/xhtml/types.js +65 -0
- package/dist/utils/xhtml/types.js.map +1 -0
- package/dist/utils/xhtml/validator.d.ts +67 -0
- package/dist/utils/xhtml/validator.d.ts.map +1 -0
- package/dist/utils/xhtml/validator.js +300 -0
- package/dist/utils/xhtml/validator.js.map +1 -0
- 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,
|
|
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
|