@t00lzcom/dev-tools 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 +86 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +34 -0
- package/src/tools.ts +421 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# T00lz Dev Tools — OpenClaw Plugin
|
|
2
|
+
|
|
3
|
+
> 8 essential developer utilities from [t00lz.com](https://t00lz.com), available directly in your OpenClaw AI assistant.
|
|
4
|
+
|
|
5
|
+
All processing is **100% local** — no data ever leaves your machine.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Tools Included
|
|
10
|
+
|
|
11
|
+
| Tool | What you say | What it does |
|
|
12
|
+
|------|-------------|--------------|
|
|
13
|
+
| `json_format` | "format this JSON" / "minify this JSON" | Beautify or minify JSON with error reporting |
|
|
14
|
+
| `json_validate` | "validate this JSON" | Check JSON syntax with exact line/col errors |
|
|
15
|
+
| `base64` | "encode this to base64" / "decode this base64" | Base64 encode or decode any string |
|
|
16
|
+
| `generate_password` | "generate a 24-char password with symbols" | Cryptographically secure password generator |
|
|
17
|
+
| `markdown_to_html` | "convert this markdown to HTML" | Full Markdown → HTML conversion |
|
|
18
|
+
| `word_count` | "count words in this text" | Words, chars, sentences, reading time |
|
|
19
|
+
| `case_convert` | "convert to snake_case" / "make this title case" | 7 case formats including camel, kebab, snake |
|
|
20
|
+
| `text_diff` | "diff these two texts" | Line-by-line diff with +/- summary |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
openclaw plugins install @t00lzcom/dev-tools
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or from source:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
openclaw plugins install ./t00lz-dev-tools
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Add to your OpenClaw config (optional):
|
|
39
|
+
|
|
40
|
+
```json5
|
|
41
|
+
{
|
|
42
|
+
plugins: {
|
|
43
|
+
entries: {
|
|
44
|
+
"t00lz-dev-tools": {
|
|
45
|
+
enabled: true,
|
|
46
|
+
config: {
|
|
47
|
+
passwordDefaultLength: 24, // default: 20
|
|
48
|
+
passwordDefaultSymbols: true // default: true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Example Usage
|
|
57
|
+
|
|
58
|
+
Once installed, just talk to your OpenClaw agent naturally:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
You: format this JSON for me: {"name":"alice","age":30}
|
|
62
|
+
Agent: {
|
|
63
|
+
"name": "alice",
|
|
64
|
+
"age": 30
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
You: generate a strong 32-character password
|
|
68
|
+
Agent: Password: kR7!mXq2#vLp9@wNsZ4$bJcTdYeF8&uA
|
|
69
|
+
Strength: 💪 Strong
|
|
70
|
+
|
|
71
|
+
You: convert "hello world from t00lz" to camelCase
|
|
72
|
+
Agent: helloWorldFromT00lz
|
|
73
|
+
|
|
74
|
+
You: how many words in this paragraph? [paste text]
|
|
75
|
+
Agent: Words: 142 | Characters: 891 | Reading time: 43s
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Contributing
|
|
79
|
+
|
|
80
|
+
Issues and PRs welcome at [github.com/t00lz/openclaw-dev-tools](https://github.com/t00lz/openclaw-dev-tools).
|
|
81
|
+
|
|
82
|
+
For more free developer tools, visit **[t00lz.com](https://t00lz.com)**.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "t00lz-dev-tools",
|
|
3
|
+
"name": "T00lz Dev Tools",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Essential developer utilities from T00lz.com — format JSON, encode/decode Base64, generate secure passwords, convert Markdown to HTML, count words, and more. All processing is local; no data leaves your machine.",
|
|
6
|
+
"author": "T00LZ <t000lz.com@gmail.com>",
|
|
7
|
+
"homepage": "https://t00lz.com",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"openclaw": {
|
|
10
|
+
"extensions": ["./src/tools.ts"],
|
|
11
|
+
"tools": true
|
|
12
|
+
},
|
|
13
|
+
"configSchema": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"passwordDefaultLength": {
|
|
17
|
+
"type": "number",
|
|
18
|
+
"default": 20,
|
|
19
|
+
"description": "Default length for generated passwords"
|
|
20
|
+
},
|
|
21
|
+
"passwordDefaultSymbols": {
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"default": true,
|
|
24
|
+
"description": "Include symbols in generated passwords by default"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"uiHints": {
|
|
29
|
+
"plugins.entries.t00lz-dev-tools.config.passwordDefaultLength": {
|
|
30
|
+
"label": "Default Password Length",
|
|
31
|
+
"placeholder": "20"
|
|
32
|
+
},
|
|
33
|
+
"plugins.entries.t00lz-dev-tools.config.passwordDefaultSymbols": {
|
|
34
|
+
"label": "Include Symbols by Default"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@t00lzcom/dev-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Essential developer utilities for OpenClaw — powered by T00lz.com",
|
|
5
|
+
"main": "src/tools.ts",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"openclaw",
|
|
8
|
+
"plugin",
|
|
9
|
+
"json",
|
|
10
|
+
"base64",
|
|
11
|
+
"password",
|
|
12
|
+
"markdown",
|
|
13
|
+
"text",
|
|
14
|
+
"developer",
|
|
15
|
+
"utilities",
|
|
16
|
+
"tools"
|
|
17
|
+
],
|
|
18
|
+
"author": "T00LZ <t000lz.com@gmail.com>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://t00lz.com",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/t00lz/openclaw-dev-tools"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"marked": "^12.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.4.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T00lz Dev Tools — OpenClaw Plugin
|
|
3
|
+
* https://t00lz.com
|
|
4
|
+
*
|
|
5
|
+
* Registers 8 developer utility tools directly into your OpenClaw agent.
|
|
6
|
+
* All processing is local — no data is sent anywhere.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { marked } from "marked";
|
|
10
|
+
|
|
11
|
+
// ─── Plugin Registration ──────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export function register(api: any) {
|
|
14
|
+
const cfg = api.config?.plugins?.entries?.["t00lz-dev-tools"]?.config ?? {};
|
|
15
|
+
const defaultPasswordLength: number = cfg.passwordDefaultLength ?? 20;
|
|
16
|
+
const defaultPasswordSymbols: boolean = cfg.passwordDefaultSymbols ?? true;
|
|
17
|
+
|
|
18
|
+
// ── 1. JSON Formatter ───────────────────────────────────────────────────────
|
|
19
|
+
api.registerTool({
|
|
20
|
+
name: "json_format",
|
|
21
|
+
description:
|
|
22
|
+
"Beautify or minify a JSON string. Use action='beautify' (default) or action='minify'. " +
|
|
23
|
+
"Returns formatted JSON or a clear error message with the problem line.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
required: ["json"],
|
|
27
|
+
properties: {
|
|
28
|
+
json: { type: "string", description: "The raw JSON string to process" },
|
|
29
|
+
action: {
|
|
30
|
+
type: "string",
|
|
31
|
+
enum: ["beautify", "minify"],
|
|
32
|
+
default: "beautify",
|
|
33
|
+
description: "Whether to beautify or minify",
|
|
34
|
+
},
|
|
35
|
+
indent: {
|
|
36
|
+
type: "number",
|
|
37
|
+
default: 2,
|
|
38
|
+
description: "Indentation spaces (beautify only)",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
async execute({ json, action = "beautify", indent = 2 }: any) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(json);
|
|
45
|
+
if (action === "minify") {
|
|
46
|
+
return { result: JSON.stringify(parsed), action: "minified" };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
result: JSON.stringify(parsed, null, indent),
|
|
50
|
+
action: "beautified",
|
|
51
|
+
};
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
return { error: `Invalid JSON: ${err.message}` };
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── 2. JSON Validator ───────────────────────────────────────────────────────
|
|
59
|
+
api.registerTool({
|
|
60
|
+
name: "json_validate",
|
|
61
|
+
description:
|
|
62
|
+
"Validate a JSON string and report any syntax errors with the exact position.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
required: ["json"],
|
|
66
|
+
properties: {
|
|
67
|
+
json: { type: "string", description: "The JSON string to validate" },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
async execute({ json }: any) {
|
|
71
|
+
try {
|
|
72
|
+
JSON.parse(json);
|
|
73
|
+
return { valid: true, message: "✅ Valid JSON — no errors found." };
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
// Extract line/col from error message where available
|
|
76
|
+
const match = err.message.match(/position (\d+)/i);
|
|
77
|
+
let hint = "";
|
|
78
|
+
if (match) {
|
|
79
|
+
const pos = parseInt(match[1], 10);
|
|
80
|
+
const before = json.slice(0, pos);
|
|
81
|
+
const line = (before.match(/\n/g) ?? []).length + 1;
|
|
82
|
+
const col = pos - before.lastIndexOf("\n");
|
|
83
|
+
hint = ` (line ${line}, col ${col})`;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
valid: false,
|
|
87
|
+
error: `❌ Invalid JSON${hint}: ${err.message}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── 3. Base64 Encoder / Decoder ─────────────────────────────────────────────
|
|
94
|
+
api.registerTool({
|
|
95
|
+
name: "base64",
|
|
96
|
+
description:
|
|
97
|
+
"Encode a plain string to Base64, or decode a Base64 string back to plain text.",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
required: ["text", "action"],
|
|
101
|
+
properties: {
|
|
102
|
+
text: { type: "string", description: "The string to process" },
|
|
103
|
+
action: {
|
|
104
|
+
type: "string",
|
|
105
|
+
enum: ["encode", "decode"],
|
|
106
|
+
description: "'encode' converts plain text → Base64; 'decode' reverses it",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
async execute({ text, action }: any) {
|
|
111
|
+
try {
|
|
112
|
+
if (action === "encode") {
|
|
113
|
+
const result = Buffer.from(text, "utf8").toString("base64");
|
|
114
|
+
return { result, action: "encoded" };
|
|
115
|
+
} else {
|
|
116
|
+
const result = Buffer.from(text, "base64").toString("utf8");
|
|
117
|
+
return { result, action: "decoded" };
|
|
118
|
+
}
|
|
119
|
+
} catch (err: any) {
|
|
120
|
+
return { error: `Base64 ${action} failed: ${err.message}` };
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── 4. Secure Password Generator ───────────────────────────────────────────
|
|
126
|
+
api.registerTool({
|
|
127
|
+
name: "generate_password",
|
|
128
|
+
description:
|
|
129
|
+
"Generate a cryptographically secure random password. Fully local — never transmitted anywhere.",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
length: {
|
|
134
|
+
type: "number",
|
|
135
|
+
default: defaultPasswordLength,
|
|
136
|
+
description: "Password length (8–128)",
|
|
137
|
+
},
|
|
138
|
+
symbols: {
|
|
139
|
+
type: "boolean",
|
|
140
|
+
default: defaultPasswordSymbols,
|
|
141
|
+
description: "Include symbols like !@#$%^&*()",
|
|
142
|
+
},
|
|
143
|
+
numbers: {
|
|
144
|
+
type: "boolean",
|
|
145
|
+
default: true,
|
|
146
|
+
description: "Include digits 0–9",
|
|
147
|
+
},
|
|
148
|
+
uppercase: {
|
|
149
|
+
type: "boolean",
|
|
150
|
+
default: true,
|
|
151
|
+
description: "Include uppercase letters A–Z",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
async execute({
|
|
156
|
+
length = defaultPasswordLength,
|
|
157
|
+
symbols = defaultPasswordSymbols,
|
|
158
|
+
numbers = true,
|
|
159
|
+
uppercase = true,
|
|
160
|
+
}: any) {
|
|
161
|
+
const clampedLength = Math.min(128, Math.max(8, length));
|
|
162
|
+
let chars = "abcdefghijklmnopqrstuvwxyz";
|
|
163
|
+
if (uppercase) chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
164
|
+
if (numbers) chars += "0123456789";
|
|
165
|
+
if (symbols) chars += "!@#$%^&*()-_=+[]{}|;:,.<>?";
|
|
166
|
+
|
|
167
|
+
const { randomBytes } = await import("crypto");
|
|
168
|
+
let password = "";
|
|
169
|
+
while (password.length < clampedLength) {
|
|
170
|
+
const byte = randomBytes(1)[0];
|
|
171
|
+
if (byte < 256 - (256 % chars.length)) {
|
|
172
|
+
password += chars[byte % chars.length];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
password,
|
|
177
|
+
length: clampedLength,
|
|
178
|
+
strength:
|
|
179
|
+
clampedLength >= 20 && symbols && numbers
|
|
180
|
+
? "💪 Strong"
|
|
181
|
+
: clampedLength >= 12
|
|
182
|
+
? "✅ Good"
|
|
183
|
+
: "⚠️ Weak — consider increasing length",
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ── 5. Markdown → HTML ──────────────────────────────────────────────────────
|
|
189
|
+
api.registerTool({
|
|
190
|
+
name: "markdown_to_html",
|
|
191
|
+
description:
|
|
192
|
+
"Convert a Markdown string to clean HTML. Returns the full HTML output.",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: "object",
|
|
195
|
+
required: ["markdown"],
|
|
196
|
+
properties: {
|
|
197
|
+
markdown: { type: "string", description: "The Markdown source text" },
|
|
198
|
+
wrap: {
|
|
199
|
+
type: "boolean",
|
|
200
|
+
default: false,
|
|
201
|
+
description: "Wrap output in a basic <!DOCTYPE html> page shell",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
async execute({ markdown, wrap = false }: any) {
|
|
206
|
+
const body = await marked.parse(markdown);
|
|
207
|
+
if (!wrap) return { html: body };
|
|
208
|
+
const full = `<!DOCTYPE html>
|
|
209
|
+
<html lang="en">
|
|
210
|
+
<head>
|
|
211
|
+
<meta charset="UTF-8" />
|
|
212
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
213
|
+
<title>Document</title>
|
|
214
|
+
</head>
|
|
215
|
+
<body>
|
|
216
|
+
${body}
|
|
217
|
+
</body>
|
|
218
|
+
</html>`;
|
|
219
|
+
return { html: full };
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ── 6. Word & Character Counter ─────────────────────────────────────────────
|
|
224
|
+
api.registerTool({
|
|
225
|
+
name: "word_count",
|
|
226
|
+
description:
|
|
227
|
+
"Count words, characters, sentences, paragraphs, and estimated reading time for any text.",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: "object",
|
|
230
|
+
required: ["text"],
|
|
231
|
+
properties: {
|
|
232
|
+
text: { type: "string", description: "The text to analyse" },
|
|
233
|
+
wpm: {
|
|
234
|
+
type: "number",
|
|
235
|
+
default: 200,
|
|
236
|
+
description: "Reading speed in words per minute (default 200)",
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
async execute({ text, wpm = 200 }: any) {
|
|
241
|
+
const words = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
|
|
242
|
+
const chars = text.length;
|
|
243
|
+
const charsNoSpaces = text.replace(/\s/g, "").length;
|
|
244
|
+
const sentences = (text.match(/[.!?]+/g) ?? []).length;
|
|
245
|
+
const paragraphs = text
|
|
246
|
+
.split(/\n\s*\n/)
|
|
247
|
+
.filter((p: string) => p.trim().length > 0).length;
|
|
248
|
+
const readingTimeSec = Math.ceil((words / wpm) * 60);
|
|
249
|
+
const minutes = Math.floor(readingTimeSec / 60);
|
|
250
|
+
const seconds = readingTimeSec % 60;
|
|
251
|
+
const readingTime =
|
|
252
|
+
minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
words,
|
|
256
|
+
characters: chars,
|
|
257
|
+
charactersNoSpaces: charsNoSpaces,
|
|
258
|
+
sentences,
|
|
259
|
+
paragraphs,
|
|
260
|
+
readingTime,
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ── 7. Case Converter ───────────────────────────────────────────────────────
|
|
266
|
+
api.registerTool({
|
|
267
|
+
name: "case_convert",
|
|
268
|
+
description:
|
|
269
|
+
"Convert text between cases: uppercase, lowercase, title, sentence, camelCase, snake_case, kebab-case.",
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: "object",
|
|
272
|
+
required: ["text", "case"],
|
|
273
|
+
properties: {
|
|
274
|
+
text: { type: "string", description: "The text to convert" },
|
|
275
|
+
case: {
|
|
276
|
+
type: "string",
|
|
277
|
+
enum: [
|
|
278
|
+
"uppercase",
|
|
279
|
+
"lowercase",
|
|
280
|
+
"title",
|
|
281
|
+
"sentence",
|
|
282
|
+
"camel",
|
|
283
|
+
"snake",
|
|
284
|
+
"kebab",
|
|
285
|
+
],
|
|
286
|
+
description: "Target case format",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
async execute({ text, case: targetCase }: any) {
|
|
291
|
+
let result: string;
|
|
292
|
+
switch (targetCase) {
|
|
293
|
+
case "uppercase":
|
|
294
|
+
result = text.toUpperCase();
|
|
295
|
+
break;
|
|
296
|
+
case "lowercase":
|
|
297
|
+
result = text.toLowerCase();
|
|
298
|
+
break;
|
|
299
|
+
case "title":
|
|
300
|
+
result = text
|
|
301
|
+
.toLowerCase()
|
|
302
|
+
.replace(/\b\w/g, (c: string) => c.toUpperCase());
|
|
303
|
+
break;
|
|
304
|
+
case "sentence":
|
|
305
|
+
result = text
|
|
306
|
+
.toLowerCase()
|
|
307
|
+
.replace(/(^\s*\w|[.!?]\s+\w)/g, (c: string) => c.toUpperCase());
|
|
308
|
+
break;
|
|
309
|
+
case "camel":
|
|
310
|
+
result = text
|
|
311
|
+
.toLowerCase()
|
|
312
|
+
.replace(/[^a-zA-Z0-9]+(.)/g, (_: string, c: string) =>
|
|
313
|
+
c.toUpperCase()
|
|
314
|
+
);
|
|
315
|
+
break;
|
|
316
|
+
case "snake":
|
|
317
|
+
result = text
|
|
318
|
+
.toLowerCase()
|
|
319
|
+
.replace(/\s+/g, "_")
|
|
320
|
+
.replace(/[^a-z0-9_]/g, "");
|
|
321
|
+
break;
|
|
322
|
+
case "kebab":
|
|
323
|
+
result = text
|
|
324
|
+
.toLowerCase()
|
|
325
|
+
.replace(/\s+/g, "-")
|
|
326
|
+
.replace(/[^a-z0-9-]/g, "");
|
|
327
|
+
break;
|
|
328
|
+
default:
|
|
329
|
+
result = text;
|
|
330
|
+
}
|
|
331
|
+
return { result, case: targetCase };
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ── 8. Text Diff ────────────────────────────────────────────────────────────
|
|
336
|
+
api.registerTool({
|
|
337
|
+
name: "text_diff",
|
|
338
|
+
description:
|
|
339
|
+
"Compare two texts line-by-line and return added, removed, and unchanged lines.",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: "object",
|
|
342
|
+
required: ["text_a", "text_b"],
|
|
343
|
+
properties: {
|
|
344
|
+
text_a: { type: "string", description: "Original text" },
|
|
345
|
+
text_b: { type: "string", description: "New/modified text" },
|
|
346
|
+
context: {
|
|
347
|
+
type: "number",
|
|
348
|
+
default: 3,
|
|
349
|
+
description: "Lines of context around each change",
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
async execute({ text_a, text_b, context = 3 }: any) {
|
|
354
|
+
const linesA = text_a.split("\n");
|
|
355
|
+
const linesB = text_b.split("\n");
|
|
356
|
+
|
|
357
|
+
// Simple LCS-based line diff
|
|
358
|
+
const diff: Array<{ type: "add" | "remove" | "same"; line: string }> = [];
|
|
359
|
+
let i = 0,
|
|
360
|
+
j = 0;
|
|
361
|
+
|
|
362
|
+
// Build LCS table
|
|
363
|
+
const m = linesA.length,
|
|
364
|
+
n = linesB.length;
|
|
365
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () =>
|
|
366
|
+
new Array(n + 1).fill(0)
|
|
367
|
+
);
|
|
368
|
+
for (let r = 1; r <= m; r++) {
|
|
369
|
+
for (let c = 1; c <= n; c++) {
|
|
370
|
+
dp[r][c] =
|
|
371
|
+
linesA[r - 1] === linesB[c - 1]
|
|
372
|
+
? dp[r - 1][c - 1] + 1
|
|
373
|
+
: Math.max(dp[r - 1][c], dp[r][c - 1]);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Trace back
|
|
378
|
+
const trace: Array<{ type: "add" | "remove" | "same"; line: string }> =
|
|
379
|
+
[];
|
|
380
|
+
let r = m,
|
|
381
|
+
c = n;
|
|
382
|
+
while (r > 0 || c > 0) {
|
|
383
|
+
if (r > 0 && c > 0 && linesA[r - 1] === linesB[c - 1]) {
|
|
384
|
+
trace.push({ type: "same", line: linesA[r - 1] });
|
|
385
|
+
r--;
|
|
386
|
+
c--;
|
|
387
|
+
} else if (c > 0 && (r === 0 || dp[r][c - 1] >= dp[r - 1][c])) {
|
|
388
|
+
trace.push({ type: "add", line: linesB[c - 1] });
|
|
389
|
+
c--;
|
|
390
|
+
} else {
|
|
391
|
+
trace.push({ type: "remove", line: linesA[r - 1] });
|
|
392
|
+
r--;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
trace.reverse();
|
|
396
|
+
|
|
397
|
+
const added = trace.filter((d) => d.type === "add").length;
|
|
398
|
+
const removed = trace.filter((d) => d.type === "remove").length;
|
|
399
|
+
const unchanged = trace.filter((d) => d.type === "same").length;
|
|
400
|
+
|
|
401
|
+
// Format as unified-diff-style string
|
|
402
|
+
const formatted = trace
|
|
403
|
+
.map((d) => {
|
|
404
|
+
if (d.type === "add") return `+ ${d.line}`;
|
|
405
|
+
if (d.type === "remove") return `- ${d.line}`;
|
|
406
|
+
return ` ${d.line}`;
|
|
407
|
+
})
|
|
408
|
+
.join("\n");
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
summary: `+${added} added, -${removed} removed, ${unchanged} unchanged`,
|
|
412
|
+
diff: formatted,
|
|
413
|
+
hasChanges: added > 0 || removed > 0,
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
api.log?.info(
|
|
419
|
+
"[t00lz-dev-tools] 8 tools registered: json_format, json_validate, base64, generate_password, markdown_to_html, word_count, case_convert, text_diff"
|
|
420
|
+
);
|
|
421
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": "src"
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts"],
|
|
13
|
+
"exclude": ["node_modules", "dist"]
|
|
14
|
+
}
|