revxl-devtools 1.0.2 → 1.0.3

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 (51) hide show
  1. package/README.md +2 -2
  2. package/dist/auth.js +7 -6
  3. package/dist/index.js +1 -1
  4. package/docs/index.html +495 -0
  5. package/landing/index.html +495 -0
  6. package/package.json +1 -1
  7. package/dist/auth.d.ts +0 -3
  8. package/dist/codegen/cron-codegen.d.ts +0 -1
  9. package/dist/codegen/regex-codegen.d.ts +0 -1
  10. package/dist/index.d.ts +0 -22
  11. package/dist/registry.d.ts +0 -10
  12. package/dist/tools/base64.d.ts +0 -1
  13. package/dist/tools/batch.d.ts +0 -1
  14. package/dist/tools/chmod.d.ts +0 -1
  15. package/dist/tools/cron.d.ts +0 -1
  16. package/dist/tools/hash.d.ts +0 -1
  17. package/dist/tools/http-status.d.ts +0 -1
  18. package/dist/tools/json-diff.d.ts +0 -1
  19. package/dist/tools/json-format.d.ts +0 -1
  20. package/dist/tools/json-query.d.ts +0 -1
  21. package/dist/tools/jwt.d.ts +0 -1
  22. package/dist/tools/regex.d.ts +0 -1
  23. package/dist/tools/secrets-scan.d.ts +0 -1
  24. package/dist/tools/sql-format.d.ts +0 -1
  25. package/dist/tools/timestamp.d.ts +0 -1
  26. package/dist/tools/url-encode.d.ts +0 -1
  27. package/dist/tools/uuid.d.ts +0 -1
  28. package/dist/tools/yaml-convert.d.ts +0 -1
  29. package/src/auth.ts +0 -99
  30. package/src/codegen/cron-codegen.ts +0 -66
  31. package/src/codegen/regex-codegen.ts +0 -132
  32. package/src/index.ts +0 -134
  33. package/src/registry.ts +0 -25
  34. package/src/tools/base64.ts +0 -32
  35. package/src/tools/batch.ts +0 -69
  36. package/src/tools/chmod.ts +0 -133
  37. package/src/tools/cron.ts +0 -365
  38. package/src/tools/hash.ts +0 -26
  39. package/src/tools/http-status.ts +0 -63
  40. package/src/tools/json-diff.ts +0 -153
  41. package/src/tools/json-format.ts +0 -43
  42. package/src/tools/json-query.ts +0 -126
  43. package/src/tools/jwt.ts +0 -193
  44. package/src/tools/regex.ts +0 -131
  45. package/src/tools/secrets-scan.ts +0 -212
  46. package/src/tools/sql-format.ts +0 -178
  47. package/src/tools/timestamp.ts +0 -74
  48. package/src/tools/url-encode.ts +0 -29
  49. package/src/tools/uuid.ts +0 -25
  50. package/src/tools/yaml-convert.ts +0 -383
  51. package/tsconfig.json +0 -14
