@utilix-tech/mcp 0.64.0 → 0.65.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/CHANGELOG.md +4 -0
- package/README.md +6 -3
- package/dist/index.js +253 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
|
|
4
4
|
|
|
5
|
+
## 0.65.0
|
|
6
|
+
- Add `build_hsts_header`, `parse_hsts_header` tools: build a Strict-Transport-Security response header from a max-age plus includeSubDomains/preload flags, flagging combinations that won't qualify for browser HSTS preload lists, or parse an existing header into the same shape.
|
|
7
|
+
- Add `align_markdown_tables` tool: reformat every GitHub-Flavored-Markdown pipe table in a document so each column's pipes line up visually, preserving left/center/right alignment markers and skipping tables inside fenced code blocks.
|
|
8
|
+
|
|
5
9
|
## 0.64.0
|
|
6
10
|
- Add `calculate_loan_refinance` tool: compare a current loan's remaining payments against a new refinanced loan on the same outstanding balance, and compute the monthly savings and the break-even month where cumulative savings recoup the closing costs.
|
|
7
11
|
- Add `parse_multipart_form_data` tool: parse a raw multipart/form-data request body into its individual parts (field name, filename, headers, decoded value) per RFC 7578, with an optional Content-Type header value to extract the boundary from.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 223 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
|
|
4
4
|
|
|
5
5
|
No API key required. All tools run locally. Requires Node.js 18 or later.
|
|
6
6
|
|
|
@@ -205,7 +205,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
205
205
|
| `plan_debt_payoff` | Simulate paying off a list of debts under both the snowball (smallest balance first) and avalanche (highest rate first) strategies given a fixed extra monthly payment, and compare months to payoff and total interest |
|
|
206
206
|
| `calculate_loan_refinance` | Compare a current loan's remaining payments against a new refinanced loan on the same outstanding balance, and compute the monthly savings and the break-even month where cumulative savings recoup the closing costs |
|
|
207
207
|
|
|
208
|
-
### API / Network (
|
|
208
|
+
### API / Network (40 tools)
|
|
209
209
|
| Tool | Description |
|
|
210
210
|
|------|-------------|
|
|
211
211
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -248,6 +248,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
248
248
|
| `lookup_close_code` | Look up what a WebSocket close code means, per RFC 6455 and the IANA reserved ranges |
|
|
249
249
|
| `list_close_codes` | List every well-known WebSocket close code (1000-1015) |
|
|
250
250
|
| `parse_multipart_form_data` | Parse a raw multipart/form-data request body into its individual parts: field name, filename, headers, and decoded value, per RFC 7578 |
|
|
251
|
+
| `build_hsts_header` | Build a Strict-Transport-Security response header from a max-age plus includeSubDomains/preload flags, flagging combinations that won't qualify for browser HSTS preload lists |
|
|
252
|
+
| `parse_hsts_header` | Parse an existing Strict-Transport-Security header value into the same shape, with preload-list eligibility flagged |
|
|
251
253
|
|
|
252
254
|
### Color (4 tools)
|
|
253
255
|
| Tool | Description |
|
|
@@ -277,7 +279,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
277
279
|
| `validate_package_exports` | Validate a package.json "exports" field: dual-package (ESM/CJS) misconfigurations like a missing default fallback or a require/import pointing at the wrong extension |
|
|
278
280
|
| `audit_dependency_ranges` | Classify each package.json dependency's version range type and flag wildcard ranges, the "latest" tag, git/URL/file/link dependencies, and packages listed with conflicting or redundant ranges across sections |
|
|
279
281
|
|
|
280
|
-
### Data (
|
|
282
|
+
### Data (16 tools)
|
|
281
283
|
| Tool | Description |
|
|
282
284
|
|------|-------------|
|
|
283
285
|
| `validate_yaml` | Validate YAML syntax |
|
|
@@ -295,6 +297,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
295
297
|
| `calculate_markdown_task_list_progress` | Parse a Markdown document's checkbox task list and report completion percentage overall and per section, grouped by the nearest preceding heading |
|
|
296
298
|
| `analyze_caption_speed` | Parse an SRT or WebVTT file and report the words-per-minute reading speed of every cue, flagging ones that exceed common broadcast accessibility guidelines |
|
|
297
299
|
| `infer_csv_column_types` | Parse CSV/TSV data and infer each column's data type (integer, float, boolean, ISO 8601 date, or string), flagging nullable columns and empty-cell counts |
|
|
300
|
+
| `align_markdown_tables` | Reformat every GitHub-Flavored-Markdown pipe table in a document so each column's pipes line up visually, preserving left/center/right alignment markers |
|
|
298
301
|
|
|
299
302
|
### CSS (2 tools)
|
|
300
303
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -6120,6 +6120,153 @@ server.tool(
|
|
|
6120
6120
|
return text(JSON.stringify(result, null, 2));
|
|
6121
6121
|
}
|
|
6122
6122
|
);
|
|
6123
|
+
var _MD_TABLE_DELIM_CELL_RE_MCP = /^:?-+:?$/;
|
|
6124
|
+
function _splitMarkdownTableRowMcp(line) {
|
|
6125
|
+
const raw = [];
|
|
6126
|
+
let current = "";
|
|
6127
|
+
for (let i = 0; i < line.length; i++) {
|
|
6128
|
+
const ch = line[i];
|
|
6129
|
+
if (ch === "\\" && line[i + 1] === "|") {
|
|
6130
|
+
current += "\\|";
|
|
6131
|
+
i++;
|
|
6132
|
+
continue;
|
|
6133
|
+
}
|
|
6134
|
+
if (ch === "|") {
|
|
6135
|
+
raw.push(current);
|
|
6136
|
+
current = "";
|
|
6137
|
+
continue;
|
|
6138
|
+
}
|
|
6139
|
+
current += ch;
|
|
6140
|
+
}
|
|
6141
|
+
raw.push(current);
|
|
6142
|
+
const cells = raw.map((c) => c.trim());
|
|
6143
|
+
if (cells.length > 1 && cells[0] === "") cells.shift();
|
|
6144
|
+
if (cells.length > 1 && cells[cells.length - 1] === "") cells.pop();
|
|
6145
|
+
return cells;
|
|
6146
|
+
}
|
|
6147
|
+
function _isMarkdownTableDelimiterRowMcp(line) {
|
|
6148
|
+
const trimmed = line.trim();
|
|
6149
|
+
if (trimmed === "" || !trimmed.includes("-")) return false;
|
|
6150
|
+
const cells = _splitMarkdownTableRowMcp(trimmed);
|
|
6151
|
+
if (cells.length === 0) return false;
|
|
6152
|
+
return cells.every((c) => _MD_TABLE_DELIM_CELL_RE_MCP.test(c));
|
|
6153
|
+
}
|
|
6154
|
+
function _markdownTableCellAlignmentMcp(delimCell) {
|
|
6155
|
+
const left = delimCell.startsWith(":");
|
|
6156
|
+
const right = delimCell.endsWith(":");
|
|
6157
|
+
if (left && right) return "center";
|
|
6158
|
+
if (right) return "right";
|
|
6159
|
+
if (left) return "left";
|
|
6160
|
+
return "none";
|
|
6161
|
+
}
|
|
6162
|
+
function _padMarkdownTableCellMcp(text2, width, alignment) {
|
|
6163
|
+
const diff = Math.max(0, width - [...text2].length);
|
|
6164
|
+
if (alignment === "right") return " ".repeat(diff) + text2;
|
|
6165
|
+
if (alignment === "center") {
|
|
6166
|
+
const left = Math.floor(diff / 2);
|
|
6167
|
+
const right = diff - left;
|
|
6168
|
+
return " ".repeat(left) + text2 + " ".repeat(right);
|
|
6169
|
+
}
|
|
6170
|
+
return text2 + " ".repeat(diff);
|
|
6171
|
+
}
|
|
6172
|
+
function _markdownTableDelimiterCellMcp(width, alignment) {
|
|
6173
|
+
switch (alignment) {
|
|
6174
|
+
case "left":
|
|
6175
|
+
return ":" + "-".repeat(Math.max(1, width - 1));
|
|
6176
|
+
case "right":
|
|
6177
|
+
return "-".repeat(Math.max(1, width - 1)) + ":";
|
|
6178
|
+
case "center":
|
|
6179
|
+
return ":" + "-".repeat(Math.max(1, width - 2)) + ":";
|
|
6180
|
+
default:
|
|
6181
|
+
return "-".repeat(width);
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
function _alignMarkdownTablesMcp(markdown) {
|
|
6185
|
+
if (typeof markdown !== "string") return { error: "markdown must be a string" };
|
|
6186
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
6187
|
+
const outputLines = [...lines];
|
|
6188
|
+
const tables = [];
|
|
6189
|
+
const warnings = [];
|
|
6190
|
+
let inFence = false;
|
|
6191
|
+
let fenceMarker = "";
|
|
6192
|
+
let i = 0;
|
|
6193
|
+
while (i < lines.length) {
|
|
6194
|
+
const line = lines[i];
|
|
6195
|
+
const fenceMatch = line.match(/^\s*(```+|~~~+)/);
|
|
6196
|
+
if (fenceMatch) {
|
|
6197
|
+
if (!inFence) {
|
|
6198
|
+
inFence = true;
|
|
6199
|
+
fenceMarker = fenceMatch[1][0];
|
|
6200
|
+
} else if (fenceMatch[1][0] === fenceMarker) {
|
|
6201
|
+
inFence = false;
|
|
6202
|
+
}
|
|
6203
|
+
i++;
|
|
6204
|
+
continue;
|
|
6205
|
+
}
|
|
6206
|
+
if (inFence) {
|
|
6207
|
+
i++;
|
|
6208
|
+
continue;
|
|
6209
|
+
}
|
|
6210
|
+
const nextLine = lines[i + 1];
|
|
6211
|
+
if (line.includes("|") && nextLine !== void 0 && _isMarkdownTableDelimiterRowMcp(nextLine)) {
|
|
6212
|
+
const headerCells = _splitMarkdownTableRowMcp(line);
|
|
6213
|
+
const delimCells = _splitMarkdownTableRowMcp(nextLine);
|
|
6214
|
+
if (headerCells.length > 0 && headerCells.length === delimCells.length) {
|
|
6215
|
+
const columnCount = headerCells.length;
|
|
6216
|
+
const alignments = delimCells.map(_markdownTableCellAlignmentMcp);
|
|
6217
|
+
let j = i + 2;
|
|
6218
|
+
const bodyRows = [];
|
|
6219
|
+
while (j < lines.length && lines[j].trim() !== "" && lines[j].includes("|")) {
|
|
6220
|
+
bodyRows.push(_splitMarkdownTableRowMcp(lines[j]));
|
|
6221
|
+
j++;
|
|
6222
|
+
}
|
|
6223
|
+
const normalizedBody = [];
|
|
6224
|
+
for (const row of bodyRows) {
|
|
6225
|
+
if (row.length !== columnCount) {
|
|
6226
|
+
warnings.push(`Table at line ${i + 1}: a row has ${row.length} cell(s) but the header has ${columnCount}; it was padded/truncated to match.`);
|
|
6227
|
+
}
|
|
6228
|
+
normalizedBody.push(Array.from({ length: columnCount }, (_, c) => row[c] ?? ""));
|
|
6229
|
+
}
|
|
6230
|
+
const allRows = [headerCells, ...normalizedBody];
|
|
6231
|
+
const widths = Array.from(
|
|
6232
|
+
{ length: columnCount },
|
|
6233
|
+
(_, c) => Math.max(3, ...allRows.map((r) => [...r[c] ?? ""].length))
|
|
6234
|
+
);
|
|
6235
|
+
const formattedHeader = "| " + headerCells.map((c, idx) => _padMarkdownTableCellMcp(c, widths[idx], alignments[idx])).join(" | ") + " |";
|
|
6236
|
+
const formattedDelim = "| " + widths.map((w, idx) => _markdownTableDelimiterCellMcp(w, alignments[idx])).join(" | ") + " |";
|
|
6237
|
+
const formattedBody = normalizedBody.map((row) => "| " + row.map((c, idx) => _padMarkdownTableCellMcp(c, widths[idx], alignments[idx])).join(" | ") + " |");
|
|
6238
|
+
const formattedLines = [formattedHeader, formattedDelim, ...formattedBody];
|
|
6239
|
+
for (let k = 0; k < formattedLines.length; k++) {
|
|
6240
|
+
outputLines[i + k] = formattedLines[k];
|
|
6241
|
+
}
|
|
6242
|
+
tables.push({
|
|
6243
|
+
startLine: i + 1,
|
|
6244
|
+
endLine: i + formattedLines.length,
|
|
6245
|
+
columnCount,
|
|
6246
|
+
rowCount: normalizedBody.length,
|
|
6247
|
+
alignments
|
|
6248
|
+
});
|
|
6249
|
+
i = j;
|
|
6250
|
+
continue;
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
i++;
|
|
6254
|
+
}
|
|
6255
|
+
if (tables.length === 0) return { error: "No Markdown tables found" };
|
|
6256
|
+
return { output: outputLines.join("\n"), tables, warnings };
|
|
6257
|
+
}
|
|
6258
|
+
server.tool(
|
|
6259
|
+
"align_markdown_tables",
|
|
6260
|
+
"Reformat every GitHub-Flavored-Markdown pipe table in a document so each column's pipes line up visually, padding cells to the widest content in that column and rebuilding the delimiter row to match, while preserving each column's declared alignment. Tables inside fenced code blocks are left untouched.",
|
|
6261
|
+
{
|
|
6262
|
+
markdown: z.string().describe("The Markdown document containing one or more pipe tables")
|
|
6263
|
+
},
|
|
6264
|
+
async ({ markdown }) => {
|
|
6265
|
+
const result = _alignMarkdownTablesMcp(markdown);
|
|
6266
|
+
if ("error" in result) return errText(result);
|
|
6267
|
+
return text(JSON.stringify(result, null, 2));
|
|
6268
|
+
}
|
|
6269
|
+
);
|
|
6123
6270
|
var _COMMIT_DEFAULT_TYPES_MCP = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"];
|
|
6124
6271
|
var _COMMIT_HEADER_RE_MCP = /^([A-Za-z][A-Za-z0-9-]*)(\(([^()]*)\))?(!)?: (.+)$/;
|
|
6125
6272
|
var _COMMIT_FOOTER_LINE_RE_MCP = /^([A-Za-z][A-Za-z0-9-]*|BREAKING CHANGE)(: ?| #)(.*)$/;
|
|
@@ -9968,6 +10115,112 @@ server.tool(
|
|
|
9968
10115
|
return text(JSON.stringify({ codes }, null, 2));
|
|
9969
10116
|
}
|
|
9970
10117
|
);
|
|
10118
|
+
var _HSTS_ONE_YEAR_SECONDS_MCP = 31536e3;
|
|
10119
|
+
function _humanizeHstsMaxAgeMcp(seconds) {
|
|
10120
|
+
if (seconds === 0) return "0 seconds (clears HSTS)";
|
|
10121
|
+
if (seconds % _HSTS_ONE_YEAR_SECONDS_MCP === 0) {
|
|
10122
|
+
const years = seconds / _HSTS_ONE_YEAR_SECONDS_MCP;
|
|
10123
|
+
return `${years} year${years === 1 ? "" : "s"}`;
|
|
10124
|
+
}
|
|
10125
|
+
if (seconds % 86400 === 0) {
|
|
10126
|
+
const days = seconds / 86400;
|
|
10127
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
10128
|
+
}
|
|
10129
|
+
if (seconds % 3600 === 0) {
|
|
10130
|
+
const hours = seconds / 3600;
|
|
10131
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
10132
|
+
}
|
|
10133
|
+
return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
10134
|
+
}
|
|
10135
|
+
function _buildHstsMcp(options) {
|
|
10136
|
+
const { maxAge, includeSubDomains = false, preload = false } = options;
|
|
10137
|
+
if (!Number.isInteger(maxAge) || maxAge < 0) {
|
|
10138
|
+
return { error: "maxAge must be a non-negative integer number of seconds" };
|
|
10139
|
+
}
|
|
10140
|
+
const warnings = [];
|
|
10141
|
+
const preloadEligible = maxAge >= _HSTS_ONE_YEAR_SECONDS_MCP && includeSubDomains && preload;
|
|
10142
|
+
if (preload && !includeSubDomains) {
|
|
10143
|
+
warnings.push("Browser HSTS preload lists require includeSubDomains whenever preload is set; this header will not qualify for preloading as configured.");
|
|
10144
|
+
}
|
|
10145
|
+
if (preload && maxAge < _HSTS_ONE_YEAR_SECONDS_MCP) {
|
|
10146
|
+
warnings.push(`Browser HSTS preload lists require max-age of at least ${_HSTS_ONE_YEAR_SECONDS_MCP} (1 year); ${maxAge} is too low to qualify.`);
|
|
10147
|
+
}
|
|
10148
|
+
if (maxAge === 0) {
|
|
10149
|
+
warnings.push("max-age=0 tells the browser to forget this host supports HSTS immediately, this clears HSTS rather than enabling it.");
|
|
10150
|
+
} else if (maxAge < 86400) {
|
|
10151
|
+
warnings.push("A max-age under a day gives very little protection; most real deployments use at least 15552000 (180 days) once confident HTTPS is fully working.");
|
|
10152
|
+
}
|
|
10153
|
+
const parts = [`max-age=${maxAge}`];
|
|
10154
|
+
if (includeSubDomains) parts.push("includeSubDomains");
|
|
10155
|
+
if (preload) parts.push("preload");
|
|
10156
|
+
const header = parts.join("; ");
|
|
10157
|
+
return {
|
|
10158
|
+
header,
|
|
10159
|
+
headerName: "Strict-Transport-Security",
|
|
10160
|
+
snippet: `Strict-Transport-Security: ${header}`,
|
|
10161
|
+
maxAge,
|
|
10162
|
+
maxAgeHuman: _humanizeHstsMaxAgeMcp(maxAge),
|
|
10163
|
+
includeSubDomains,
|
|
10164
|
+
preload,
|
|
10165
|
+
preloadEligible,
|
|
10166
|
+
warnings
|
|
10167
|
+
};
|
|
10168
|
+
}
|
|
10169
|
+
function _parseHstsMcp(raw) {
|
|
10170
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
10171
|
+
const withoutLabel = raw.trim().replace(/^strict-transport-security:\s*/i, "");
|
|
10172
|
+
const tokens = withoutLabel.split(";").map((t) => t.trim()).filter((t) => t !== "");
|
|
10173
|
+
if (tokens.length === 0) return { error: "No directives found" };
|
|
10174
|
+
let maxAge = null;
|
|
10175
|
+
let includeSubDomains = false;
|
|
10176
|
+
let preload = false;
|
|
10177
|
+
for (const token of tokens) {
|
|
10178
|
+
const eq = token.indexOf("=");
|
|
10179
|
+
const name = (eq === -1 ? token : token.slice(0, eq)).trim().toLowerCase();
|
|
10180
|
+
if (name === "max-age") {
|
|
10181
|
+
const rawValue = eq === -1 ? "" : token.slice(eq + 1).trim();
|
|
10182
|
+
const n = Number(rawValue);
|
|
10183
|
+
if (rawValue === "" || !Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
|
|
10184
|
+
return { error: `max-age value "${rawValue}" is not a valid non-negative integer` };
|
|
10185
|
+
}
|
|
10186
|
+
maxAge = n;
|
|
10187
|
+
} else if (name === "includesubdomains") {
|
|
10188
|
+
includeSubDomains = true;
|
|
10189
|
+
} else if (name === "preload") {
|
|
10190
|
+
preload = true;
|
|
10191
|
+
} else {
|
|
10192
|
+
return { error: `Unrecognized directive "${token}"` };
|
|
10193
|
+
}
|
|
10194
|
+
}
|
|
10195
|
+
if (maxAge === null) return { error: "max-age directive is required" };
|
|
10196
|
+
return _buildHstsMcp({ maxAge, includeSubDomains, preload });
|
|
10197
|
+
}
|
|
10198
|
+
server.tool(
|
|
10199
|
+
"build_hsts_header",
|
|
10200
|
+
"Build a Strict-Transport-Security response-header value from a max-age (in seconds) plus optional includeSubDomains and preload directives, flagging combinations that will not qualify for browser HSTS preload lists (which require max-age >= 1 year, includeSubDomains, and preload all together).",
|
|
10201
|
+
{
|
|
10202
|
+
maxAge: z.number().int().min(0).describe("How long, in seconds, the browser should remember this host is HTTPS-only, e.g. 31536000 for 1 year"),
|
|
10203
|
+
includeSubDomains: z.boolean().optional().describe("Whether the policy also applies to all subdomains, default false"),
|
|
10204
|
+
preload: z.boolean().optional().describe("Whether to opt into browser HSTS preload lists, default false")
|
|
10205
|
+
},
|
|
10206
|
+
async ({ maxAge, includeSubDomains, preload }) => {
|
|
10207
|
+
const result = _buildHstsMcp({ maxAge, includeSubDomains, preload });
|
|
10208
|
+
if ("error" in result) return errText(result);
|
|
10209
|
+
return text(JSON.stringify(result, null, 2));
|
|
10210
|
+
}
|
|
10211
|
+
);
|
|
10212
|
+
server.tool(
|
|
10213
|
+
"parse_hsts_header",
|
|
10214
|
+
'Parse an existing Strict-Transport-Security header value (with or without a leading "Strict-Transport-Security:" label) into its max-age, includeSubDomains, and preload fields, flagging whether it qualifies for browser HSTS preload lists.',
|
|
10215
|
+
{
|
|
10216
|
+
header: z.string().min(1).describe("The Strict-Transport-Security header value to parse")
|
|
10217
|
+
},
|
|
10218
|
+
async ({ header }) => {
|
|
10219
|
+
const result = _parseHstsMcp(header);
|
|
10220
|
+
if ("error" in result) return errText(result);
|
|
10221
|
+
return text(JSON.stringify(result, null, 2));
|
|
10222
|
+
}
|
|
10223
|
+
);
|
|
9971
10224
|
var _DEBT_PAYOFF_MAX_MONTHS_MCP = 1200;
|
|
9972
10225
|
function _simulateDebtPayoffMcp(debts, order, extraMonthlyPayment) {
|
|
9973
10226
|
const round2 = (n) => Math.round(n * 100) / 100;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"description": "MCP server exposing 198 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
|
|
5
5
|
"author": "Utilix <hello@utilix.tech>",
|
|
6
6
|
"license": "MIT",
|