revxl-devtools 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 +84 -0
- package/checkout/index.html +195 -0
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +77 -0
- package/dist/codegen/cron-codegen.d.ts +1 -0
- package/dist/codegen/cron-codegen.js +56 -0
- package/dist/codegen/regex-codegen.d.ts +1 -0
- package/dist/codegen/regex-codegen.js +125 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +100 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.js +13 -0
- package/dist/tools/base64.d.ts +1 -0
- package/dist/tools/base64.js +29 -0
- package/dist/tools/batch.d.ts +1 -0
- package/dist/tools/batch.js +56 -0
- package/dist/tools/chmod.d.ts +1 -0
- package/dist/tools/chmod.js +115 -0
- package/dist/tools/cron.d.ts +1 -0
- package/dist/tools/cron.js +311 -0
- package/dist/tools/hash.d.ts +1 -0
- package/dist/tools/hash.js +25 -0
- package/dist/tools/http-status.d.ts +1 -0
- package/dist/tools/http-status.js +59 -0
- package/dist/tools/json-diff.d.ts +1 -0
- package/dist/tools/json-diff.js +131 -0
- package/dist/tools/json-format.d.ts +1 -0
- package/dist/tools/json-format.js +38 -0
- package/dist/tools/json-query.d.ts +1 -0
- package/dist/tools/json-query.js +114 -0
- package/dist/tools/jwt.d.ts +1 -0
- package/dist/tools/jwt.js +177 -0
- package/dist/tools/regex.d.ts +1 -0
- package/dist/tools/regex.js +116 -0
- package/dist/tools/secrets-scan.d.ts +1 -0
- package/dist/tools/secrets-scan.js +173 -0
- package/dist/tools/sql-format.d.ts +1 -0
- package/dist/tools/sql-format.js +157 -0
- package/dist/tools/timestamp.d.ts +1 -0
- package/dist/tools/timestamp.js +72 -0
- package/dist/tools/url-encode.d.ts +1 -0
- package/dist/tools/url-encode.js +26 -0
- package/dist/tools/uuid.d.ts +1 -0
- package/dist/tools/uuid.js +24 -0
- package/dist/tools/yaml-convert.d.ts +1 -0
- package/dist/tools/yaml-convert.js +371 -0
- package/package.json +29 -0
- package/src/auth.ts +99 -0
- package/src/codegen/cron-codegen.ts +66 -0
- package/src/codegen/regex-codegen.ts +132 -0
- package/src/index.ts +134 -0
- package/src/registry.ts +25 -0
- package/src/tools/base64.ts +32 -0
- package/src/tools/batch.ts +69 -0
- package/src/tools/chmod.ts +133 -0
- package/src/tools/cron.ts +365 -0
- package/src/tools/hash.ts +26 -0
- package/src/tools/http-status.ts +63 -0
- package/src/tools/json-diff.ts +153 -0
- package/src/tools/json-format.ts +43 -0
- package/src/tools/json-query.ts +126 -0
- package/src/tools/jwt.ts +193 -0
- package/src/tools/regex.ts +131 -0
- package/src/tools/secrets-scan.ts +212 -0
- package/src/tools/sql-format.ts +178 -0
- package/src/tools/timestamp.ts +74 -0
- package/src/tools/url-encode.ts +29 -0
- package/src/tools/uuid.ts +25 -0
- package/src/tools/yaml-convert.ts +383 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { registerTool } from "../registry.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// SQL Formatter — normalize whitespace, add newlines before major clauses,
|
|
5
|
+
// indent columns and conditions
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
const MAJOR_CLAUSES = [
|
|
9
|
+
"SELECT",
|
|
10
|
+
"FROM",
|
|
11
|
+
"WHERE",
|
|
12
|
+
"INNER JOIN",
|
|
13
|
+
"LEFT JOIN",
|
|
14
|
+
"RIGHT JOIN",
|
|
15
|
+
"FULL JOIN",
|
|
16
|
+
"CROSS JOIN",
|
|
17
|
+
"JOIN",
|
|
18
|
+
"ON",
|
|
19
|
+
"GROUP BY",
|
|
20
|
+
"ORDER BY",
|
|
21
|
+
"HAVING",
|
|
22
|
+
"LIMIT",
|
|
23
|
+
"OFFSET",
|
|
24
|
+
"INSERT INTO",
|
|
25
|
+
"VALUES",
|
|
26
|
+
"UPDATE",
|
|
27
|
+
"SET",
|
|
28
|
+
"DELETE FROM",
|
|
29
|
+
"WITH",
|
|
30
|
+
"UNION ALL",
|
|
31
|
+
"UNION",
|
|
32
|
+
"EXCEPT",
|
|
33
|
+
"INTERSECT",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function formatSQL(sql: string, _dialect: string): string {
|
|
37
|
+
// Normalize whitespace: collapse runs of whitespace into single space
|
|
38
|
+
let normalized = sql.replace(/\s+/g, " ").trim();
|
|
39
|
+
|
|
40
|
+
// Uppercase major clauses and add newlines before them
|
|
41
|
+
// Sort clauses longest-first so multi-word clauses match before single-word
|
|
42
|
+
const sortedClauses = [...MAJOR_CLAUSES].sort(
|
|
43
|
+
(a, b) => b.length - a.length,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
for (const clause of sortedClauses) {
|
|
47
|
+
const pattern = new RegExp(`\\b${clause}\\b`, "gi");
|
|
48
|
+
normalized = normalized.replace(pattern, `\n${clause}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Clean up: remove leading newline
|
|
52
|
+
normalized = normalized.replace(/^\n/, "");
|
|
53
|
+
|
|
54
|
+
// Split into lines for indentation processing
|
|
55
|
+
const lines = normalized.split("\n").map((l) => l.trim());
|
|
56
|
+
const result: string[] = [];
|
|
57
|
+
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
if (!line) continue;
|
|
60
|
+
|
|
61
|
+
// Check if this line starts with a major clause
|
|
62
|
+
const upperLine = line.toUpperCase();
|
|
63
|
+
const startsWithClause = sortedClauses.some((c) =>
|
|
64
|
+
upperLine.startsWith(c),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (startsWithClause) {
|
|
68
|
+
// Find where the clause keyword ends
|
|
69
|
+
const matchedClause = sortedClauses.find((c) =>
|
|
70
|
+
upperLine.startsWith(c),
|
|
71
|
+
);
|
|
72
|
+
if (matchedClause) {
|
|
73
|
+
const rest = line.slice(matchedClause.length).trim();
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
matchedClause === "SELECT" ||
|
|
77
|
+
matchedClause === "GROUP BY" ||
|
|
78
|
+
matchedClause === "ORDER BY"
|
|
79
|
+
) {
|
|
80
|
+
// Split comma-separated items onto separate indented lines
|
|
81
|
+
if (rest) {
|
|
82
|
+
const items = splitTopLevel(rest, ",");
|
|
83
|
+
if (items.length > 1) {
|
|
84
|
+
result.push(matchedClause);
|
|
85
|
+
for (let i = 0; i < items.length; i++) {
|
|
86
|
+
const comma = i < items.length - 1 ? "," : "";
|
|
87
|
+
result.push(` ${items[i].trim()}${comma}`);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (matchedClause === "WHERE" || matchedClause === "HAVING") {
|
|
95
|
+
// Indent AND/OR conditions
|
|
96
|
+
if (rest) {
|
|
97
|
+
const withConditions = rest
|
|
98
|
+
.replace(/\b(AND)\b/gi, "\n AND")
|
|
99
|
+
.replace(/\b(OR)\b/gi, "\n OR");
|
|
100
|
+
const condLines = withConditions
|
|
101
|
+
.split("\n")
|
|
102
|
+
.map((l) => l.trim())
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
result.push(`${matchedClause}`);
|
|
105
|
+
for (const cl of condLines) {
|
|
106
|
+
result.push(` ${cl}`);
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
result.push(line);
|
|
113
|
+
} else {
|
|
114
|
+
result.push(line);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
result.push(` ${line}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result.join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Split a string by a delimiter, but only at the top level (not inside parens).
|
|
126
|
+
*/
|
|
127
|
+
function splitTopLevel(s: string, delimiter: string): string[] {
|
|
128
|
+
const parts: string[] = [];
|
|
129
|
+
let depth = 0;
|
|
130
|
+
let current = "";
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < s.length; i++) {
|
|
133
|
+
const ch = s[i];
|
|
134
|
+
if (ch === "(") depth++;
|
|
135
|
+
else if (ch === ")") depth--;
|
|
136
|
+
|
|
137
|
+
if (ch === delimiter && depth === 0) {
|
|
138
|
+
parts.push(current);
|
|
139
|
+
current = "";
|
|
140
|
+
} else {
|
|
141
|
+
current += ch;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (current.trim()) parts.push(current);
|
|
145
|
+
return parts;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
registerTool({
|
|
149
|
+
name: "sql_format",
|
|
150
|
+
description: "Format and prettify SQL queries with proper indentation and clause separation",
|
|
151
|
+
pro: true,
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
sql: { type: "string", description: "SQL query to format" },
|
|
156
|
+
dialect: {
|
|
157
|
+
type: "string",
|
|
158
|
+
enum: ["standard", "postgresql", "mysql", "sqlite"],
|
|
159
|
+
description: "SQL dialect (default: standard)",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
required: ["sql"],
|
|
163
|
+
},
|
|
164
|
+
handler: async (args) => {
|
|
165
|
+
const sql = args.sql as string;
|
|
166
|
+
const dialect = (args.dialect as string) || "standard";
|
|
167
|
+
|
|
168
|
+
if (!sql.trim()) throw new Error("SQL string is empty");
|
|
169
|
+
|
|
170
|
+
const formatted = formatSQL(sql, dialect);
|
|
171
|
+
|
|
172
|
+
return [
|
|
173
|
+
`=== Formatted SQL (${dialect}) ===`,
|
|
174
|
+
"",
|
|
175
|
+
formatted,
|
|
176
|
+
].join("\n");
|
|
177
|
+
},
|
|
178
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { registerTool } from "../registry.js";
|
|
2
|
+
|
|
3
|
+
function relativeTime(date: Date): string {
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
const diffMs = now - date.getTime();
|
|
6
|
+
const absDiff = Math.abs(diffMs);
|
|
7
|
+
const suffix = diffMs >= 0 ? "ago" : "from now";
|
|
8
|
+
|
|
9
|
+
const seconds = Math.floor(absDiff / 1000);
|
|
10
|
+
if (seconds < 60) return `${seconds} seconds ${suffix}`;
|
|
11
|
+
const minutes = Math.floor(seconds / 60);
|
|
12
|
+
if (minutes < 60) return `${minutes} minutes ${suffix}`;
|
|
13
|
+
const hours = Math.floor(minutes / 60);
|
|
14
|
+
if (hours < 24) return `${hours} hours ${suffix}`;
|
|
15
|
+
const days = Math.floor(hours / 24);
|
|
16
|
+
if (days < 30) return `${days} days ${suffix}`;
|
|
17
|
+
const months = Math.floor(days / 30);
|
|
18
|
+
if (months < 12) return `${months} months ${suffix}`;
|
|
19
|
+
const years = Math.floor(days / 365);
|
|
20
|
+
return `${years} years ${suffix}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
registerTool({
|
|
24
|
+
name: "timestamp",
|
|
25
|
+
description:
|
|
26
|
+
"Convert between timestamp formats: epoch seconds, epoch ms, ISO 8601, human-readable",
|
|
27
|
+
pro: false,
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
value: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description:
|
|
34
|
+
'Timestamp value: "now", epoch seconds, epoch milliseconds, or ISO 8601 string',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["value"],
|
|
38
|
+
},
|
|
39
|
+
handler: async (args) => {
|
|
40
|
+
const value = (args.value as string).trim();
|
|
41
|
+
let date: Date;
|
|
42
|
+
|
|
43
|
+
if (value.toLowerCase() === "now") {
|
|
44
|
+
date = new Date();
|
|
45
|
+
} else if (/^\d{10}$/.test(value)) {
|
|
46
|
+
// Epoch seconds
|
|
47
|
+
date = new Date(parseInt(value, 10) * 1000);
|
|
48
|
+
} else if (/^\d{13}$/.test(value)) {
|
|
49
|
+
// Epoch milliseconds
|
|
50
|
+
date = new Date(parseInt(value, 10));
|
|
51
|
+
} else if (value.includes("T") || value.includes("-")) {
|
|
52
|
+
date = new Date(value);
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Cannot parse timestamp: "${value}". Use "now", epoch seconds (10 digits), epoch ms (13 digits), or ISO 8601.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isNaN(date.getTime())) {
|
|
60
|
+
throw new Error(`Invalid date: "${value}"`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const epochS = Math.floor(date.getTime() / 1000);
|
|
64
|
+
const epochMs = date.getTime();
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
`epoch_seconds: ${epochS}`,
|
|
68
|
+
`epoch_ms: ${epochMs}`,
|
|
69
|
+
`iso8601: ${date.toISOString()}`,
|
|
70
|
+
`human: ${date.toUTCString()}`,
|
|
71
|
+
`relative: ${relativeTime(date)}`,
|
|
72
|
+
].join("\n");
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { registerTool } from "../registry.js";
|
|
2
|
+
|
|
3
|
+
registerTool({
|
|
4
|
+
name: "url_encode",
|
|
5
|
+
description: "URL encode or decode strings",
|
|
6
|
+
pro: false,
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
text: { type: "string", description: "Text to encode or encoded string to decode" },
|
|
11
|
+
action: {
|
|
12
|
+
type: "string",
|
|
13
|
+
enum: ["encode", "decode"],
|
|
14
|
+
description: "encode or decode",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
required: ["text", "action"],
|
|
18
|
+
},
|
|
19
|
+
handler: async (args) => {
|
|
20
|
+
const text = args.text as string;
|
|
21
|
+
const action = args.action as string;
|
|
22
|
+
|
|
23
|
+
if (action === "encode") {
|
|
24
|
+
return encodeURIComponent(text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return decodeURIComponent(text);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { registerTool } from "../registry.js";
|
|
3
|
+
|
|
4
|
+
registerTool({
|
|
5
|
+
name: "uuid_generate",
|
|
6
|
+
description: "Generate one or more v4 UUIDs",
|
|
7
|
+
pro: false,
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
count: {
|
|
12
|
+
type: "number",
|
|
13
|
+
description: "Number of UUIDs to generate (default: 1, max: 10)",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
handler: async (args) => {
|
|
18
|
+
let count = Math.min(Math.max((args.count as number) || 1, 1), 10);
|
|
19
|
+
const uuids: string[] = [];
|
|
20
|
+
for (let i = 0; i < count; i++) {
|
|
21
|
+
uuids.push(randomUUID());
|
|
22
|
+
}
|
|
23
|
+
return uuids.join("\n");
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { registerTool } from "../registry.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Minimal YAML parser
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
function parseYaml(text: string): unknown {
|
|
8
|
+
const lines = text.split("\n");
|
|
9
|
+
return parseYamlLines(lines, 0, 0).value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ParseResult {
|
|
13
|
+
value: unknown;
|
|
14
|
+
nextLine: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getIndent(line: string): number {
|
|
18
|
+
const match = line.match(/^(\s*)/);
|
|
19
|
+
return match ? match[1].length : 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseScalar(val: string): unknown {
|
|
23
|
+
const trimmed = val.trim();
|
|
24
|
+
if (trimmed === "" || trimmed === "null" || trimmed === "~") return null;
|
|
25
|
+
if (trimmed === "true") return true;
|
|
26
|
+
if (trimmed === "false") return false;
|
|
27
|
+
if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
|
|
28
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed);
|
|
29
|
+
// Strip quotes
|
|
30
|
+
if (
|
|
31
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
32
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
33
|
+
) {
|
|
34
|
+
return trimmed.slice(1, -1);
|
|
35
|
+
}
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseYamlLines(
|
|
40
|
+
lines: string[],
|
|
41
|
+
startLine: number,
|
|
42
|
+
baseIndent: number,
|
|
43
|
+
): ParseResult {
|
|
44
|
+
// Skip empty lines and comments
|
|
45
|
+
let i = startLine;
|
|
46
|
+
while (i < lines.length && (lines[i].trim() === "" || lines[i].trim().startsWith("#"))) {
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
if (i >= lines.length) return { value: null, nextLine: i };
|
|
50
|
+
|
|
51
|
+
const line = lines[i];
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
|
|
54
|
+
// Check if it's a list item
|
|
55
|
+
if (trimmed.startsWith("- ")) {
|
|
56
|
+
const arr: unknown[] = [];
|
|
57
|
+
const listIndent = getIndent(line);
|
|
58
|
+
while (i < lines.length) {
|
|
59
|
+
const cur = lines[i];
|
|
60
|
+
if (cur.trim() === "" || cur.trim().startsWith("#")) { i++; continue; }
|
|
61
|
+
const curIndent = getIndent(cur);
|
|
62
|
+
if (curIndent < listIndent) break;
|
|
63
|
+
if (curIndent === listIndent && cur.trim().startsWith("- ")) {
|
|
64
|
+
const itemText = cur.trim().slice(2);
|
|
65
|
+
// Check if it's a key: value (nested map in list)
|
|
66
|
+
if (itemText.includes(": ")) {
|
|
67
|
+
const obj: Record<string, unknown> = {};
|
|
68
|
+
const colonIdx = itemText.indexOf(": ");
|
|
69
|
+
const key = itemText.slice(0, colonIdx).trim();
|
|
70
|
+
const val = itemText.slice(colonIdx + 2).trim();
|
|
71
|
+
obj[key] = parseScalar(val);
|
|
72
|
+
// Check for continuation lines at deeper indent
|
|
73
|
+
i++;
|
|
74
|
+
while (i < lines.length) {
|
|
75
|
+
const nextLine = lines[i];
|
|
76
|
+
if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) { i++; continue; }
|
|
77
|
+
const nextIndent = getIndent(nextLine);
|
|
78
|
+
if (nextIndent <= listIndent) break;
|
|
79
|
+
if (nextLine.trim().includes(": ")) {
|
|
80
|
+
const ci = nextLine.trim().indexOf(": ");
|
|
81
|
+
const k = nextLine.trim().slice(0, ci).trim();
|
|
82
|
+
const v = nextLine.trim().slice(ci + 2).trim();
|
|
83
|
+
obj[k] = parseScalar(v);
|
|
84
|
+
i++;
|
|
85
|
+
} else {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
arr.push(obj);
|
|
90
|
+
} else {
|
|
91
|
+
arr.push(parseScalar(itemText));
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { value: arr, nextLine: i };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if it's a map (key: value)
|
|
102
|
+
if (trimmed.includes(": ") || trimmed.endsWith(":")) {
|
|
103
|
+
const obj: Record<string, unknown> = {};
|
|
104
|
+
const mapIndent = getIndent(line);
|
|
105
|
+
while (i < lines.length) {
|
|
106
|
+
const cur = lines[i];
|
|
107
|
+
if (cur.trim() === "" || cur.trim().startsWith("#")) { i++; continue; }
|
|
108
|
+
const curIndent = getIndent(cur);
|
|
109
|
+
if (curIndent < mapIndent && i > startLine) break;
|
|
110
|
+
if (curIndent !== mapIndent) break;
|
|
111
|
+
const curTrimmed = cur.trim();
|
|
112
|
+
if (curTrimmed.endsWith(":")) {
|
|
113
|
+
// Block value — next lines are the value
|
|
114
|
+
const key = curTrimmed.slice(0, -1).trim();
|
|
115
|
+
i++;
|
|
116
|
+
const result = parseYamlLines(lines, i, mapIndent + 2);
|
|
117
|
+
obj[key] = result.value;
|
|
118
|
+
i = result.nextLine;
|
|
119
|
+
} else if (curTrimmed.includes(": ")) {
|
|
120
|
+
const colonIdx = curTrimmed.indexOf(": ");
|
|
121
|
+
const key = curTrimmed.slice(0, colonIdx).trim();
|
|
122
|
+
const val = curTrimmed.slice(colonIdx + 2).trim();
|
|
123
|
+
obj[key] = parseScalar(val);
|
|
124
|
+
i++;
|
|
125
|
+
} else {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { value: obj, nextLine: i };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Plain scalar
|
|
133
|
+
return { value: parseScalar(trimmed), nextLine: i + 1 };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Minimal TOML parser
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
function parseToml(text: string): Record<string, unknown> {
|
|
141
|
+
const result: Record<string, unknown> = {};
|
|
142
|
+
let currentSection = result;
|
|
143
|
+
const lines = text.split("\n");
|
|
144
|
+
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
148
|
+
|
|
149
|
+
// Section header [section]
|
|
150
|
+
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
151
|
+
if (sectionMatch) {
|
|
152
|
+
const sectionName = sectionMatch[1];
|
|
153
|
+
const parts = sectionName.split(".");
|
|
154
|
+
let target = result;
|
|
155
|
+
for (const part of parts) {
|
|
156
|
+
if (!(part in target)) {
|
|
157
|
+
target[part] = {};
|
|
158
|
+
}
|
|
159
|
+
target = target[part] as Record<string, unknown>;
|
|
160
|
+
}
|
|
161
|
+
currentSection = target;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// key = value
|
|
166
|
+
const kvMatch = trimmed.match(/^([^=]+)=\s*(.*)$/);
|
|
167
|
+
if (kvMatch) {
|
|
168
|
+
const key = kvMatch[1].trim();
|
|
169
|
+
const val = kvMatch[2].trim();
|
|
170
|
+
currentSection[key] = parseTomlValue(val);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseTomlValue(val: string): unknown {
|
|
178
|
+
if (val === "true") return true;
|
|
179
|
+
if (val === "false") return false;
|
|
180
|
+
if (/^-?\d+$/.test(val)) return parseInt(val, 10);
|
|
181
|
+
if (/^-?\d+\.\d+$/.test(val)) return parseFloat(val);
|
|
182
|
+
if (
|
|
183
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
184
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
185
|
+
) {
|
|
186
|
+
return val.slice(1, -1);
|
|
187
|
+
}
|
|
188
|
+
// Arrays
|
|
189
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
190
|
+
const inner = val.slice(1, -1).trim();
|
|
191
|
+
if (inner === "") return [];
|
|
192
|
+
return inner.split(",").map((item) => parseTomlValue(item.trim()));
|
|
193
|
+
}
|
|
194
|
+
return val;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Serializers
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
function toYaml(data: unknown, indent: number = 0): string {
|
|
202
|
+
const prefix = " ".repeat(indent);
|
|
203
|
+
|
|
204
|
+
if (data === null || data === undefined) return `${prefix}null\n`;
|
|
205
|
+
if (typeof data === "boolean") return `${prefix}${data}\n`;
|
|
206
|
+
if (typeof data === "number") return `${prefix}${data}\n`;
|
|
207
|
+
if (typeof data === "string") {
|
|
208
|
+
if (data.includes("\n") || data.includes(": ") || data.includes("#")) {
|
|
209
|
+
return `${prefix}"${data.replace(/"/g, '\\"')}"\n`;
|
|
210
|
+
}
|
|
211
|
+
return `${prefix}${data}\n`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (Array.isArray(data)) {
|
|
215
|
+
if (data.length === 0) return `${prefix}[]\n`;
|
|
216
|
+
let out = "";
|
|
217
|
+
for (const item of data) {
|
|
218
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
219
|
+
const entries = Object.entries(item as Record<string, unknown>);
|
|
220
|
+
if (entries.length > 0) {
|
|
221
|
+
const [firstKey, firstVal] = entries[0];
|
|
222
|
+
out += `${prefix}- ${firstKey}: ${scalarToYaml(firstVal)}\n`;
|
|
223
|
+
for (let i = 1; i < entries.length; i++) {
|
|
224
|
+
out += `${prefix} ${entries[i][0]}: ${scalarToYaml(entries[i][1])}\n`;
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
out += `${prefix}- ${scalarToYaml(item)}\n`;
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (typeof data === "object") {
|
|
235
|
+
const obj = data as Record<string, unknown>;
|
|
236
|
+
const keys = Object.keys(obj);
|
|
237
|
+
if (keys.length === 0) return `${prefix}{}\n`;
|
|
238
|
+
let out = "";
|
|
239
|
+
for (const key of keys) {
|
|
240
|
+
const val = obj[key];
|
|
241
|
+
if (
|
|
242
|
+
typeof val === "object" &&
|
|
243
|
+
val !== null &&
|
|
244
|
+
!Array.isArray(val) &&
|
|
245
|
+
Object.keys(val).length > 0
|
|
246
|
+
) {
|
|
247
|
+
out += `${prefix}${key}:\n`;
|
|
248
|
+
out += toYaml(val, indent + 1);
|
|
249
|
+
} else if (Array.isArray(val)) {
|
|
250
|
+
out += `${prefix}${key}:\n`;
|
|
251
|
+
out += toYaml(val, indent + 1);
|
|
252
|
+
} else {
|
|
253
|
+
out += `${prefix}${key}: ${scalarToYaml(val)}\n`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return `${prefix}${String(data)}\n`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function scalarToYaml(val: unknown): string {
|
|
263
|
+
if (val === null || val === undefined) return "null";
|
|
264
|
+
if (typeof val === "boolean") return String(val);
|
|
265
|
+
if (typeof val === "number") return String(val);
|
|
266
|
+
if (typeof val === "string") {
|
|
267
|
+
if (val.includes(": ") || val.includes("#") || val.includes("\n")) {
|
|
268
|
+
return `"${val.replace(/"/g, '\\"')}"`;
|
|
269
|
+
}
|
|
270
|
+
return val;
|
|
271
|
+
}
|
|
272
|
+
return JSON.stringify(val);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function toToml(data: unknown, sectionPath: string = ""): string {
|
|
276
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
277
|
+
return String(data);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const obj = data as Record<string, unknown>;
|
|
281
|
+
let topLevel = "";
|
|
282
|
+
let sections = "";
|
|
283
|
+
|
|
284
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
285
|
+
if (
|
|
286
|
+
typeof val === "object" &&
|
|
287
|
+
val !== null &&
|
|
288
|
+
!Array.isArray(val)
|
|
289
|
+
) {
|
|
290
|
+
const path = sectionPath ? `${sectionPath}.${key}` : key;
|
|
291
|
+
sections += `[${path}]\n`;
|
|
292
|
+
sections += toToml(val, path);
|
|
293
|
+
sections += "\n";
|
|
294
|
+
} else {
|
|
295
|
+
topLevel += `${key} = ${toTomlValue(val)}\n`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return topLevel + sections;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function toTomlValue(val: unknown): string {
|
|
303
|
+
if (val === null || val === undefined) return '""';
|
|
304
|
+
if (typeof val === "boolean") return String(val);
|
|
305
|
+
if (typeof val === "number") return String(val);
|
|
306
|
+
if (typeof val === "string") return `"${val.replace(/"/g, '\\"')}"`;
|
|
307
|
+
if (Array.isArray(val)) {
|
|
308
|
+
return `[${val.map((v) => toTomlValue(v)).join(", ")}]`;
|
|
309
|
+
}
|
|
310
|
+
return JSON.stringify(val);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Tool registration
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
type Format = "yaml" | "json" | "toml";
|
|
318
|
+
|
|
319
|
+
function parse(text: string, format: Format): unknown {
|
|
320
|
+
switch (format) {
|
|
321
|
+
case "json":
|
|
322
|
+
return JSON.parse(text);
|
|
323
|
+
case "yaml":
|
|
324
|
+
return parseYaml(text);
|
|
325
|
+
case "toml":
|
|
326
|
+
return parseToml(text);
|
|
327
|
+
default:
|
|
328
|
+
throw new Error(`Unsupported input format: ${format}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function serialize(data: unknown, format: Format): string {
|
|
333
|
+
switch (format) {
|
|
334
|
+
case "json":
|
|
335
|
+
return JSON.stringify(data, null, 2);
|
|
336
|
+
case "yaml":
|
|
337
|
+
return toYaml(data).trimEnd();
|
|
338
|
+
case "toml":
|
|
339
|
+
return toToml(data).trimEnd();
|
|
340
|
+
default:
|
|
341
|
+
throw new Error(`Unsupported output format: ${format}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
registerTool({
|
|
346
|
+
name: "yaml_convert",
|
|
347
|
+
description:
|
|
348
|
+
"Convert between YAML, JSON, and TOML formats",
|
|
349
|
+
pro: true,
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: {
|
|
353
|
+
text: { type: "string", description: "Input text to convert" },
|
|
354
|
+
from: {
|
|
355
|
+
type: "string",
|
|
356
|
+
enum: ["yaml", "json", "toml"],
|
|
357
|
+
description: "Source format",
|
|
358
|
+
},
|
|
359
|
+
to: {
|
|
360
|
+
type: "string",
|
|
361
|
+
enum: ["yaml", "json", "toml"],
|
|
362
|
+
description: "Target format",
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
required: ["text", "from", "to"],
|
|
366
|
+
},
|
|
367
|
+
handler: async (args) => {
|
|
368
|
+
const text = args.text as string;
|
|
369
|
+
const from = args.from as Format;
|
|
370
|
+
const to = args.to as Format;
|
|
371
|
+
|
|
372
|
+
if (!text.trim()) throw new Error("Input text is empty");
|
|
373
|
+
|
|
374
|
+
const data = parse(text, from);
|
|
375
|
+
const output = serialize(data, to);
|
|
376
|
+
|
|
377
|
+
return [
|
|
378
|
+
`=== Converted ${from.toUpperCase()} → ${to.toUpperCase()} ===`,
|
|
379
|
+
"",
|
|
380
|
+
output,
|
|
381
|
+
].join("\n");
|
|
382
|
+
},
|
|
383
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|