package/src/auth.ts DELETED
@@ -1,99 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
-
5
- const CACHE_PATH = join(homedir(), ".revxl-devtools-cache.json");
6
- const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
7
- const MAX_TRIAL_USES = 3;
8
-
9
- interface Cache {
10
- proValidated: boolean;
11
- validatedAt: number;
12
- trialUses: Record<string, number>;
13
- }
14
-
15
- function loadCache(): Cache {
16
- try {
17
- const raw = readFileSync(CACHE_PATH, "utf-8");
18
- return JSON.parse(raw) as Cache;
19
- } catch {
20
- return { proValidated: false, validatedAt: 0, trialUses: {} };
21
- }
22
- }
23
-
24
- function saveCache(cache: Cache): void {
25
- try {
26
- writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2), "utf-8");
27
- } catch {
28
- // Silently fail — cache is best-effort
29
- }
30
- }
31
-
32
- async function validateKeyWithSupabase(key: string): Promise<boolean> {
33
- const supabaseUrl = process.env.SUPABASE_URL;
34
- const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
35
-
36
- if (!supabaseUrl || !supabaseAnonKey) {
37
- // Dev mode: accept any RX-prefixed key
38
- return key.startsWith("RX.");
39
- }
40
-
41
- try {
42
- const res = await fetch(
43
- `${supabaseUrl}/rest/v1/pro_keys?key=eq.${encodeURIComponent(key)}&select=active`,
44
- {
45
- headers: {
46
- apikey: supabaseAnonKey,
47
- Authorization: `Bearer ${supabaseAnonKey}`,
48
- },
49
- }
50
- );
51
-
52
- if (!res.ok) {
53
- // Supabase down — fall back to format check
54
- return key.startsWith("RX.");
55
- }
56
-
57
- const rows = (await res.json()) as Array<{ active: boolean }>;
58
- return rows.length > 0 && rows[0].active === true;
59
- } catch {
60
- // Network error — fall back to format check
61
- return key.startsWith("RX.");
62
- }
63
- }
64
-
65
- export async function checkProAccess(): Promise<boolean> {
66
- const key = process.env.MCP_DEVTOOLS_KEY;
67
- if (!key || !key.startsWith("RX.")) {
68
- return false;
69
- }
70
-
71
- const cache = loadCache();
72
-
73
- // Check cache validity
74
- const cacheAge = Date.now() - cache.validatedAt;
75
- if (cache.proValidated && cacheAge < CACHE_TTL_MS) {
76
- return true;
77
- }
78
-
79
- // Validate against Supabase (or dev mode fallback)
80
- const valid = await validateKeyWithSupabase(key);
81
-
82
- cache.proValidated = valid;
83
- cache.validatedAt = Date.now();
84
- saveCache(cache);
85
-
86
- return valid;
87
- }
88
-
89
- export function getTrialUsesRemaining(toolName: string): number {
90
- const cache = loadCache();
91
- const used = cache.trialUses[toolName] ?? 0;
92
- return Math.max(0, MAX_TRIAL_USES - used);
93
- }
94
-
95
- export function incrementTrialUse(toolName: string): void {
96
- const cache = loadCache();
97
- cache.trialUses[toolName] = (cache.trialUses[toolName] ?? 0) + 1;
98
- saveCache(cache);
99
- }
@@ -1,66 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Cron code generation — crontab, systemd timer, and node-cron snippets
3
- // ---------------------------------------------------------------------------
4
-
5
- export function generateCronCode(expression: string): string {
6
- const crontab = `# Crontab entry
7
- ${expression} /path/to/command`;
8
-
9
- const systemd = generateSystemdTimer(expression);
10
- const nodeCron = generateNodeCron(expression);
11
-
12
- return [crontab, systemd, nodeCron].join("\n\n---\n\n");
13
- }
14
-
15
- function cronToOnCalendar(expression: string): string {
16
- const parts = expression.split(/\s+/);
17
- if (parts.length !== 5) return expression;
18
-
19
- const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
20
-
21
- const dayMap: Record<string, string> = {
22
- "0": "Sun", "1": "Mon", "2": "Tue", "3": "Wed",
23
- "4": "Thu", "5": "Fri", "6": "Sat", "7": "Sun",
24
- };
25
-
26
- let dow = "*";
27
- if (dayOfWeek !== "*") {
28
- dow = dayOfWeek
29
- .split(",")
30
- .map((d) => dayMap[d] || d)
31
- .join(",");
32
- }
33
-
34
- const dom = dayOfMonth === "*" ? "*" : dayOfMonth;
35
- const mon = month === "*" ? "*" : month.padStart(2, "0");
36
- const h = hour === "*" ? "*" : hour.padStart(2, "0");
37
- const m = minute === "*" ? "*" : minute.padStart(2, "0");
38
-
39
- return `${dow} *-${mon}-${dom} ${h}:${m}:00`;
40
- }
41
-
42
- function generateSystemdTimer(expression: string): string {
43
- const onCalendar = cronToOnCalendar(expression);
44
- return `# systemd timer unit — save as /etc/systemd/system/mytask.timer
45
- [Unit]
46
- Description=My scheduled task
47
-
48
- [Timer]
49
- OnCalendar=${onCalendar}
50
- Persistent=true
51
-
52
- [Install]
53
- WantedBy=timers.target`;
54
- }
55
-
56
- function generateNodeCron(expression: string): string {
57
- return `// Node.js — npm install cron
58
- import { CronJob } from 'cron';
59
-
60
- const job = new CronJob('${expression}', () => {
61
- console.log('Task executed at', new Date().toISOString());
62
- // your logic here
63
- });
64
-
65
- job.start();`;
66
- }
@@ -1,132 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Regex code generation — produces working snippets in 5 languages
3
- // ---------------------------------------------------------------------------
4
-
5
- type Language = "javascript" | "python" | "go" | "rust" | "java";
6
-
7
- const ALL_LANGUAGES: Language[] = ["javascript", "python", "go", "rust", "java"];
8
-
9
- function jsFlags(flags: string): string {
10
- return flags || "";
11
- }
12
-
13
- function pyFlags(flags: string): string {
14
- const parts: string[] = [];
15
- if (flags.includes("i")) parts.push("re.IGNORECASE");
16
- if (flags.includes("m")) parts.push("re.MULTILINE");
17
- if (flags.includes("s")) parts.push("re.DOTALL");
18
- return parts.length ? ", " + parts.join(" | ") : "";
19
- }
20
-
21
- function generateJS(pattern: string, flags: string): string {
22
- const f = jsFlags(flags);
23
- return `// JavaScript
24
- const regex = /${pattern}/${f};
25
- const text = "your text here";
26
-
27
- const matches = text.match(regex);
28
- if (matches) {
29
- console.log("Matches:", matches);
30
- } else {
31
- console.log("No matches found");
32
- }`;
33
- }
34
-
35
- function generatePython(pattern: string, flags: string): string {
36
- const f = pyFlags(flags);
37
- return `# Python
38
- import re
39
-
40
- pattern = re.compile(r'${pattern}'${f})
41
- text = "your text here"
42
-
43
- matches = pattern.findall(text)
44
- print("Matches:", matches)`;
45
- }
46
-
47
- function generateGo(pattern: string, flags: string): string {
48
- // Go uses inline flags (?i) etc.
49
- let goFlags = "";
50
- if (flags.includes("i")) goFlags += "i";
51
- if (flags.includes("m")) goFlags += "m";
52
- if (flags.includes("s")) goFlags += "s";
53
- const prefix = goFlags ? `(?${goFlags})` : "";
54
- return `// Go
55
- package main
56
-
57
- import (
58
- \t"fmt"
59
- \t"regexp"
60
- )
61
-
62
- func main() {
63
- \tre := regexp.MustCompile(\`${prefix}${pattern}\`)
64
- \ttext := "your text here"
65
-
66
- \tmatches := re.FindAllString(text, -1)
67
- \tfmt.Println("Matches:", matches)
68
- }`;
69
- }
70
-
71
- function generateRust(pattern: string, flags: string): string {
72
- let rustFlags = "";
73
- if (flags.includes("i")) rustFlags += "(?i)";
74
- if (flags.includes("m")) rustFlags += "(?m)";
75
- if (flags.includes("s")) rustFlags += "(?s)";
76
- return `// Rust — add \`regex = "1"\` to Cargo.toml [dependencies]
77
- use regex::Regex;
78
-
79
- fn main() {
80
- let re = Regex::new(r"${rustFlags}${pattern}").unwrap();
81
- let text = "your text here";
82
-
83
- for m in re.find_iter(text) {
84
- println!("Match: {}", m.as_str());
85
- }
86
- }`;
87
- }
88
-
89
- function generateJava(pattern: string, flags: string): string {
90
- const javaFlags: string[] = [];
91
- if (flags.includes("i")) javaFlags.push("Pattern.CASE_INSENSITIVE");
92
- if (flags.includes("m")) javaFlags.push("Pattern.MULTILINE");
93
- if (flags.includes("s")) javaFlags.push("Pattern.DOTALL");
94
- const flagArg = javaFlags.length ? ", " + javaFlags.join(" | ") : "";
95
- return `// Java
96
- import java.util.regex.Pattern;
97
- import java.util.regex.Matcher;
98
-
99
- public class RegexDemo {
100
- public static void main(String[] args) {
101
- Pattern pattern = Pattern.compile("${pattern}"${flagArg});
102
- String text = "your text here";
103
- Matcher matcher = pattern.matcher(text);
104
-
105
- while (matcher.find()) {
106
- System.out.println("Match: " + matcher.group());
107
- }
108
- }
109
- }`;
110
- }
111
-
112
- const generators: Record<Language, (pattern: string, flags: string) => string> = {
113
- javascript: generateJS,
114
- python: generatePython,
115
- go: generateGo,
116
- rust: generateRust,
117
- java: generateJava,
118
- };
119
-
120
- export function generateRegexCode(
121
- pattern: string,
122
- flags: string,
123
- languages?: string[],
124
- ): string {
125
- const langs: Language[] = languages && languages.length
126
- ? (languages.map((l) => l.toLowerCase()) as Language[]).filter(
127
- (l) => l in generators,
128
- )
129
- : ALL_LANGUAGES;
130
-
131
- return langs.map((lang) => generators[lang](pattern, flags)).join("\n\n---\n\n");
132
- }
package/src/index.ts DELETED
@@ -1,134 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- } from "@modelcontextprotocol/sdk/types.js";
9
- import {
10
- checkProAccess,
11
- getTrialUsesRemaining,
12
- incrementTrialUse,
13
- } from "./auth.js";
14
- import {
15
- registerTool,
16
- getToolByName,
17
- getAllTools,
18
- } from "./registry.js";
19
- import type { ToolDefinition } from "./registry.js";
20
-
21
- // Re-export for tool files that import from index
22
- export { registerTool, getToolByName, getAllTools };
23
- export type { ToolDefinition };
24
-
25
- // ---------------------------------------------------------------------------
26
- // MCP Server
27
- // ---------------------------------------------------------------------------
28
-
29
- const PURCHASE_URL = "https://buy.stripe.com/28E14pfy1g5X1nz9HHgbm0s";
30
-
31
- const server = new Server(
32
- { name: "@revxl/devtools", version: "1.0.0" },
33
- { capabilities: { tools: {} } }
34
- );
35
-
36
- // --- ListTools ------------------------------------------------------------
37
-
38
- server.setRequestHandler(ListToolsRequestSchema, async () => {
39
- const toolList = getAllTools().map((t) => ({
40
- name: t.name,
41
- description: t.pro
42
- ? `${t.description} [PRO - 3 free trials]`
43
- : t.description,
44
- inputSchema: t.inputSchema,
45
- }));
46
- return { tools: toolList };
47
- });
48
-
49
- // --- CallTool -------------------------------------------------------------
50
-
51
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
52
- const { name, arguments: args } = request.params;
53
-
54
- const tool = getToolByName(name);
55
- if (!tool) {
56
- return {
57
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
58
- isError: true,
59
- };
60
- }
61
-
62
- // Pro gate
63
- if (tool.pro) {
64
- const isPro = await checkProAccess();
65
- if (!isPro) {
66
- const remaining = getTrialUsesRemaining(name);
67
- if (remaining <= 0) {
68
- return {
69
- content: [
70
- {
71
- type: "text",
72
- text: `⚡ ${name} is a Pro tool and you've used all 3 free trials.\n\nUpgrade for $7 one-time at ${PURCHASE_URL} to unlock unlimited access to all Pro tools.`,
73
- },
74
- ],
75
- isError: false,
76
- };
77
- }
78
- incrementTrialUse(name);
79
- }
80
- }
81
-
82
- try {
83
- const result = await tool.handler((args ?? {}) as Record<string, unknown>);
84
- const text =
85
- typeof result === "string" ? result : JSON.stringify(result, null, 2);
86
- return { content: [{ type: "text", text }] };
87
- } catch (err: unknown) {
88
- const message = err instanceof Error ? err.message : String(err);
89
- return {
90
- content: [{ type: "text", text: `Error in ${name}: ${message}` }],
91
- isError: true,
92
- };
93
- }
94
- });
95
-
96
- // ---------------------------------------------------------------------------
97
- // Tool imports
98
- // ---------------------------------------------------------------------------
99
-
100
- // Free tools
101
- import "./tools/json-format.js";
102
- import "./tools/base64.js";
103
- import "./tools/url-encode.js";
104
- import "./tools/uuid.js";
105
- import "./tools/hash.js";
106
- import "./tools/timestamp.js";
107
- import "./tools/http-status.js";
108
-
109
- // Pro tools
110
- import "./tools/jwt.js";
111
- import "./tools/regex.js";
112
- import "./tools/cron.js";
113
- import "./tools/json-diff.js";
114
- import "./tools/json-query.js";
115
- import "./tools/batch.js";
116
- import "./tools/sql-format.js";
117
- import "./tools/yaml-convert.js";
118
- import "./tools/chmod.js";
119
- import "./tools/secrets-scan.js";
120
-
121
- // ---------------------------------------------------------------------------
122
- // Start
123
- // ---------------------------------------------------------------------------
124
-
125
- async function main(): Promise<void> {
126
- const transport = new StdioServerTransport();
127
- await server.connect(transport);
128
- console.error("@revxl/devtools MCP server running on stdio");
129
- }
130
-
131
- main().catch((err) => {
132
- console.error("Fatal error:", err);
133
- process.exit(1);
134
- });
package/src/registry.ts DELETED
@@ -1,25 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Tool registry — separated to avoid circular import issues with ESM hoisting
3
- // ---------------------------------------------------------------------------
4
-
5
- export interface ToolDefinition {
6
- name: string;
7
- description: string;
8
- pro: boolean;
9
- inputSchema: Record<string, unknown>;
10
- handler: (args: Record<string, unknown>) => Promise<unknown>;
11
- }
12
-
13
- const tools: Map<string, ToolDefinition> = new Map();
14
-
15
- export function registerTool(tool: ToolDefinition): void {
16
- tools.set(tool.name, tool);
17
- }
18
-
19
- export function getToolByName(name: string): ToolDefinition | undefined {
20
- return tools.get(name);
21
- }
22
-
23
- export function getAllTools(): ToolDefinition[] {
24
- return Array.from(tools.values());
25
- }
@@ -1,32 +0,0 @@
1
- import { registerTool } from "../registry.js";
2
-
3
- registerTool({
4
- name: "base64",
5
- description: "Encode or decode Base64 strings",
6
- pro: false,
7
- inputSchema: {
8
- type: "object",
9
- properties: {
10
- text: { type: "string", description: "Text to encode or Base64 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 Buffer.from(text, "utf-8").toString("base64");
25
- }
26
-
27
- // Handle URL-safe Base64 variant
28
- const normalized = text.replace(/-/g, "+").replace(/_/g, "/");
29
- const decoded = Buffer.from(normalized, "base64").toString("utf-8");
30
- return decoded;
31
- },
32
- });
@@ -1,69 +0,0 @@
1
- import { registerTool, getToolByName } from "../registry.js";
2
-
3
- // ---------------------------------------------------------------------------
4
- // Batch runner — execute a free tool across multiple inputs
5
- // ---------------------------------------------------------------------------
6
-
7
- const MAX_ITEMS = 500;
8
-
9
- registerTool({
10
- name: "batch",
11
- description:
12
- "Run any free tool across multiple inputs in one call — up to 500 items. Only works with free (non-Pro) tools.",
13
- pro: true,
14
- inputSchema: {
15
- type: "object",
16
- properties: {
17
- tool: {
18
- type: "string",
19
- description: "Name of the tool to run (must be a free tool)",
20
- },
21
- items: {
22
- type: "array",
23
- items: { type: "object" },
24
- description: "Array of argument objects — each is passed to the tool's handler (max 500)",
25
- },
26
- },
27
- required: ["tool", "items"],
28
- },
29
- handler: async (args) => {
30
- const toolName = args.tool as string;
31
- const items = args.items as Record<string, unknown>[];
32
-
33
- if (!Array.isArray(items) || items.length === 0) {
34
- throw new Error("items must be a non-empty array");
35
- }
36
-
37
- if (items.length > MAX_ITEMS) {
38
- throw new Error(`Maximum ${MAX_ITEMS} items per batch (got ${items.length})`);
39
- }
40
-
41
- const tool = getToolByName(toolName);
42
- if (!tool) {
43
- throw new Error(`Unknown tool: ${toolName}`);
44
- }
45
-
46
- if (tool.pro) {
47
- throw new Error(
48
- `Batch only works with free tools. "${toolName}" is a Pro tool.`,
49
- );
50
- }
51
-
52
- const results: string[] = [];
53
-
54
- for (let i = 0; i < items.length; i++) {
55
- try {
56
- const result = await tool.handler(items[i]);
57
- const text =
58
- typeof result === "string" ? result : JSON.stringify(result, null, 2);
59
- results.push(`[${i}] ${text}`);
60
- } catch (err: unknown) {
61
- const message = err instanceof Error ? err.message : String(err);
62
- results.push(`[${i}] ERROR: ${message}`);
63
- }
64
- }
65
-
66
- const header = `Batch ${toolName}: ${items.length} items`;
67
- return `${header}\n${"=".repeat(header.length)}\n\n${results.join("\n\n")}`;
68
- },
69
- });
@@ -1,133 +0,0 @@
1
- import { registerTool } from "../registry.js";
2
-
3
- // ---------------------------------------------------------------------------
4
- // Chmod converter — numeric ↔ symbolic with human-readable explanation
5
- // ---------------------------------------------------------------------------
6
-
7
- const PERM_MAP: Record<number, string> = {
8
- 0: "---",
9
- 1: "--x",
10
- 2: "-w-",
11
- 3: "-wx",
12
- 4: "r--",
13
- 5: "r-x",
14
- 6: "rw-",
15
- 7: "rwx",
16
- };
17
-
18
- const ROLE_NAMES = ["Owner", "Group", "Others"] as const;
19
-
20
- const PERM_LABELS: Record<string, string> = {
21
- r: "read",
22
- w: "write",
23
- x: "execute",
24
- };
25
-
26
- function numericToSymbolic(mode: string): string {
27
- const digits = mode.split("").map(Number);
28
- return digits.map((d) => PERM_MAP[d]).join("");
29
- }
30
-
31
- function symbolicToNumeric(mode: string): string {
32
- // Expect 9-char symbolic like "rwxr-xr-x"
33
- const clean = mode.replace(/^-/, ""); // strip leading - if present (like -rwxr-xr-x)
34
- const chars = clean.length >= 9 ? clean.slice(0, 9) : clean;
35
- let result = "";
36
- for (let i = 0; i < 3; i++) {
37
- const group = chars.slice(i * 3, i * 3 + 3);
38
- let val = 0;
39
- if (group[0] === "r") val += 4;
40
- if (group[1] === "w") val += 2;
41
- if (group[2] === "x") val += 1;
42
- result += val;
43
- }
44
- return result;
45
- }
46
-
47
- function describePermissions(numericMode: string): string[] {
48
- const digits = numericMode.split("").map(Number);
49
- const descriptions: string[] = [];
50
-
51
- for (let i = 0; i < 3; i++) {
52
- const symbolic = PERM_MAP[digits[i]];
53
- const perms: string[] = [];
54
- for (const ch of symbolic) {
55
- if (ch !== "-" && PERM_LABELS[ch]) {
56
- perms.push(PERM_LABELS[ch]);
57
- }
58
- }
59
- const permStr = perms.length > 0 ? perms.join(", ") : "none";
60
- descriptions.push(`${ROLE_NAMES[i]}: ${permStr}`);
61
- }
62
-
63
- return descriptions;
64
- }
65
-
66
- function isNumericMode(mode: string): boolean {
67
- return /^[0-7]{3,4}$/.test(mode);
68
- }
69
-
70
- function isSymbolicMode(mode: string): boolean {
71
- // Accept rwxr-xr-x or -rwxr-xr-x
72
- return /^-?[rwx-]{9}$/.test(mode);
73
- }
74
-
75
- registerTool({
76
- name: "chmod",
77
- description:
78
- "Convert between numeric (755) and symbolic (rwxr-xr-x) chmod permissions with explanation",
79
- pro: true,
80
- inputSchema: {
81
- type: "object",
82
- properties: {
83
- mode: {
84
- type: "string",
85
- description:
86
- 'Permission mode — numeric (e.g. "755") or symbolic (e.g. "rwxr-xr-x")',
87
- },
88
- },
89
- required: ["mode"],
90
- },
91
- handler: async (args) => {
92
- const mode = (args.mode as string).trim();
93
-
94
- if (!mode) throw new Error("mode is required");
95
-
96
- if (isNumericMode(mode)) {
97
- // Take last 3 digits (ignore leading 0 in 0755)
98
- const digits = mode.slice(-3);
99
- const symbolic = numericToSymbolic(digits);
100
- const explanation = describePermissions(digits);
101
-
102
- return [
103
- "=== Chmod: Numeric → Symbolic ===",
104
- "",
105
- `Numeric: ${digits}`,
106
- `Symbolic: ${symbolic}`,
107
- "",
108
- "--- Permissions ---",
109
- ...explanation.map((e) => ` ${e}`),
110
- ].join("\n");
111
- }
112
-
113
- if (isSymbolicMode(mode)) {
114
- const clean = mode.startsWith("-") ? mode.slice(1) : mode;
115
- const numeric = symbolicToNumeric(clean);
116
- const explanation = describePermissions(numeric);
117
-
118
- return [
119
- "=== Chmod: Symbolic → Numeric ===",
120
- "",
121
- `Symbolic: ${clean}`,
122
- `Numeric: ${numeric}`,
123
- "",
124
- "--- Permissions ---",
125
- ...explanation.map((e) => ` ${e}`),
126
- ].join("\n");
127
- }
128
-
129
- throw new Error(
130
- `Invalid mode: "${mode}". Use numeric (e.g. "755") or symbolic (e.g. "rwxr-xr-x").`,
131
- );
132
- },
133
- });