@unclick/mcp-server 0.2.4 → 0.2.5
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/dist/algolia-tool.d.ts +5 -0
- package/dist/algolia-tool.d.ts.map +1 -0
- package/dist/algolia-tool.js +121 -0
- package/dist/algolia-tool.js.map +1 -0
- package/dist/assemblyai-tool.d.ts +7 -0
- package/dist/assemblyai-tool.d.ts.map +1 -0
- package/dist/assemblyai-tool.js +127 -0
- package/dist/assemblyai-tool.js.map +1 -0
- package/dist/circleci-tool.d.ts +7 -0
- package/dist/circleci-tool.d.ts.map +1 -0
- package/dist/circleci-tool.js +133 -0
- package/dist/circleci-tool.js.map +1 -0
- package/dist/cohere-tool.d.ts +7 -0
- package/dist/cohere-tool.d.ts.map +1 -0
- package/dist/cohere-tool.js +225 -0
- package/dist/cohere-tool.js.map +1 -0
- package/dist/convertkit-tool.d.ts +7 -0
- package/dist/convertkit-tool.d.ts.map +1 -0
- package/dist/convertkit-tool.js +213 -0
- package/dist/convertkit-tool.js.map +1 -0
- package/dist/datadog-tool.d.ts +7 -0
- package/dist/datadog-tool.d.ts.map +1 -0
- package/dist/datadog-tool.js +121 -0
- package/dist/datadog-tool.js.map +1 -0
- package/dist/deepl-tool.d.ts +5 -0
- package/dist/deepl-tool.d.ts.map +1 -0
- package/dist/deepl-tool.js +137 -0
- package/dist/deepl-tool.js.map +1 -0
- package/dist/flyio-tool.d.ts +6 -0
- package/dist/flyio-tool.d.ts.map +1 -0
- package/dist/flyio-tool.js +158 -0
- package/dist/flyio-tool.js.map +1 -0
- package/dist/groq-tool.d.ts +3 -0
- package/dist/groq-tool.d.ts.map +1 -0
- package/dist/groq-tool.js +109 -0
- package/dist/groq-tool.js.map +1 -0
- package/dist/gumroad-tool.d.ts +6 -0
- package/dist/gumroad-tool.d.ts.map +1 -0
- package/dist/gumroad-tool.js +90 -0
- package/dist/gumroad-tool.js.map +1 -0
- package/dist/heygen-tool.d.ts +5 -0
- package/dist/heygen-tool.d.ts.map +1 -0
- package/dist/heygen-tool.js +134 -0
- package/dist/heygen-tool.js.map +1 -0
- package/dist/higgsfield-tool.d.ts +5 -0
- package/dist/higgsfield-tool.d.ts.map +1 -0
- package/dist/higgsfield-tool.js +120 -0
- package/dist/higgsfield-tool.js.map +1 -0
- package/dist/keychain-crypto.d.ts +24 -0
- package/dist/keychain-crypto.d.ts.map +1 -0
- package/dist/keychain-crypto.js +60 -0
- package/dist/keychain-crypto.js.map +1 -0
- package/dist/keychain-secure-input.d.ts +17 -0
- package/dist/keychain-secure-input.d.ts.map +1 -0
- package/dist/keychain-secure-input.js +229 -0
- package/dist/keychain-secure-input.js.map +1 -0
- package/dist/keychain-tool.d.ts +3 -0
- package/dist/keychain-tool.d.ts.map +1 -0
- package/dist/keychain-tool.js +516 -0
- package/dist/keychain-tool.js.map +1 -0
- package/dist/kling-tool.d.ts +3 -0
- package/dist/kling-tool.d.ts.map +1 -0
- package/dist/kling-tool.js +102 -0
- package/dist/kling-tool.js.map +1 -0
- package/dist/lemonsqueezy-tool.d.ts +7 -0
- package/dist/lemonsqueezy-tool.d.ts.map +1 -0
- package/dist/lemonsqueezy-tool.js +220 -0
- package/dist/lemonsqueezy-tool.js.map +1 -0
- package/dist/local-catalog-handlers.d.ts +3 -0
- package/dist/local-catalog-handlers.d.ts.map +1 -0
- package/dist/local-catalog-handlers.js +1254 -0
- package/dist/local-catalog-handlers.js.map +1 -0
- package/dist/mailchimp-tool.d.ts +8 -0
- package/dist/mailchimp-tool.d.ts.map +1 -0
- package/dist/mailchimp-tool.js +138 -0
- package/dist/mailchimp-tool.js.map +1 -0
- package/dist/mapbox-tool.d.ts +6 -0
- package/dist/mapbox-tool.d.ts.map +1 -0
- package/dist/mapbox-tool.js +106 -0
- package/dist/mapbox-tool.js.map +1 -0
- package/dist/mistral-tool.d.ts +4 -0
- package/dist/mistral-tool.d.ts.map +1 -0
- package/dist/mistral-tool.js +145 -0
- package/dist/mistral-tool.js.map +1 -0
- package/dist/mixpanel-tool.d.ts +6 -0
- package/dist/mixpanel-tool.d.ts.map +1 -0
- package/dist/mixpanel-tool.js +162 -0
- package/dist/mixpanel-tool.js.map +1 -0
- package/dist/neon-tool.d.ts +7 -0
- package/dist/neon-tool.d.ts.map +1 -0
- package/dist/neon-tool.js +156 -0
- package/dist/neon-tool.js.map +1 -0
- package/dist/pagerduty-tool.d.ts +8 -0
- package/dist/pagerduty-tool.d.ts.map +1 -0
- package/dist/pagerduty-tool.js +185 -0
- package/dist/pagerduty-tool.js.map +1 -0
- package/dist/perplexity-tool.d.ts +2 -0
- package/dist/perplexity-tool.d.ts.map +1 -0
- package/dist/perplexity-tool.js +93 -0
- package/dist/perplexity-tool.js.map +1 -0
- package/dist/pika-tool.d.ts +4 -0
- package/dist/pika-tool.d.ts.map +1 -0
- package/dist/pika-tool.js +102 -0
- package/dist/pika-tool.js.map +1 -0
- package/dist/pinecone-tool.d.ts +6 -0
- package/dist/pinecone-tool.d.ts.map +1 -0
- package/dist/pinecone-tool.js +148 -0
- package/dist/pinecone-tool.js.map +1 -0
- package/dist/postmark-tool.d.ts +7 -0
- package/dist/postmark-tool.d.ts.map +1 -0
- package/dist/postmark-tool.js +148 -0
- package/dist/postmark-tool.js.map +1 -0
- package/dist/qc-tool.d.ts +4 -0
- package/dist/qc-tool.d.ts.map +1 -0
- package/dist/qc-tool.js +415 -0
- package/dist/qc-tool.js.map +1 -0
- package/dist/render-tool.d.ts +7 -0
- package/dist/render-tool.d.ts.map +1 -0
- package/dist/render-tool.js +158 -0
- package/dist/render-tool.js.map +1 -0
- package/dist/runway-tool.d.ts +4 -0
- package/dist/runway-tool.d.ts.map +1 -0
- package/dist/runway-tool.js +110 -0
- package/dist/runway-tool.js.map +1 -0
- package/dist/segment-tool.d.ts +6 -0
- package/dist/segment-tool.d.ts.map +1 -0
- package/dist/segment-tool.js +129 -0
- package/dist/segment-tool.js.map +1 -0
- package/dist/sendgrid-tool.d.ts +7 -0
- package/dist/sendgrid-tool.d.ts.map +1 -0
- package/dist/sendgrid-tool.js +124 -0
- package/dist/sendgrid-tool.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +10 -0
- package/dist/server.js.map +1 -1
- package/dist/togetherai-tool.d.ts +5 -0
- package/dist/togetherai-tool.d.ts.map +1 -0
- package/dist/togetherai-tool.js +129 -0
- package/dist/togetherai-tool.js.map +1 -0
- package/dist/tool-wiring.d.ts +4608 -692
- package/dist/tool-wiring.d.ts.map +1 -1
- package/dist/tool-wiring.js +2946 -463
- package/dist/tool-wiring.js.map +1 -1
- package/dist/turso-tool.d.ts +6 -0
- package/dist/turso-tool.d.ts.map +1 -0
- package/dist/turso-tool.js +158 -0
- package/dist/turso-tool.js.map +1 -0
- package/dist/upstash-tool.d.ts +8 -0
- package/dist/upstash-tool.d.ts.map +1 -0
- package/dist/upstash-tool.js +191 -0
- package/dist/upstash-tool.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
// local-catalog-handlers.ts
|
|
2
|
+
// Local implementations of all catalog endpoint handlers.
|
|
3
|
+
// Used by unclick_call to avoid remote API calls to api.unclick.world.
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { parse as csvParse } from "csv-parse/sync";
|
|
6
|
+
import { stringify as csvStringify } from "csv-stringify/sync";
|
|
7
|
+
import { marked } from "marked";
|
|
8
|
+
// ─── In-memory KV store (per-process session) ────────────────────────────────
|
|
9
|
+
const KV_STORE = new Map();
|
|
10
|
+
// ─── Crypto-quality random ───────────────────────────────────────────────────
|
|
11
|
+
function secureRandom() {
|
|
12
|
+
const arr = new Uint32Array(1);
|
|
13
|
+
crypto.getRandomValues(arr);
|
|
14
|
+
return arr[0] / 0x100000000;
|
|
15
|
+
}
|
|
16
|
+
// ─── Parse timestamp to Date ─────────────────────────────────────────────────
|
|
17
|
+
function parseTs(ts) {
|
|
18
|
+
if (typeof ts === "number") {
|
|
19
|
+
return ts > 9_999_999_999 ? new Date(ts) : new Date(ts * 1000);
|
|
20
|
+
}
|
|
21
|
+
return new Date(ts);
|
|
22
|
+
}
|
|
23
|
+
function hexToRgb(hex) {
|
|
24
|
+
const h = hex.replace(/^#/, "");
|
|
25
|
+
const n = parseInt(h.length === 3 ? h.split("").map((c) => c + c).join("") : h, 16);
|
|
26
|
+
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
27
|
+
}
|
|
28
|
+
function rgbToHex({ r, g, b }) {
|
|
29
|
+
return "#" + [r, g, b].map((n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0")).join("");
|
|
30
|
+
}
|
|
31
|
+
function rgbToHsl({ r, g, b }) {
|
|
32
|
+
const rr = r / 255, gg = g / 255, bb = b / 255;
|
|
33
|
+
const max = Math.max(rr, gg, bb), min = Math.min(rr, gg, bb);
|
|
34
|
+
const l = (max + min) / 2;
|
|
35
|
+
if (max === min)
|
|
36
|
+
return { h: 0, s: 0, l: Math.round(l * 100) };
|
|
37
|
+
const d = max - min;
|
|
38
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
39
|
+
let h = 0;
|
|
40
|
+
if (max === rr)
|
|
41
|
+
h = ((gg - bb) / d + (gg < bb ? 6 : 0)) / 6;
|
|
42
|
+
else if (max === gg)
|
|
43
|
+
h = ((bb - rr) / d + 2) / 6;
|
|
44
|
+
else
|
|
45
|
+
h = ((rr - gg) / d + 4) / 6;
|
|
46
|
+
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
47
|
+
}
|
|
48
|
+
function hslToRgb({ h, s, l }) {
|
|
49
|
+
const ss = s / 100, ll = l / 100;
|
|
50
|
+
if (ss === 0) {
|
|
51
|
+
const v = Math.round(ll * 255);
|
|
52
|
+
return { r: v, g: v, b: v };
|
|
53
|
+
}
|
|
54
|
+
const q = ll < 0.5 ? ll * (1 + ss) : ll + ss - ll * ss;
|
|
55
|
+
const p = 2 * ll - q;
|
|
56
|
+
const hk = h / 360;
|
|
57
|
+
function hue2rgb(t) {
|
|
58
|
+
if (t < 0)
|
|
59
|
+
t += 1;
|
|
60
|
+
if (t > 1)
|
|
61
|
+
t -= 1;
|
|
62
|
+
if (t < 1 / 6)
|
|
63
|
+
return p + (q - p) * 6 * t;
|
|
64
|
+
if (t < 1 / 2)
|
|
65
|
+
return q;
|
|
66
|
+
if (t < 2 / 3)
|
|
67
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
r: Math.round(hue2rgb(hk + 1 / 3) * 255),
|
|
72
|
+
g: Math.round(hue2rgb(hk) * 255),
|
|
73
|
+
b: Math.round(hue2rgb(hk - 1 / 3) * 255),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function parseColorToRgb(color) {
|
|
77
|
+
const s = color.trim();
|
|
78
|
+
if (s.startsWith("#")) {
|
|
79
|
+
try {
|
|
80
|
+
return hexToRgb(s);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const rgb = s.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
|
|
87
|
+
if (rgb)
|
|
88
|
+
return { r: parseInt(rgb[1]), g: parseInt(rgb[2]), b: parseInt(rgb[3]) };
|
|
89
|
+
const hsl = s.match(/^hsl\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*\)$/i);
|
|
90
|
+
if (hsl)
|
|
91
|
+
return hslToRgb({ h: parseInt(hsl[1]), s: parseInt(hsl[2]), l: parseInt(hsl[3]) });
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function colorInfo(rgb) {
|
|
95
|
+
const hsl = rgbToHsl(rgb);
|
|
96
|
+
return {
|
|
97
|
+
hex: rgbToHex(rgb),
|
|
98
|
+
rgb: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
|
|
99
|
+
rgb_object: rgb,
|
|
100
|
+
hsl: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`,
|
|
101
|
+
hsl_object: hsl,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// ─── Diff helper ──────────────────────────────────────────────────────────────
|
|
105
|
+
function computeDiff(a, b, mode) {
|
|
106
|
+
const tokensA = mode === "line" ? a.split("\n") : mode === "word" ? a.split(/\s+/) : a.split("");
|
|
107
|
+
const tokensB = mode === "line" ? b.split("\n") : mode === "word" ? b.split(/\s+/) : b.split("");
|
|
108
|
+
// LCS-based diff (simple O(n*m) for small inputs)
|
|
109
|
+
const n = tokensA.length, m = tokensB.length;
|
|
110
|
+
// For large inputs, fall back to simple diff
|
|
111
|
+
if (n * m > 100000) {
|
|
112
|
+
return { a_length: n, b_length: m, note: "Inputs too large for local diff. Use smaller inputs." };
|
|
113
|
+
}
|
|
114
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
115
|
+
for (let i = 1; i <= n; i++) {
|
|
116
|
+
for (let j = 1; j <= m; j++) {
|
|
117
|
+
lcs[i][j] = tokensA[i - 1] === tokensB[j - 1] ? lcs[i - 1][j - 1] + 1 : Math.max(lcs[i - 1][j], lcs[i][j - 1]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const changes = [];
|
|
121
|
+
let i = n, j = m;
|
|
122
|
+
while (i > 0 || j > 0) {
|
|
123
|
+
if (i > 0 && j > 0 && tokensA[i - 1] === tokensB[j - 1]) {
|
|
124
|
+
changes.unshift({ type: "unchanged", value: tokensA[i - 1] });
|
|
125
|
+
i--;
|
|
126
|
+
j--;
|
|
127
|
+
}
|
|
128
|
+
else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
129
|
+
changes.unshift({ type: "added", value: tokensB[j - 1] });
|
|
130
|
+
j--;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
changes.unshift({ type: "removed", value: tokensA[i - 1] });
|
|
134
|
+
i--;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const sep = mode === "line" ? "\n" : mode === "word" ? " " : "";
|
|
138
|
+
if (mode === "patch") {
|
|
139
|
+
const lines = [];
|
|
140
|
+
for (const c of changes) {
|
|
141
|
+
const prefix = c.type === "added" ? "+ " : c.type === "removed" ? "- " : " ";
|
|
142
|
+
lines.push(prefix + c.value);
|
|
143
|
+
}
|
|
144
|
+
return { patch: lines.join("\n"), added: changes.filter((c) => c.type === "added").length, removed: changes.filter((c) => c.type === "removed").length };
|
|
145
|
+
}
|
|
146
|
+
const added = changes.filter((c) => c.type === "added").map((c) => c.value).join(sep);
|
|
147
|
+
const removed = changes.filter((c) => c.type === "removed").map((c) => c.value).join(sep);
|
|
148
|
+
return {
|
|
149
|
+
identical: a === b,
|
|
150
|
+
added_count: changes.filter((c) => c.type === "added").length,
|
|
151
|
+
removed_count: changes.filter((c) => c.type === "removed").length,
|
|
152
|
+
unchanged_count: changes.filter((c) => c.type === "unchanged").length,
|
|
153
|
+
added,
|
|
154
|
+
removed,
|
|
155
|
+
changes,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// ─── Cron helpers ─────────────────────────────────────────────────────────────
|
|
159
|
+
const CRON_FIELDS = ["minute", "hour", "day_of_month", "month", "day_of_week"];
|
|
160
|
+
const DOW_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
161
|
+
const MONTH_NAMES = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"];
|
|
162
|
+
function parseCronField(field, min, max) {
|
|
163
|
+
if (field === "*")
|
|
164
|
+
return null; // means "any"
|
|
165
|
+
const values = [];
|
|
166
|
+
for (const part of field.split(",")) {
|
|
167
|
+
if (part.includes("/")) {
|
|
168
|
+
const [range, step] = part.split("/");
|
|
169
|
+
const s = parseInt(step);
|
|
170
|
+
const [rMin, rMax] = range === "*" ? [min, max] : range.split("-").map(Number);
|
|
171
|
+
for (let v = rMin; v <= rMax; v += s)
|
|
172
|
+
values.push(v);
|
|
173
|
+
}
|
|
174
|
+
else if (part.includes("-")) {
|
|
175
|
+
const [lo, hi] = part.split("-").map(Number);
|
|
176
|
+
for (let v = lo; v <= hi; v++)
|
|
177
|
+
values.push(v);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
values.push(parseInt(part));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return values.every((v) => !isNaN(v) && v >= min && v <= max) ? values : null;
|
|
184
|
+
}
|
|
185
|
+
function parseCronExpression(expr) {
|
|
186
|
+
const parts = expr.trim().split(/\s+/);
|
|
187
|
+
if (parts.length !== 5)
|
|
188
|
+
return { error: "Cron expression must have 5 fields: minute hour day_of_month month day_of_week" };
|
|
189
|
+
const [min, hr, dom, mon, dow] = parts;
|
|
190
|
+
const desc = [];
|
|
191
|
+
if (min === "*" && hr === "*")
|
|
192
|
+
desc.push("every minute");
|
|
193
|
+
else if (min !== "*" && hr === "*")
|
|
194
|
+
desc.push(`at minute ${min} of every hour`);
|
|
195
|
+
else if (min === "0" && hr !== "*")
|
|
196
|
+
desc.push(`at ${hr.padStart(2, "0")}:00`);
|
|
197
|
+
else
|
|
198
|
+
desc.push(`at minute ${min} of hour ${hr}`);
|
|
199
|
+
if (dom !== "*")
|
|
200
|
+
desc.push(`on day ${dom} of the month`);
|
|
201
|
+
if (mon !== "*")
|
|
202
|
+
desc.push(`in month ${mon}`);
|
|
203
|
+
if (dow !== "*") {
|
|
204
|
+
const dayName = DOW_NAMES[parseInt(dow)] ?? dow;
|
|
205
|
+
desc.push(`on ${dayName}`);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
expression: expr,
|
|
209
|
+
description: desc.join(", "),
|
|
210
|
+
fields: { minute: min, hour: hr, day_of_month: dom, month: mon, day_of_week: dow },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function validateCronExpression(expr) {
|
|
214
|
+
const parts = expr.trim().split(/\s+/);
|
|
215
|
+
if (parts.length !== 5)
|
|
216
|
+
return { valid: false, error: "Must have exactly 5 fields" };
|
|
217
|
+
const limits = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]];
|
|
218
|
+
const errors = [];
|
|
219
|
+
parts.forEach((p, i) => {
|
|
220
|
+
if (p === "*")
|
|
221
|
+
return;
|
|
222
|
+
const result = parseCronField(p, limits[i][0], limits[i][1]);
|
|
223
|
+
if (!result)
|
|
224
|
+
errors.push(`Invalid ${CRON_FIELDS[i]} field: "${p}"`);
|
|
225
|
+
});
|
|
226
|
+
return { expression: expr, valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined, fields: Object.fromEntries(CRON_FIELDS.map((f, i) => [f, parts[i]])) };
|
|
227
|
+
}
|
|
228
|
+
function getCronNextOccurrences(expr, count, after) {
|
|
229
|
+
const valid = validateCronExpression(expr);
|
|
230
|
+
if (!valid.valid)
|
|
231
|
+
return { error: valid.error ?? "Invalid cron expression" };
|
|
232
|
+
const parts = expr.trim().split(/\s+/);
|
|
233
|
+
const [minField, hrField, domField, monField, dowField] = parts;
|
|
234
|
+
const matches = (field, val, min, max) => {
|
|
235
|
+
if (field === "*")
|
|
236
|
+
return true;
|
|
237
|
+
const vals = parseCronField(field, min, max);
|
|
238
|
+
return vals ? vals.includes(val) : false;
|
|
239
|
+
};
|
|
240
|
+
const dates = [];
|
|
241
|
+
const cursor = new Date(after.getTime() + 60000); // start 1 min after
|
|
242
|
+
cursor.setSeconds(0, 0);
|
|
243
|
+
let attempts = 0;
|
|
244
|
+
while (dates.length < count && attempts < 500000) {
|
|
245
|
+
attempts++;
|
|
246
|
+
const mo = cursor.getMonth() + 1;
|
|
247
|
+
const dom = cursor.getDate();
|
|
248
|
+
const hr = cursor.getHours();
|
|
249
|
+
const mn = cursor.getMinutes();
|
|
250
|
+
const dow = cursor.getDay();
|
|
251
|
+
if (matches(monField, mo, 1, 12) && matches(domField, dom, 1, 31) && matches(dowField, dow, 0, 7) && matches(hrField, hr, 0, 23) && matches(minField, mn, 0, 59)) {
|
|
252
|
+
dates.push(cursor.toISOString());
|
|
253
|
+
}
|
|
254
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
255
|
+
}
|
|
256
|
+
return { expression: expr, count: dates.length, next_occurrences: dates };
|
|
257
|
+
}
|
|
258
|
+
function buildCronExpression(every, at, on) {
|
|
259
|
+
let minute = "0", hour = "0", dom = "*", month = "*", dow = "*";
|
|
260
|
+
const [hh, mm] = at ? at.split(":") : ["0", "0"];
|
|
261
|
+
if (at) {
|
|
262
|
+
hour = hh;
|
|
263
|
+
minute = mm ?? "0";
|
|
264
|
+
}
|
|
265
|
+
const dowIdx = DOW_NAMES.indexOf(every);
|
|
266
|
+
const monIdx = MONTH_NAMES.indexOf(every);
|
|
267
|
+
if (every === "minute") {
|
|
268
|
+
minute = "*";
|
|
269
|
+
hour = "*";
|
|
270
|
+
}
|
|
271
|
+
else if (every === "hour") {
|
|
272
|
+
minute = mm ?? "0";
|
|
273
|
+
hour = "*";
|
|
274
|
+
}
|
|
275
|
+
else if (every === "day") { /* defaults fine */ }
|
|
276
|
+
else if (every === "week") {
|
|
277
|
+
dow = on ? String(DOW_NAMES.indexOf(on.toLowerCase())) : "1";
|
|
278
|
+
}
|
|
279
|
+
else if (every === "month") {
|
|
280
|
+
dom = on ?? "1";
|
|
281
|
+
}
|
|
282
|
+
else if (dowIdx >= 0) {
|
|
283
|
+
dow = String(dowIdx);
|
|
284
|
+
}
|
|
285
|
+
else if (monIdx >= 0) {
|
|
286
|
+
month = String(monIdx + 1);
|
|
287
|
+
}
|
|
288
|
+
else
|
|
289
|
+
return { error: `Unknown 'every' value: "${every}"` };
|
|
290
|
+
const expression = `${minute} ${hour} ${dom} ${month} ${dow}`;
|
|
291
|
+
return { expression, fields: { minute, hour, day_of_month: dom, month, day_of_week: dow } };
|
|
292
|
+
}
|
|
293
|
+
// ─── IP helpers ───────────────────────────────────────────────────────────────
|
|
294
|
+
function parseIpAddress(ip) {
|
|
295
|
+
const parts = ip.trim().split(".");
|
|
296
|
+
if (parts.length !== 4 || !parts.every((p) => /^\d+$/.test(p) && parseInt(p) <= 255)) {
|
|
297
|
+
return { error: `"${ip}" is not a valid IPv4 address` };
|
|
298
|
+
}
|
|
299
|
+
const nums = parts.map(Number);
|
|
300
|
+
const decimal = nums.reduce((acc, n) => acc * 256 + n, 0);
|
|
301
|
+
const binary = nums.map((n) => n.toString(2).padStart(8, "0")).join(".");
|
|
302
|
+
const hex = "0x" + nums.map((n) => n.toString(16).padStart(2, "0")).join("");
|
|
303
|
+
const type = nums[0] === 10 || (nums[0] === 172 && nums[1] >= 16 && nums[1] <= 31) || (nums[0] === 192 && nums[1] === 168) ? "private" :
|
|
304
|
+
nums[0] === 127 ? "loopback" :
|
|
305
|
+
nums[0] >= 224 && nums[0] <= 239 ? "multicast" :
|
|
306
|
+
nums[0] === 0 ? "this_network" :
|
|
307
|
+
"public";
|
|
308
|
+
return { ip, decimal, binary, hex, octet: nums, type, version: 4 };
|
|
309
|
+
}
|
|
310
|
+
function computeSubnet(cidr) {
|
|
311
|
+
const [ip, bits] = cidr.split("/");
|
|
312
|
+
const prefixLen = parseInt(bits);
|
|
313
|
+
if (!ip || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) {
|
|
314
|
+
return { error: `"${cidr}" is not a valid CIDR notation` };
|
|
315
|
+
}
|
|
316
|
+
const parsed = parseIpAddress(ip);
|
|
317
|
+
if (parsed.error)
|
|
318
|
+
return parsed;
|
|
319
|
+
const mask = prefixLen === 0 ? 0 : (~0 << (32 - prefixLen)) >>> 0;
|
|
320
|
+
const network = (parsed.decimal & mask) >>> 0;
|
|
321
|
+
const broadcast = (network | (~mask >>> 0)) >>> 0;
|
|
322
|
+
const hosts = prefixLen >= 31 ? Math.max(0, Math.pow(2, 32 - prefixLen) - 2) : Math.pow(2, 32 - prefixLen) - 2;
|
|
323
|
+
const toIp = (n) => [(n >> 24) & 255, (n >> 16) & 255, (n >> 8) & 255, n & 255].join(".");
|
|
324
|
+
return {
|
|
325
|
+
cidr,
|
|
326
|
+
prefix_length: prefixLen,
|
|
327
|
+
subnet_mask: toIp(mask),
|
|
328
|
+
network_address: toIp(network),
|
|
329
|
+
broadcast_address: toIp(broadcast),
|
|
330
|
+
first_host: toIp(network + 1),
|
|
331
|
+
last_host: toIp(broadcast - 1),
|
|
332
|
+
total_hosts: hosts,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function checkIpRange(args) {
|
|
336
|
+
const ip = String(args.ip ?? "");
|
|
337
|
+
const cidr = String(args.cidr ?? "");
|
|
338
|
+
const parsed = parseIpAddress(ip);
|
|
339
|
+
if (parsed.error)
|
|
340
|
+
return parsed;
|
|
341
|
+
const [netIp, bits] = cidr.split("/");
|
|
342
|
+
const prefixLen = parseInt(bits);
|
|
343
|
+
const netParsed = parseIpAddress(netIp);
|
|
344
|
+
if (netParsed.error)
|
|
345
|
+
return { error: `Invalid CIDR: ${cidr}` };
|
|
346
|
+
const mask = prefixLen === 0 ? 0 : (~0 << (32 - prefixLen)) >>> 0;
|
|
347
|
+
const inRange = (parsed.decimal & mask) >>> 0 === (netParsed.decimal & mask) >>> 0;
|
|
348
|
+
return { ip, cidr, in_range: inRange };
|
|
349
|
+
}
|
|
350
|
+
function convertIpFormat(args) {
|
|
351
|
+
const ip = String(args.ip ?? "");
|
|
352
|
+
return parseIpAddress(ip);
|
|
353
|
+
}
|
|
354
|
+
// ─── Duration formatting ──────────────────────────────────────────────────────
|
|
355
|
+
function formatDuration(ms, sign) {
|
|
356
|
+
const abs = Math.abs(ms);
|
|
357
|
+
const parts = [];
|
|
358
|
+
const days = Math.floor(abs / 86400000);
|
|
359
|
+
const hours = Math.floor((abs % 86400000) / 3600000);
|
|
360
|
+
const minutes = Math.floor((abs % 3600000) / 60000);
|
|
361
|
+
const seconds = Math.floor((abs % 60000) / 1000);
|
|
362
|
+
if (days)
|
|
363
|
+
parts.push(`${days}d`);
|
|
364
|
+
if (hours)
|
|
365
|
+
parts.push(`${hours}h`);
|
|
366
|
+
if (minutes)
|
|
367
|
+
parts.push(`${minutes}m`);
|
|
368
|
+
if (seconds || parts.length === 0)
|
|
369
|
+
parts.push(`${seconds}s`);
|
|
370
|
+
return (sign < 0 ? "-" : "") + parts.join(" ");
|
|
371
|
+
}
|
|
372
|
+
// ─── HTML entity map (partial) ────────────────────────────────────────────────
|
|
373
|
+
const HTML_ENTITIES = {
|
|
374
|
+
" ": " ", "©": "©", "®": "®", "™": "™",
|
|
375
|
+
"€": "€", "£": "£", "¥": "¥", "¢": "¢",
|
|
376
|
+
"—": "-", "–": "-", "«": "«", "»": "»",
|
|
377
|
+
};
|
|
378
|
+
// ─── Catalog endpoint handlers ───────────────────────────────────────────────
|
|
379
|
+
export const LOCAL_CATALOG_HANDLERS = {
|
|
380
|
+
// ── TRANSFORM ────────────────────────────────────────────────────────────────
|
|
381
|
+
"transform.case": (args) => {
|
|
382
|
+
const text = String(args.text ?? "");
|
|
383
|
+
const to = String(args.to ?? "").toLowerCase();
|
|
384
|
+
const transforms = {
|
|
385
|
+
upper: (s) => s.toUpperCase(),
|
|
386
|
+
lower: (s) => s.toLowerCase(),
|
|
387
|
+
title: (s) => s.replace(/\w\S*/g, (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()),
|
|
388
|
+
sentence: (s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(),
|
|
389
|
+
camel: (s) => { const ws = s.split(/[\s_\-]+/); return ws[0].toLowerCase() + ws.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(""); },
|
|
390
|
+
snake: (s) => s.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s\-]+/g, "_").replace(/[^a-zA-Z0-9_]/g, "").toLowerCase(),
|
|
391
|
+
kebab: (s) => s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").replace(/[^a-zA-Z0-9\-]/g, "").toLowerCase(),
|
|
392
|
+
pascal: (s) => s.split(/[\s_\-]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(""),
|
|
393
|
+
};
|
|
394
|
+
const fn = transforms[to];
|
|
395
|
+
if (!fn)
|
|
396
|
+
return { error: `Unknown case "${to}". Use: upper, lower, title, sentence, camel, snake, kebab, pascal` };
|
|
397
|
+
return { input: text, to, result: fn(text) };
|
|
398
|
+
},
|
|
399
|
+
"transform.slug": (args) => {
|
|
400
|
+
const text = String(args.text ?? "");
|
|
401
|
+
const slug = text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9\s\-]/g, "").trim().replace(/[\s\-]+/g, "-");
|
|
402
|
+
return { input: text, slug };
|
|
403
|
+
},
|
|
404
|
+
"transform.truncate": (args) => {
|
|
405
|
+
const text = String(args.text ?? "");
|
|
406
|
+
const length = Number(args.length ?? 100);
|
|
407
|
+
const ellipsis = args.ellipsis !== false;
|
|
408
|
+
if (text.length <= length)
|
|
409
|
+
return { input: text, result: text, truncated: false };
|
|
410
|
+
const result = ellipsis ? text.slice(0, length - 3) + "..." : text.slice(0, length);
|
|
411
|
+
return { input: text, length, result, truncated: true, original_length: text.length };
|
|
412
|
+
},
|
|
413
|
+
"transform.count": (args) => {
|
|
414
|
+
const text = String(args.text ?? "");
|
|
415
|
+
const wpm = Number(args.words_per_minute ?? 200);
|
|
416
|
+
const wordCount = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
|
|
417
|
+
const sentenceCount = (text.match(/[.!?]+/g) ?? []).length;
|
|
418
|
+
const paragraphCount = text.split(/\n\s*\n/).filter((p) => p.trim().length > 0).length || (text.trim() ? 1 : 0);
|
|
419
|
+
const readingTimeSec = Math.ceil((wordCount / wpm) * 60);
|
|
420
|
+
return {
|
|
421
|
+
character_count: text.length,
|
|
422
|
+
character_count_no_spaces: text.replace(/\s/g, "").length,
|
|
423
|
+
word_count: wordCount,
|
|
424
|
+
sentence_count: sentenceCount,
|
|
425
|
+
paragraph_count: paragraphCount,
|
|
426
|
+
line_count: text.split("\n").length,
|
|
427
|
+
reading_time_seconds: readingTimeSec,
|
|
428
|
+
reading_time_minutes: Math.ceil(readingTimeSec / 60),
|
|
429
|
+
};
|
|
430
|
+
},
|
|
431
|
+
"transform.strip": (args) => {
|
|
432
|
+
const text = String(args.text ?? "");
|
|
433
|
+
const stripped = text.replace(/<[^>]*>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))).trim();
|
|
434
|
+
return { result: stripped, original_length: text.length, result_length: stripped.length };
|
|
435
|
+
},
|
|
436
|
+
"transform.reverse": (args) => {
|
|
437
|
+
const text = String(args.text ?? "");
|
|
438
|
+
const result = [...text].reverse().join("");
|
|
439
|
+
return { input: text, result };
|
|
440
|
+
},
|
|
441
|
+
// ── ENCODE / DECODE ───────────────────────────────────────────────────────────
|
|
442
|
+
"encode.base64": (args) => ({ input: String(args.text ?? ""), encoded: Buffer.from(String(args.text ?? ""), "utf8").toString("base64") }),
|
|
443
|
+
"decode.base64": (args) => {
|
|
444
|
+
try {
|
|
445
|
+
return { input: String(args.text ?? ""), decoded: Buffer.from(String(args.text ?? ""), "base64").toString("utf8") };
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return { error: "Invalid base64 string" };
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
"encode.url": (args) => ({ input: String(args.text ?? ""), encoded: encodeURIComponent(String(args.text ?? "")) }),
|
|
452
|
+
"decode.url": (args) => {
|
|
453
|
+
try {
|
|
454
|
+
return { input: String(args.text ?? ""), decoded: decodeURIComponent(String(args.text ?? "")) };
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return { error: "Invalid URL-encoded string" };
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
"encode.html": (args) => {
|
|
461
|
+
const text = String(args.text ?? "");
|
|
462
|
+
return { input: text, encoded: text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'") };
|
|
463
|
+
},
|
|
464
|
+
"decode.html": (args) => {
|
|
465
|
+
const text = String(args.text ?? "");
|
|
466
|
+
const decoded = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))).replace(/&[a-zA-Z]+;/g, (m) => HTML_ENTITIES[m] ?? m);
|
|
467
|
+
return { input: text, decoded };
|
|
468
|
+
},
|
|
469
|
+
"encode.hex": (args) => ({ input: String(args.text ?? ""), encoded: Buffer.from(String(args.text ?? ""), "utf8").toString("hex") }),
|
|
470
|
+
"decode.hex": (args) => {
|
|
471
|
+
const text = String(args.text ?? "").replace(/^0x/, "");
|
|
472
|
+
try {
|
|
473
|
+
return { input: args.text, decoded: Buffer.from(text, "hex").toString("utf8") };
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
return { error: "Invalid hex string" };
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
// ── HASH ─────────────────────────────────────────────────────────────────────
|
|
480
|
+
"hash.compute": (args) => {
|
|
481
|
+
const text = String(args.text ?? "");
|
|
482
|
+
const algorithm = String(args.algorithm ?? "sha256").toLowerCase();
|
|
483
|
+
const encoding = (String(args.encoding ?? "hex"));
|
|
484
|
+
try {
|
|
485
|
+
return { algorithm, encoding, input_length: text.length, hash: crypto.createHash(algorithm).update(text, "utf8").digest(encoding) };
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return { error: `Unsupported algorithm "${algorithm}". Try: md5, sha1, sha256, sha512` };
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
"hash.verify": (args) => {
|
|
492
|
+
const text = String(args.text ?? "");
|
|
493
|
+
const hash = String(args.hash ?? "");
|
|
494
|
+
const algorithm = String(args.algorithm ?? "sha256").toLowerCase();
|
|
495
|
+
const encoding = (String(args.encoding ?? "hex"));
|
|
496
|
+
try {
|
|
497
|
+
const computed = crypto.createHash(algorithm).update(text, "utf8").digest(encoding);
|
|
498
|
+
return { algorithm, matches: computed === hash, computed };
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return { error: `Unsupported algorithm "${algorithm}"` };
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
"hash.hmac": (args) => {
|
|
505
|
+
const text = String(args.text ?? "");
|
|
506
|
+
const key = String(args.key ?? "");
|
|
507
|
+
const algorithm = String(args.algorithm ?? "sha256").toLowerCase();
|
|
508
|
+
const encoding = (String(args.encoding ?? "hex"));
|
|
509
|
+
try {
|
|
510
|
+
return { algorithm, encoding, hmac: crypto.createHmac(algorithm, key).update(text, "utf8").digest(encoding) };
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return { error: `Unsupported algorithm "${algorithm}"` };
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
// ── REGEX ─────────────────────────────────────────────────────────────────────
|
|
517
|
+
"regex.test": (args) => {
|
|
518
|
+
const pattern = String(args.pattern ?? "");
|
|
519
|
+
const flags = String(args.flags ?? "g");
|
|
520
|
+
const input = String(args.input ?? "");
|
|
521
|
+
try {
|
|
522
|
+
const re = new RegExp(pattern, flags.includes("g") ? flags : flags + "g");
|
|
523
|
+
const matches = [];
|
|
524
|
+
let m;
|
|
525
|
+
while ((m = re.exec(input)) !== null) {
|
|
526
|
+
matches.push({ match: m[0], index: m.index, groups: m.groups });
|
|
527
|
+
if (!re.global)
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
return { pattern, flags, input, matches, count: matches.length, matched: matches.length > 0 };
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
return { error: `Invalid regex: ${String(e)}` };
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
"regex.replace": (args) => {
|
|
537
|
+
const pattern = String(args.pattern ?? "");
|
|
538
|
+
const flags = String(args.flags ?? "g");
|
|
539
|
+
const input = String(args.input ?? "");
|
|
540
|
+
const replacement = String(args.replacement ?? "");
|
|
541
|
+
try {
|
|
542
|
+
return { result: input.replace(new RegExp(pattern, flags), replacement) };
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
return { error: `Invalid regex: ${String(e)}` };
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
"regex.extract": (args) => {
|
|
549
|
+
const pattern = String(args.pattern ?? "");
|
|
550
|
+
const flags = String(args.flags ?? "g");
|
|
551
|
+
const input = String(args.input ?? "");
|
|
552
|
+
const group = args.group != null ? Number(args.group) : 0;
|
|
553
|
+
try {
|
|
554
|
+
const re = new RegExp(pattern, flags.includes("g") ? flags : flags + "g");
|
|
555
|
+
const extractions = [];
|
|
556
|
+
let m;
|
|
557
|
+
while ((m = re.exec(input)) !== null) {
|
|
558
|
+
extractions.push(m[group] ?? m[0]);
|
|
559
|
+
}
|
|
560
|
+
return { extractions, count: extractions.length };
|
|
561
|
+
}
|
|
562
|
+
catch (e) {
|
|
563
|
+
return { error: `Invalid regex: ${String(e)}` };
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
"regex.split": (args) => {
|
|
567
|
+
const pattern = String(args.pattern ?? "");
|
|
568
|
+
const flags = String(args.flags ?? "");
|
|
569
|
+
const input = String(args.input ?? "");
|
|
570
|
+
try {
|
|
571
|
+
const parts = input.split(new RegExp(pattern, flags));
|
|
572
|
+
return { parts, count: parts.length };
|
|
573
|
+
}
|
|
574
|
+
catch (e) {
|
|
575
|
+
return { error: `Invalid regex: ${String(e)}` };
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
"regex.validate": (args) => {
|
|
579
|
+
const pattern = String(args.pattern ?? "");
|
|
580
|
+
const flags = String(args.flags ?? "");
|
|
581
|
+
try {
|
|
582
|
+
new RegExp(pattern, flags);
|
|
583
|
+
return { pattern, flags, valid: true };
|
|
584
|
+
}
|
|
585
|
+
catch (e) {
|
|
586
|
+
return { pattern, flags, valid: false, error: String(e) };
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
// ── MARKDOWN ──────────────────────────────────────────────────────────────────
|
|
590
|
+
"markdown.to-html": async (args) => {
|
|
591
|
+
const md = String(args.markdown ?? "");
|
|
592
|
+
const html = await marked(md);
|
|
593
|
+
return { html, input_length: md.length };
|
|
594
|
+
},
|
|
595
|
+
"markdown.to-text": async (args) => {
|
|
596
|
+
const md = String(args.markdown ?? "");
|
|
597
|
+
const html = await marked(md);
|
|
598
|
+
const text = html.replace(/<[^>]+>/g, "").replace(/\n\n+/g, "\n\n").trim();
|
|
599
|
+
return { text, input_length: md.length };
|
|
600
|
+
},
|
|
601
|
+
"markdown.toc": (args) => {
|
|
602
|
+
const md = String(args.markdown ?? "");
|
|
603
|
+
const headings = [];
|
|
604
|
+
for (const line of md.split("\n")) {
|
|
605
|
+
const m = line.match(/^(#{1,6})\s+(.+)/);
|
|
606
|
+
if (m) {
|
|
607
|
+
const text = m[2].trim();
|
|
608
|
+
const anchor = text.toLowerCase().replace(/[^a-z0-9\s\-]/g, "").replace(/\s+/g, "-");
|
|
609
|
+
headings.push({ level: m[1].length, text, anchor });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const toc = headings.map((h) => `${" ".repeat(h.level - 1)}- [${h.text}](#${h.anchor})`).join("\n");
|
|
613
|
+
return { toc, headings, heading_count: headings.length };
|
|
614
|
+
},
|
|
615
|
+
"markdown.lint": (args) => {
|
|
616
|
+
const md = String(args.markdown ?? "");
|
|
617
|
+
const issues = [];
|
|
618
|
+
md.split("\n").forEach((line, i) => {
|
|
619
|
+
if (line.endsWith(" ") || line.endsWith("\t"))
|
|
620
|
+
issues.push({ line: i + 1, type: "trailing-whitespace", message: "Line has trailing whitespace" });
|
|
621
|
+
if (/^#{1,6}[^#\s]/.test(line))
|
|
622
|
+
issues.push({ line: i + 1, type: "heading-space", message: "Heading must have a space after #" });
|
|
623
|
+
});
|
|
624
|
+
return { issues, issue_count: issues.length, valid: issues.length === 0 };
|
|
625
|
+
},
|
|
626
|
+
// ── DIFF ─────────────────────────────────────────────────────────────────────
|
|
627
|
+
"diff.text": (args) => computeDiff(String(args.a ?? ""), String(args.b ?? ""), "char"),
|
|
628
|
+
"diff.lines": (args) => computeDiff(String(args.a ?? ""), String(args.b ?? ""), "line"),
|
|
629
|
+
"diff.words": (args) => computeDiff(String(args.a ?? ""), String(args.b ?? ""), "word"),
|
|
630
|
+
"diff.patch": (args) => computeDiff(String(args.a ?? ""), String(args.b ?? ""), "patch"),
|
|
631
|
+
// ── JSON ─────────────────────────────────────────────────────────────────────
|
|
632
|
+
"json.format": (args) => {
|
|
633
|
+
const indent = args.indent === "tab" ? "\t" : Number(args.indent ?? 2);
|
|
634
|
+
try {
|
|
635
|
+
const parsed = JSON.parse(String(args.json ?? ""));
|
|
636
|
+
const result = JSON.stringify(parsed, null, indent);
|
|
637
|
+
return { result, bytes: result.length };
|
|
638
|
+
}
|
|
639
|
+
catch (e) {
|
|
640
|
+
return { error: `Invalid JSON: ${String(e)}` };
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
"json.minify": (args) => {
|
|
644
|
+
const input = String(args.json ?? "");
|
|
645
|
+
try {
|
|
646
|
+
const minified = JSON.stringify(JSON.parse(input));
|
|
647
|
+
return { result: minified, original_bytes: input.length, minified_bytes: minified.length, saved_bytes: input.length - minified.length };
|
|
648
|
+
}
|
|
649
|
+
catch (e) {
|
|
650
|
+
return { error: `Invalid JSON: ${String(e)}` };
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
"json.query": (args) => {
|
|
654
|
+
const query = String(args.query ?? "");
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse(String(args.json ?? ""));
|
|
657
|
+
const result = query.split(".").reduce((obj, key) => {
|
|
658
|
+
if (obj == null)
|
|
659
|
+
return undefined;
|
|
660
|
+
return obj[key];
|
|
661
|
+
}, parsed);
|
|
662
|
+
return { query, result, found: result !== undefined };
|
|
663
|
+
}
|
|
664
|
+
catch (e) {
|
|
665
|
+
return { error: String(e) };
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
"json.flatten": (args) => {
|
|
669
|
+
const sep = String(args.separator ?? ".");
|
|
670
|
+
try {
|
|
671
|
+
const parsed = JSON.parse(String(args.json ?? ""));
|
|
672
|
+
const flat = {};
|
|
673
|
+
function flatten(obj, prefix) {
|
|
674
|
+
if (Array.isArray(obj)) {
|
|
675
|
+
obj.forEach((v, i) => flatten(v, prefix ? `${prefix}${sep}${i}` : String(i)));
|
|
676
|
+
}
|
|
677
|
+
else if (typeof obj === "object" && obj !== null) {
|
|
678
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
679
|
+
flatten(v, prefix ? `${prefix}${sep}${k}` : k);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
flat[prefix] = obj;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
flatten(parsed, "");
|
|
687
|
+
return { result: flat, key_count: Object.keys(flat).length };
|
|
688
|
+
}
|
|
689
|
+
catch (e) {
|
|
690
|
+
return { error: `Invalid JSON: ${String(e)}` };
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
"json.unflatten": (args) => {
|
|
694
|
+
const sep = String(args.separator ?? ".");
|
|
695
|
+
try {
|
|
696
|
+
const parsed = JSON.parse(String(args.json ?? ""));
|
|
697
|
+
const result = {};
|
|
698
|
+
for (const [flatKey, value] of Object.entries(parsed)) {
|
|
699
|
+
const keys = flatKey.split(sep);
|
|
700
|
+
let cur = result;
|
|
701
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
702
|
+
if (cur[keys[i]] == null)
|
|
703
|
+
cur[keys[i]] = {};
|
|
704
|
+
cur = cur[keys[i]];
|
|
705
|
+
}
|
|
706
|
+
cur[keys[keys.length - 1]] = value;
|
|
707
|
+
}
|
|
708
|
+
return { result };
|
|
709
|
+
}
|
|
710
|
+
catch (e) {
|
|
711
|
+
return { error: `Invalid JSON: ${String(e)}` };
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
"json.diff": (args) => {
|
|
715
|
+
try {
|
|
716
|
+
const a = JSON.parse(String(args.a ?? ""));
|
|
717
|
+
const b = JSON.parse(String(args.b ?? ""));
|
|
718
|
+
const diffs = [];
|
|
719
|
+
function diff(objA, objB, path) {
|
|
720
|
+
if (JSON.stringify(objA) === JSON.stringify(objB))
|
|
721
|
+
return;
|
|
722
|
+
if (typeof objA !== "object" || typeof objB !== "object" || objA === null || objB === null) {
|
|
723
|
+
diffs.push({ path: path || ".", type: "changed", a: objA, b: objB });
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const keysA = Object.keys(objA), keysB = Object.keys(objB);
|
|
727
|
+
for (const k of new Set([...keysA, ...keysB])) {
|
|
728
|
+
const p = path ? `${path}.${k}` : k;
|
|
729
|
+
if (!(k in objA))
|
|
730
|
+
diffs.push({ path: p, type: "added", b: objB[k] });
|
|
731
|
+
else if (!(k in objB))
|
|
732
|
+
diffs.push({ path: p, type: "removed", a: objA[k] });
|
|
733
|
+
else
|
|
734
|
+
diff(objA[k], objB[k], p);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
diff(a, b, "");
|
|
738
|
+
return { changes: diffs.length, identical: diffs.length === 0, diffs };
|
|
739
|
+
}
|
|
740
|
+
catch (e) {
|
|
741
|
+
return { error: `Invalid JSON: ${String(e)}` };
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
"json.merge": (args) => {
|
|
745
|
+
const deep = args.deep !== false;
|
|
746
|
+
try {
|
|
747
|
+
const a = JSON.parse(String(args.a ?? ""));
|
|
748
|
+
const b = JSON.parse(String(args.b ?? ""));
|
|
749
|
+
function merge(target, source) {
|
|
750
|
+
if (!deep || typeof target !== "object" || target === null || typeof source !== "object" || source === null)
|
|
751
|
+
return source;
|
|
752
|
+
if (Array.isArray(target) || Array.isArray(source))
|
|
753
|
+
return source;
|
|
754
|
+
const result = { ...target };
|
|
755
|
+
for (const [k, v] of Object.entries(source)) {
|
|
756
|
+
result[k] = merge(result[k], v);
|
|
757
|
+
}
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
return { result: merge(a, b) };
|
|
761
|
+
}
|
|
762
|
+
catch (e) {
|
|
763
|
+
return { error: `Invalid JSON: ${String(e)}` };
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
"json.schema": (args) => {
|
|
767
|
+
try {
|
|
768
|
+
const parsed = JSON.parse(String(args.json ?? ""));
|
|
769
|
+
function infer(v) {
|
|
770
|
+
if (v === null)
|
|
771
|
+
return { type: "null" };
|
|
772
|
+
if (Array.isArray(v))
|
|
773
|
+
return { type: "array", items: v.length > 0 ? infer(v[0]) : {} };
|
|
774
|
+
if (typeof v === "object") {
|
|
775
|
+
const props = {};
|
|
776
|
+
for (const [k, val] of Object.entries(v))
|
|
777
|
+
props[k] = infer(val);
|
|
778
|
+
return { type: "object", properties: props };
|
|
779
|
+
}
|
|
780
|
+
return { type: typeof v };
|
|
781
|
+
}
|
|
782
|
+
return { schema: { "$schema": "http://json-schema.org/draft-07/schema#", ...infer(parsed) } };
|
|
783
|
+
}
|
|
784
|
+
catch (e) {
|
|
785
|
+
return { error: `Invalid JSON: ${String(e)}` };
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
// ── CSV ───────────────────────────────────────────────────────────────────────
|
|
789
|
+
"csv.parse": (args) => {
|
|
790
|
+
const header = args.header !== false;
|
|
791
|
+
const delimiter = String(args.delimiter ?? ",");
|
|
792
|
+
try {
|
|
793
|
+
const records = csvParse(String(args.csv ?? ""), { delimiter, columns: header, skip_empty_lines: true });
|
|
794
|
+
return { rows: records, row_count: records.length, columns: header && records.length > 0 ? Object.keys(records[0]) : [] };
|
|
795
|
+
}
|
|
796
|
+
catch (e) {
|
|
797
|
+
return { error: `CSV parse error: ${String(e)}` };
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
"csv.generate": (args) => {
|
|
801
|
+
const rows = args.rows;
|
|
802
|
+
if (!Array.isArray(rows))
|
|
803
|
+
return { error: "rows must be an array" };
|
|
804
|
+
const delimiter = String(args.delimiter ?? ",");
|
|
805
|
+
try {
|
|
806
|
+
const csv = csvStringify(rows, { header: args.header !== false, delimiter });
|
|
807
|
+
return { csv, row_count: rows.length };
|
|
808
|
+
}
|
|
809
|
+
catch (e) {
|
|
810
|
+
return { error: `CSV generate error: ${String(e)}` };
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
"csv.query": (args) => {
|
|
814
|
+
const delimiter = String(args.delimiter ?? ",");
|
|
815
|
+
const filter = args.filter;
|
|
816
|
+
const select = args.select;
|
|
817
|
+
try {
|
|
818
|
+
let records = csvParse(String(args.csv ?? ""), { delimiter, columns: true, skip_empty_lines: true });
|
|
819
|
+
if (filter)
|
|
820
|
+
records = records.filter((row) => Object.entries(filter).every(([k, v]) => String(row[k]) === String(v)));
|
|
821
|
+
if (select?.length)
|
|
822
|
+
records = records.map((row) => Object.fromEntries(select.map((c) => [c, row[c]])));
|
|
823
|
+
return { rows: records, row_count: records.length };
|
|
824
|
+
}
|
|
825
|
+
catch (e) {
|
|
826
|
+
return { error: `CSV query error: ${String(e)}` };
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
"csv.sort": (args) => {
|
|
830
|
+
const delimiter = String(args.delimiter ?? ",");
|
|
831
|
+
const column = String(args.column ?? "");
|
|
832
|
+
const desc = String(args.direction ?? "asc").toLowerCase() === "desc";
|
|
833
|
+
try {
|
|
834
|
+
const records = csvParse(String(args.csv ?? ""), { delimiter, columns: true, skip_empty_lines: true });
|
|
835
|
+
records.sort((a, b) => {
|
|
836
|
+
const av = a[column], bv = b[column];
|
|
837
|
+
const an = Number(av), bn = Number(bv);
|
|
838
|
+
const cmp = !isNaN(an) && !isNaN(bn) ? an - bn : String(av).localeCompare(String(bv));
|
|
839
|
+
return desc ? -cmp : cmp;
|
|
840
|
+
});
|
|
841
|
+
return { csv: csvStringify(records, { header: true, delimiter }), row_count: records.length };
|
|
842
|
+
}
|
|
843
|
+
catch (e) {
|
|
844
|
+
return { error: `CSV sort error: ${String(e)}` };
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
"csv.columns": (args) => {
|
|
848
|
+
try {
|
|
849
|
+
const records = csvParse(String(args.csv ?? ""), { delimiter: String(args.delimiter ?? ","), columns: true, skip_empty_lines: true });
|
|
850
|
+
const columns = records.length > 0 ? Object.keys(records[0]) : [];
|
|
851
|
+
return { columns, column_count: columns.length, row_count: records.length };
|
|
852
|
+
}
|
|
853
|
+
catch (e) {
|
|
854
|
+
return { error: String(e) };
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
"csv.stats": (args) => {
|
|
858
|
+
const column = String(args.column ?? "");
|
|
859
|
+
try {
|
|
860
|
+
const records = csvParse(String(args.csv ?? ""), { delimiter: String(args.delimiter ?? ","), columns: true, skip_empty_lines: true });
|
|
861
|
+
const values = records.map((r) => Number(r[column])).filter((v) => !isNaN(v));
|
|
862
|
+
if (!values.length)
|
|
863
|
+
return { error: `Column "${column}" has no numeric values` };
|
|
864
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
865
|
+
const sum = values.reduce((a, b) => a + b, 0);
|
|
866
|
+
const mean = sum / values.length;
|
|
867
|
+
const variance = values.reduce((acc, v) => acc + (v - mean) ** 2, 0) / values.length;
|
|
868
|
+
const mid = Math.floor(sorted.length / 2);
|
|
869
|
+
const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
870
|
+
return { column, count: values.length, sum, mean: Math.round(mean * 10000) / 10000, median, min: sorted[0], max: sorted[sorted.length - 1], stddev: Math.round(Math.sqrt(variance) * 10000) / 10000 };
|
|
871
|
+
}
|
|
872
|
+
catch (e) {
|
|
873
|
+
return { error: String(e) };
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
// ── VALIDATE ─────────────────────────────────────────────────────────────────
|
|
877
|
+
"validate.email": (args) => {
|
|
878
|
+
const email = String(args.email ?? "");
|
|
879
|
+
const valid = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/.test(email);
|
|
880
|
+
return { email, valid };
|
|
881
|
+
},
|
|
882
|
+
"validate.url": (args) => {
|
|
883
|
+
const url = String(args.url ?? "");
|
|
884
|
+
try {
|
|
885
|
+
const p = new URL(url);
|
|
886
|
+
return { url, valid: true, protocol: p.protocol, hostname: p.hostname, path: p.pathname };
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
return { url, valid: false };
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
"validate.phone": (args) => {
|
|
893
|
+
const phone = String(args.phone ?? "");
|
|
894
|
+
const digits = phone.replace(/[\s()\-+.]/g, "");
|
|
895
|
+
const valid = /^\+?[1-9]\d{6,14}$/.test(phone.replace(/[\s()\-.]/g, ""));
|
|
896
|
+
return { phone, valid, digits_only: digits };
|
|
897
|
+
},
|
|
898
|
+
"validate.json": (args) => {
|
|
899
|
+
const json = String(args.json ?? "");
|
|
900
|
+
try {
|
|
901
|
+
const parsed = JSON.parse(json);
|
|
902
|
+
return { valid: true, type: Array.isArray(parsed) ? "array" : typeof parsed };
|
|
903
|
+
}
|
|
904
|
+
catch (e) {
|
|
905
|
+
return { valid: false, error: String(e) };
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
"validate.credit-card": (args) => {
|
|
909
|
+
const number = String(args.number ?? "").replace(/[\s\-]/g, "");
|
|
910
|
+
let sum = 0, alt = false;
|
|
911
|
+
for (let i = number.length - 1; i >= 0; i--) {
|
|
912
|
+
let n = parseInt(number[i]);
|
|
913
|
+
if (isNaN(n))
|
|
914
|
+
return { valid: false, error: "Non-numeric characters" };
|
|
915
|
+
if (alt) {
|
|
916
|
+
n *= 2;
|
|
917
|
+
if (n > 9)
|
|
918
|
+
n -= 9;
|
|
919
|
+
}
|
|
920
|
+
sum += n;
|
|
921
|
+
alt = !alt;
|
|
922
|
+
}
|
|
923
|
+
const valid = sum % 10 === 0 && number.length >= 13;
|
|
924
|
+
const brand = /^4/.test(number) ? "visa" : /^5[1-5]/.test(number) ? "mastercard" : /^3[47]/.test(number) ? "amex" : /^6(?:011|5)/.test(number) ? "discover" : "unknown";
|
|
925
|
+
return { valid, brand, length: number.length };
|
|
926
|
+
},
|
|
927
|
+
"validate.ip": (args) => {
|
|
928
|
+
const ip = String(args.ip ?? "");
|
|
929
|
+
const v4 = /^(\d{1,3}\.){3}\d{1,3}$/.test(ip) && ip.split(".").every((n) => parseInt(n) <= 255);
|
|
930
|
+
const v6 = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/.test(ip);
|
|
931
|
+
return { ip, valid: v4 || v6, version: v4 ? 4 : v6 ? 6 : null };
|
|
932
|
+
},
|
|
933
|
+
"validate.color": (args) => {
|
|
934
|
+
const color = String(args.color ?? "");
|
|
935
|
+
const hexValid = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(color);
|
|
936
|
+
const rgbValid = /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/i.test(color);
|
|
937
|
+
const hslValid = /^hsl\(\s*\d+\s*,\s*\d+%?\s*,\s*\d+%?\s*\)$/i.test(color);
|
|
938
|
+
return { color, valid: hexValid || rgbValid || hslValid, format: hexValid ? "hex" : rgbValid ? "rgb" : hslValid ? "hsl" : null };
|
|
939
|
+
},
|
|
940
|
+
// ── COLOR ─────────────────────────────────────────────────────────────────────
|
|
941
|
+
"color.convert": (args) => {
|
|
942
|
+
const raw = args.color;
|
|
943
|
+
const colorStr = typeof raw === "object" && raw !== null
|
|
944
|
+
? ("r" in raw ? `rgb(${raw.r}, ${raw.g}, ${raw.b})` : `hsl(${raw.h}, ${raw.s}%, ${raw.l}%)`)
|
|
945
|
+
: String(raw ?? "");
|
|
946
|
+
const rgb = parseColorToRgb(colorStr);
|
|
947
|
+
if (!rgb)
|
|
948
|
+
return { error: `Cannot parse color: "${colorStr}"` };
|
|
949
|
+
return colorInfo(rgb);
|
|
950
|
+
},
|
|
951
|
+
"color.palette": (args) => {
|
|
952
|
+
const rgb = parseColorToRgb(String(args.color ?? ""));
|
|
953
|
+
if (!rgb)
|
|
954
|
+
return { error: `Cannot parse color: "${args.color}"` };
|
|
955
|
+
const hsl = rgbToHsl(rgb);
|
|
956
|
+
const type = String(args.type ?? "complementary");
|
|
957
|
+
let palette = [];
|
|
958
|
+
if (type === "complementary")
|
|
959
|
+
palette = [hsl, { h: (hsl.h + 180) % 360, s: hsl.s, l: hsl.l }];
|
|
960
|
+
else if (type === "analogous")
|
|
961
|
+
palette = [{ h: (hsl.h - 30 + 360) % 360, s: hsl.s, l: hsl.l }, hsl, { h: (hsl.h + 30) % 360, s: hsl.s, l: hsl.l }];
|
|
962
|
+
else if (type === "triadic")
|
|
963
|
+
palette = [hsl, { h: (hsl.h + 120) % 360, s: hsl.s, l: hsl.l }, { h: (hsl.h + 240) % 360, s: hsl.s, l: hsl.l }];
|
|
964
|
+
else if (type === "tetradic")
|
|
965
|
+
palette = [hsl, { h: (hsl.h + 90) % 360, s: hsl.s, l: hsl.l }, { h: (hsl.h + 180) % 360, s: hsl.s, l: hsl.l }, { h: (hsl.h + 270) % 360, s: hsl.s, l: hsl.l }];
|
|
966
|
+
else if (type === "monochromatic")
|
|
967
|
+
palette = [{ h: hsl.h, s: hsl.s, l: Math.max(10, hsl.l - 30) }, { h: hsl.h, s: hsl.s, l: Math.max(10, hsl.l - 15) }, hsl, { h: hsl.h, s: hsl.s, l: Math.min(90, hsl.l + 15) }, { h: hsl.h, s: hsl.s, l: Math.min(90, hsl.l + 30) }];
|
|
968
|
+
else
|
|
969
|
+
return { error: `Unknown palette type "${type}"` };
|
|
970
|
+
return { type, colors: palette.map((c) => colorInfo(hslToRgb(c))) };
|
|
971
|
+
},
|
|
972
|
+
"color.mix": (args) => {
|
|
973
|
+
const rgb1 = parseColorToRgb(String(args.color1 ?? ""));
|
|
974
|
+
const rgb2 = parseColorToRgb(String(args.color2 ?? ""));
|
|
975
|
+
if (!rgb1)
|
|
976
|
+
return { error: `Cannot parse color1: "${args.color1}"` };
|
|
977
|
+
if (!rgb2)
|
|
978
|
+
return { error: `Cannot parse color2: "${args.color2}"` };
|
|
979
|
+
const w = Math.max(0, Math.min(1, Number(args.weight ?? 0.5)));
|
|
980
|
+
const mixed = { r: Math.round(rgb1.r * (1 - w) + rgb2.r * w), g: Math.round(rgb1.g * (1 - w) + rgb2.g * w), b: Math.round(rgb1.b * (1 - w) + rgb2.b * w) };
|
|
981
|
+
return { color1: rgbToHex(rgb1), color2: rgbToHex(rgb2), weight: w, result: colorInfo(mixed) };
|
|
982
|
+
},
|
|
983
|
+
"color.contrast": (args) => {
|
|
984
|
+
const rgb1 = parseColorToRgb(String(args.color1 ?? ""));
|
|
985
|
+
const rgb2 = parseColorToRgb(String(args.color2 ?? ""));
|
|
986
|
+
if (!rgb1)
|
|
987
|
+
return { error: `Cannot parse color1` };
|
|
988
|
+
if (!rgb2)
|
|
989
|
+
return { error: `Cannot parse color2` };
|
|
990
|
+
function relative(c) {
|
|
991
|
+
return [c.r, c.g, c.b].map((n) => { const s = n / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); }).reduce((acc, v, i) => acc + [0.2126, 0.7152, 0.0722][i] * v, 0);
|
|
992
|
+
}
|
|
993
|
+
const l1 = relative(rgb1), l2 = relative(rgb2);
|
|
994
|
+
const lighter = Math.max(l1, l2), darker = Math.min(l1, l2);
|
|
995
|
+
const ratio = Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
|
|
996
|
+
return { color1: rgbToHex(rgb1), color2: rgbToHex(rgb2), contrast_ratio: ratio, wcag_aa_normal: ratio >= 4.5, wcag_aa_large: ratio >= 3, wcag_aaa_normal: ratio >= 7, wcag_aaa_large: ratio >= 4.5 };
|
|
997
|
+
},
|
|
998
|
+
"color.lighten": (args) => {
|
|
999
|
+
const rgb = parseColorToRgb(String(args.color ?? ""));
|
|
1000
|
+
if (!rgb)
|
|
1001
|
+
return { error: `Cannot parse color: "${args.color}"` };
|
|
1002
|
+
const amount = Number(args.amount ?? 10);
|
|
1003
|
+
const hsl = rgbToHsl(rgb);
|
|
1004
|
+
const lightened = hslToRgb({ h: hsl.h, s: hsl.s, l: Math.min(100, hsl.l + amount) });
|
|
1005
|
+
return { original: rgbToHex(rgb), amount, result: colorInfo(lightened) };
|
|
1006
|
+
},
|
|
1007
|
+
"color.darken": (args) => {
|
|
1008
|
+
const rgb = parseColorToRgb(String(args.color ?? ""));
|
|
1009
|
+
if (!rgb)
|
|
1010
|
+
return { error: `Cannot parse color: "${args.color}"` };
|
|
1011
|
+
const amount = Number(args.amount ?? 10);
|
|
1012
|
+
const hsl = rgbToHsl(rgb);
|
|
1013
|
+
const darkened = hslToRgb({ h: hsl.h, s: hsl.s, l: Math.max(0, hsl.l - amount) });
|
|
1014
|
+
return { original: rgbToHex(rgb), amount, result: colorInfo(darkened) };
|
|
1015
|
+
},
|
|
1016
|
+
// ── TIMESTAMP ────────────────────────────────────────────────────────────────
|
|
1017
|
+
"timestamp.now": () => {
|
|
1018
|
+
const now = new Date();
|
|
1019
|
+
return { iso: now.toISOString(), unix_seconds: Math.floor(now.getTime() / 1000), unix_ms: now.getTime(), utc: now.toUTCString() };
|
|
1020
|
+
},
|
|
1021
|
+
"timestamp.convert": (args) => {
|
|
1022
|
+
const ts = args.timestamp;
|
|
1023
|
+
if (ts == null)
|
|
1024
|
+
return { error: "timestamp is required" };
|
|
1025
|
+
const d = parseTs(ts);
|
|
1026
|
+
if (isNaN(d.getTime()))
|
|
1027
|
+
return { error: `Cannot parse timestamp: ${ts}` };
|
|
1028
|
+
return { iso: d.toISOString(), unix_seconds: Math.floor(d.getTime() / 1000), unix_ms: d.getTime(), utc: d.toUTCString(), date: d.toISOString().split("T")[0], time: d.toISOString().split("T")[1].split(".")[0] };
|
|
1029
|
+
},
|
|
1030
|
+
"timestamp.diff": (args) => {
|
|
1031
|
+
const from = parseTs(args.from);
|
|
1032
|
+
const to = parseTs(args.to);
|
|
1033
|
+
if (isNaN(from.getTime()) || isNaN(to.getTime()))
|
|
1034
|
+
return { error: "Invalid timestamp" };
|
|
1035
|
+
const ms = to.getTime() - from.getTime();
|
|
1036
|
+
return { from: from.toISOString(), to: to.toISOString(), milliseconds: ms, seconds: Math.floor(ms / 1000), minutes: Math.floor(ms / 60000), hours: Math.floor(ms / 3600000), days: Math.floor(ms / 86400000), human: formatDuration(Math.abs(ms), Math.sign(ms)) };
|
|
1037
|
+
},
|
|
1038
|
+
"timestamp.add": (args) => {
|
|
1039
|
+
const ts = args.timestamp;
|
|
1040
|
+
if (ts == null)
|
|
1041
|
+
return { error: "timestamp is required" };
|
|
1042
|
+
const d = parseTs(ts);
|
|
1043
|
+
if (isNaN(d.getTime()))
|
|
1044
|
+
return { error: `Cannot parse timestamp: ${ts}` };
|
|
1045
|
+
const dur = (args.duration ?? {});
|
|
1046
|
+
let ms = d.getTime() + (dur.seconds ?? 0) * 1000 + (dur.minutes ?? 0) * 60000 + (dur.hours ?? 0) * 3600000 + (dur.days ?? 0) * 86400000 + (dur.weeks ?? 0) * 604800000;
|
|
1047
|
+
const result = new Date(ms);
|
|
1048
|
+
if (dur.months)
|
|
1049
|
+
result.setMonth(result.getMonth() + dur.months);
|
|
1050
|
+
if (dur.years)
|
|
1051
|
+
result.setFullYear(result.getFullYear() + dur.years);
|
|
1052
|
+
return { original: d.toISOString(), result: result.toISOString(), unix_seconds: Math.floor(result.getTime() / 1000) };
|
|
1053
|
+
},
|
|
1054
|
+
"timestamp.format": (args) => {
|
|
1055
|
+
const ts = args.timestamp;
|
|
1056
|
+
if (ts == null)
|
|
1057
|
+
return { error: "timestamp is required" };
|
|
1058
|
+
const d = parseTs(ts);
|
|
1059
|
+
if (isNaN(d.getTime()))
|
|
1060
|
+
return { error: `Cannot parse timestamp: ${ts}` };
|
|
1061
|
+
const fmt = String(args.format ?? "YYYY-MM-DD HH:mm:ss");
|
|
1062
|
+
const pad = (n, len = 2) => String(n).padStart(len, "0");
|
|
1063
|
+
const result = fmt.replace("YYYY", String(d.getFullYear())).replace("MM", pad(d.getMonth() + 1)).replace("DD", pad(d.getDate())).replace("HH", pad(d.getHours())).replace("mm", pad(d.getMinutes())).replace("ss", pad(d.getSeconds())).replace("SSS", pad(d.getMilliseconds(), 3));
|
|
1064
|
+
return { timestamp: d.toISOString(), format: fmt, result };
|
|
1065
|
+
},
|
|
1066
|
+
// ── CRON ─────────────────────────────────────────────────────────────────────
|
|
1067
|
+
"cron.parse": (args) => parseCronExpression(String(args.expression ?? "")),
|
|
1068
|
+
"cron.next": (args) => getCronNextOccurrences(String(args.expression ?? ""), Math.min(50, Math.max(1, Number(args.count ?? 5))), args.after ? new Date(String(args.after)) : new Date()),
|
|
1069
|
+
"cron.validate": (args) => validateCronExpression(String(args.expression ?? "")),
|
|
1070
|
+
"cron.build": (args) => buildCronExpression(String(args.every ?? ""), args.at ? String(args.at) : undefined, args.on ? String(args.on) : undefined),
|
|
1071
|
+
// ── IP ────────────────────────────────────────────────────────────────────────
|
|
1072
|
+
"ip.lookup": () => ({ message: "ip.lookup requires a server context. Use a geolocation API (e.g. ip-api.com) directly.", available: false }),
|
|
1073
|
+
"ip.parse": (args) => parseIpAddress(String(args.ip ?? "")),
|
|
1074
|
+
"ip.subnet": (args) => computeSubnet(String(args.cidr ?? "")),
|
|
1075
|
+
"ip.range": (args) => checkIpRange(args),
|
|
1076
|
+
"ip.convert": (args) => parseIpAddress(String(args.ip ?? "")),
|
|
1077
|
+
// ── UUID ─────────────────────────────────────────────────────────────────────
|
|
1078
|
+
"uuid.v4": () => ({ uuid: crypto.randomUUID(), version: 4 }),
|
|
1079
|
+
"uuid.validate": (args) => {
|
|
1080
|
+
const uuid = String(args.uuid ?? "");
|
|
1081
|
+
const valid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid);
|
|
1082
|
+
return { uuid, valid, version: valid ? parseInt(uuid[14]) : null };
|
|
1083
|
+
},
|
|
1084
|
+
"uuid.parse": (args) => {
|
|
1085
|
+
const uuid = String(args.uuid ?? "");
|
|
1086
|
+
const m = uuid.match(/^([0-9a-f]{8})-([0-9a-f]{4})-([1-5])([0-9a-f]{3})-([89ab][0-9a-f]{3})-([0-9a-f]{12})$/i);
|
|
1087
|
+
if (!m)
|
|
1088
|
+
return { uuid, valid: false, error: "Not a valid UUID" };
|
|
1089
|
+
return { uuid, valid: true, version: parseInt(m[3]), hex: uuid.replace(/-/g, ""), parts: { time_low: m[1], time_mid: m[2], time_hi_and_version: m[3] + m[4], clock_seq: m[5], node: m[6] } };
|
|
1090
|
+
},
|
|
1091
|
+
// ── RANDOM ────────────────────────────────────────────────────────────────────
|
|
1092
|
+
"random.number": (args) => {
|
|
1093
|
+
const min = Number(args.min ?? 0), max = Number(args.max ?? 100);
|
|
1094
|
+
const count = Math.min(1000, Math.max(1, Number(args.count ?? 1)));
|
|
1095
|
+
const decimals = Math.min(10, Math.max(0, Number(args.decimals ?? 0)));
|
|
1096
|
+
if (min > max)
|
|
1097
|
+
return { error: "min must be <= max" };
|
|
1098
|
+
const mult = Math.pow(10, decimals);
|
|
1099
|
+
const numbers = Array.from({ length: count }, () => Math.round((min + secureRandom() * (max - min)) * mult) / mult);
|
|
1100
|
+
return { min, max, count, decimals, numbers, single: count === 1 ? numbers[0] : undefined };
|
|
1101
|
+
},
|
|
1102
|
+
"random.string": (args) => {
|
|
1103
|
+
const length = Math.min(4096, Math.max(1, Number(args.length ?? 16)));
|
|
1104
|
+
const charset = String(args.charset ?? "alphanumeric").toLowerCase();
|
|
1105
|
+
const count = Math.min(100, Math.max(1, Number(args.count ?? 1)));
|
|
1106
|
+
const charsets = { alpha: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", numeric: "0123456789", alphanumeric: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", hex: "0123456789abcdef", custom: String(args.custom_chars ?? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") };
|
|
1107
|
+
const chars = charsets[charset];
|
|
1108
|
+
if (!chars)
|
|
1109
|
+
return { error: `charset "${charset}" not supported` };
|
|
1110
|
+
const strings = Array.from({ length: count }, () => Array.from({ length }, () => chars[Math.floor(secureRandom() * chars.length)]).join(""));
|
|
1111
|
+
return { length, charset, count, strings, single: count === 1 ? strings[0] : undefined };
|
|
1112
|
+
},
|
|
1113
|
+
"random.password": (args) => {
|
|
1114
|
+
const length = Math.min(512, Math.max(4, Number(args.length ?? 16)));
|
|
1115
|
+
const count = Math.min(100, Math.max(1, Number(args.count ?? 1)));
|
|
1116
|
+
let chars = "";
|
|
1117
|
+
if (args.uppercase !== false)
|
|
1118
|
+
chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
1119
|
+
if (args.lowercase !== false)
|
|
1120
|
+
chars += "abcdefghijklmnopqrstuvwxyz";
|
|
1121
|
+
if (args.numbers !== false)
|
|
1122
|
+
chars += "0123456789";
|
|
1123
|
+
if (args.symbols !== false)
|
|
1124
|
+
chars += "!@#$%^&*()-_=+[]{}|;:,.<>?";
|
|
1125
|
+
if (!chars)
|
|
1126
|
+
return { error: "At least one character set must be enabled" };
|
|
1127
|
+
const passwords = Array.from({ length: count }, () => Array.from({ length }, () => chars[Math.floor(secureRandom() * chars.length)]).join(""));
|
|
1128
|
+
return { length, count, passwords, single: count === 1 ? passwords[0] : undefined };
|
|
1129
|
+
},
|
|
1130
|
+
"random.pick": (args) => {
|
|
1131
|
+
const items = args.items;
|
|
1132
|
+
if (!Array.isArray(items))
|
|
1133
|
+
return { error: "items must be an array" };
|
|
1134
|
+
const count = Math.min(items.length, Math.max(1, Number(args.count ?? 1)));
|
|
1135
|
+
const unique = args.unique !== false;
|
|
1136
|
+
const picked = unique
|
|
1137
|
+
? [...items].sort(() => secureRandom() - 0.5).slice(0, count)
|
|
1138
|
+
: Array.from({ length: count }, () => items[Math.floor(secureRandom() * items.length)]);
|
|
1139
|
+
return { total_items: items.length, count, unique, picked, single: count === 1 ? picked[0] : undefined };
|
|
1140
|
+
},
|
|
1141
|
+
"random.shuffle": (args) => {
|
|
1142
|
+
const items = args.items;
|
|
1143
|
+
if (!Array.isArray(items))
|
|
1144
|
+
return { error: "items must be an array" };
|
|
1145
|
+
const shuffled = [...items];
|
|
1146
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
1147
|
+
const j = Math.floor(secureRandom() * (i + 1));
|
|
1148
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
1149
|
+
}
|
|
1150
|
+
return { original_length: items.length, shuffled };
|
|
1151
|
+
},
|
|
1152
|
+
"random.color": (args) => {
|
|
1153
|
+
const format = String(args.format ?? "hex").toLowerCase();
|
|
1154
|
+
const count = Math.min(100, Math.max(1, Number(args.count ?? 1)));
|
|
1155
|
+
const colors = Array.from({ length: count }, () => {
|
|
1156
|
+
const r = Math.floor(secureRandom() * 256), g = Math.floor(secureRandom() * 256), b = Math.floor(secureRandom() * 256);
|
|
1157
|
+
if (format === "rgb")
|
|
1158
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
1159
|
+
if (format === "hsl") {
|
|
1160
|
+
const hsl = rgbToHsl({ r, g, b });
|
|
1161
|
+
return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
|
|
1162
|
+
}
|
|
1163
|
+
return rgbToHex({ r, g, b });
|
|
1164
|
+
});
|
|
1165
|
+
return { format, count, colors, single: count === 1 ? colors[0] : undefined };
|
|
1166
|
+
},
|
|
1167
|
+
// ── REPORT BUG ────────────────────────────────────────────────────────────────
|
|
1168
|
+
"report_bug.create": (args) => {
|
|
1169
|
+
process.stderr.write(`[UnClick BugReport] ${JSON.stringify(args)}\n`);
|
|
1170
|
+
return { submitted: true, message: "Bug report logged. To file publicly, visit https://github.com/malamutemayhem/unclick-agent-native-endpoints/issues" };
|
|
1171
|
+
},
|
|
1172
|
+
// ── KV STORE (in-memory, per session) ─────────────────────────────────────────
|
|
1173
|
+
"kv.set": (args) => {
|
|
1174
|
+
const key = String(args.key ?? "");
|
|
1175
|
+
if (!key)
|
|
1176
|
+
return { error: "key is required" };
|
|
1177
|
+
const entry = KV_STORE.get(key);
|
|
1178
|
+
const version = (entry?.version ?? 0) + 1;
|
|
1179
|
+
const expires = args.ttl ? Date.now() + Number(args.ttl) * 1000 : undefined;
|
|
1180
|
+
KV_STORE.set(key, { value: args.value, expires, version });
|
|
1181
|
+
return { key, set: true, version, expires: expires ? new Date(expires).toISOString() : null };
|
|
1182
|
+
},
|
|
1183
|
+
"kv.get": (args) => {
|
|
1184
|
+
const key = String(args.key ?? "");
|
|
1185
|
+
const entry = KV_STORE.get(key);
|
|
1186
|
+
if (!entry)
|
|
1187
|
+
return { key, found: false, value: null };
|
|
1188
|
+
if (entry.expires && entry.expires < Date.now()) {
|
|
1189
|
+
KV_STORE.delete(key);
|
|
1190
|
+
return { key, found: false, value: null, expired: true };
|
|
1191
|
+
}
|
|
1192
|
+
return { key, found: true, value: entry.value, version: entry.version };
|
|
1193
|
+
},
|
|
1194
|
+
"kv.delete": (args) => {
|
|
1195
|
+
const key = String(args.key ?? "");
|
|
1196
|
+
const existed = KV_STORE.has(key);
|
|
1197
|
+
KV_STORE.delete(key);
|
|
1198
|
+
return { key, deleted: existed };
|
|
1199
|
+
},
|
|
1200
|
+
"kv.list": (args) => {
|
|
1201
|
+
const prefix = args.prefix ? String(args.prefix) : undefined;
|
|
1202
|
+
const now = Date.now();
|
|
1203
|
+
const keys = [];
|
|
1204
|
+
for (const [k, v] of KV_STORE.entries()) {
|
|
1205
|
+
if (v.expires && v.expires < now) {
|
|
1206
|
+
KV_STORE.delete(k);
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
if (!prefix || k.startsWith(prefix))
|
|
1210
|
+
keys.push(k);
|
|
1211
|
+
}
|
|
1212
|
+
return { keys, count: keys.length };
|
|
1213
|
+
},
|
|
1214
|
+
"kv.exists": (args) => {
|
|
1215
|
+
const key = String(args.key ?? "");
|
|
1216
|
+
const entry = KV_STORE.get(key);
|
|
1217
|
+
if (!entry)
|
|
1218
|
+
return { key, exists: false };
|
|
1219
|
+
if (entry.expires && entry.expires < Date.now()) {
|
|
1220
|
+
KV_STORE.delete(key);
|
|
1221
|
+
return { key, exists: false, expired: true };
|
|
1222
|
+
}
|
|
1223
|
+
return { key, exists: true };
|
|
1224
|
+
},
|
|
1225
|
+
"kv.increment": (args) => {
|
|
1226
|
+
const key = String(args.key ?? "");
|
|
1227
|
+
const by = Number(args.by ?? 1);
|
|
1228
|
+
const entry = KV_STORE.get(key);
|
|
1229
|
+
const current = entry?.value != null ? Number(entry.value) : 0;
|
|
1230
|
+
if (isNaN(current))
|
|
1231
|
+
return { error: `Value for "${key}" is not numeric` };
|
|
1232
|
+
const newValue = current + by;
|
|
1233
|
+
KV_STORE.set(key, { value: newValue, expires: entry?.expires, version: (entry?.version ?? 0) + 1 });
|
|
1234
|
+
return { key, previous: current, value: newValue, incremented_by: by };
|
|
1235
|
+
},
|
|
1236
|
+
// ── IMAGE (requires external library, not available locally) ─────────────────
|
|
1237
|
+
"image.resize": () => ({ error: "Image processing requires Sharp or the remote UnClick API. Install sharp and configure UNCLICK_BASE_URL, or use a different image processing tool." }),
|
|
1238
|
+
"image.convert": () => ({ error: "Image processing requires the remote UnClick API." }),
|
|
1239
|
+
"image.compress": () => ({ error: "Image processing requires the remote UnClick API." }),
|
|
1240
|
+
"image.metadata": () => ({ error: "Image processing requires the remote UnClick API." }),
|
|
1241
|
+
"image.crop": () => ({ error: "Image processing requires the remote UnClick API." }),
|
|
1242
|
+
"image.rotate": () => ({ error: "Image processing requires the remote UnClick API." }),
|
|
1243
|
+
"image.grayscale": () => ({ error: "Image processing requires the remote UnClick API." }),
|
|
1244
|
+
// ── QR CODE ───────────────────────────────────────────────────────────────────
|
|
1245
|
+
"qr.generate": () => ({ error: "QR code generation requires the remote UnClick API." }),
|
|
1246
|
+
// ── URL SHORTENER ─────────────────────────────────────────────────────────────
|
|
1247
|
+
"shorten.create": () => ({ error: "URL shortening requires the remote UnClick API." }),
|
|
1248
|
+
"shorten.stats": () => ({ error: "URL shortener stats require the remote UnClick API." }),
|
|
1249
|
+
// ── WEBHOOK ───────────────────────────────────────────────────────────────────
|
|
1250
|
+
"webhook.create": () => ({ error: "Webhooks require the remote UnClick API." }),
|
|
1251
|
+
"webhook.requests": () => ({ error: "Webhooks require the remote UnClick API." }),
|
|
1252
|
+
"webhook.delete": () => ({ error: "Webhooks require the remote UnClick API." }),
|
|
1253
|
+
};
|
|
1254
|
+
//# sourceMappingURL=local-catalog-handlers.js.map
|