@unclick/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/dist/amazon-tool.d.ts +5 -0
- package/dist/amazon-tool.d.ts.map +1 -0
- package/dist/amazon-tool.js +307 -0
- package/dist/amazon-tool.js.map +1 -0
- package/dist/bluesky-tool.d.ts +2 -0
- package/dist/bluesky-tool.d.ts.map +1 -0
- package/dist/bluesky-tool.js +368 -0
- package/dist/bluesky-tool.js.map +1 -0
- package/dist/catalog.d.ts +28 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +1865 -0
- package/dist/catalog.js.map +1 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +73 -0
- package/dist/client.js.map +1 -0
- package/dist/converter-tools.d.ts +50 -0
- package/dist/converter-tools.d.ts.map +1 -0
- package/dist/converter-tools.js +137 -0
- package/dist/converter-tools.js.map +1 -0
- package/dist/csuite-tool.d.ts +35 -0
- package/dist/csuite-tool.d.ts.map +1 -0
- package/dist/csuite-tool.js +791 -0
- package/dist/csuite-tool.js.map +1 -0
- package/dist/discord-tool.d.ts +8 -0
- package/dist/discord-tool.d.ts.map +1 -0
- package/dist/discord-tool.js +195 -0
- package/dist/discord-tool.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/local-tools.d.ts +72 -0
- package/dist/local-tools.d.ts.map +1 -0
- package/dist/local-tools.js +563 -0
- package/dist/local-tools.js.map +1 -0
- package/dist/mastodon-tool.d.ts +2 -0
- package/dist/mastodon-tool.d.ts.map +1 -0
- package/dist/mastodon-tool.js +372 -0
- package/dist/mastodon-tool.js.map +1 -0
- package/dist/reddit-tool.d.ts +59 -0
- package/dist/reddit-tool.d.ts.map +1 -0
- package/dist/reddit-tool.js +348 -0
- package/dist/reddit-tool.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2337 -0
- package/dist/server.js.map +1 -0
- package/dist/shopify-tool.d.ts +8 -0
- package/dist/shopify-tool.d.ts.map +1 -0
- package/dist/shopify-tool.js +305 -0
- package/dist/shopify-tool.js.map +1 -0
- package/dist/slack-tool.d.ts +2 -0
- package/dist/slack-tool.d.ts.map +1 -0
- package/dist/slack-tool.js +316 -0
- package/dist/slack-tool.js.map +1 -0
- package/dist/telegram-tool.d.ts +7 -0
- package/dist/telegram-tool.d.ts.map +1 -0
- package/dist/telegram-tool.js +297 -0
- package/dist/telegram-tool.js.map +1 -0
- package/dist/vault-tool.d.ts +2 -0
- package/dist/vault-tool.d.ts.map +1 -0
- package/dist/vault-tool.js +395 -0
- package/dist/vault-tool.js.map +1 -0
- package/dist/xero-tool.d.ts +9 -0
- package/dist/xero-tool.d.ts.map +1 -0
- package/dist/xero-tool.js +330 -0
- package/dist/xero-tool.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// ─── Pure-local tool implementations ────────────────────────────────────────
|
|
2
|
+
// These run entirely inside the MCP process — no API calls, no external deps.
|
|
3
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// TEXT COUNT
|
|
5
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
6
|
+
export function countText(text) {
|
|
7
|
+
const chars = text.length;
|
|
8
|
+
const charsNoSpaces = text.replace(/\s/g, "").length;
|
|
9
|
+
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
10
|
+
const sentences = text.trim()
|
|
11
|
+
? (text.match(/[^.!?]*[.!?]+(?:\s|$)/g) ?? []).length
|
|
12
|
+
: 0;
|
|
13
|
+
const lines = text.split("\n").length;
|
|
14
|
+
const paragraphs = text.split(/\n\s*\n/).filter((p) => p.trim()).length || (text.trim() ? 1 : 0);
|
|
15
|
+
return { chars, chars_no_spaces: charsNoSpaces, words, sentences, lines, paragraphs };
|
|
16
|
+
}
|
|
17
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
18
|
+
// SLUG
|
|
19
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
20
|
+
export function generateSlug(text, separator = "-") {
|
|
21
|
+
return text
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.normalize("NFD")
|
|
24
|
+
.replace(/[\u0300-\u036f]/g, "") // strip diacritics
|
|
25
|
+
.replace(/[^a-z0-9\s_-]/g, "") // strip non-alphanumeric
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/[\s_-]+/g, separator); // collapse whitespace/underscores/hyphens
|
|
28
|
+
}
|
|
29
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
// LOREM IPSUM
|
|
31
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
32
|
+
const LOREM_POOL = ("lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor " +
|
|
33
|
+
"incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud " +
|
|
34
|
+
"exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure " +
|
|
35
|
+
"reprehenderit voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur " +
|
|
36
|
+
"sint occaecat cupidatat non proident sunt culpa officia deserunt mollit anim est " +
|
|
37
|
+
"laborum perspiciatis unde omnis iste natus error accusantium doloremque laudantium " +
|
|
38
|
+
"totam rem aperiam eaque ipsa quae ab inventore veritatis quasi architecto beatae " +
|
|
39
|
+
"vitae dicta explicabo nemo voluptatem quia voluptas aspernatur odit fugit eos " +
|
|
40
|
+
"consequuntur magni dolores ratione sequi nesciunt neque porro quisquam adipisci " +
|
|
41
|
+
"numquam eius modi tempora quaerat saepe eveniet repellat asperiores maxime placeat " +
|
|
42
|
+
"facilis expedita distinctio nam libero tempore soluta nobis optio cumque impedit " +
|
|
43
|
+
"minus praesentium ullam corporis suscipit laboriosam aliquid commodi consequatur " +
|
|
44
|
+
"quis autem vel eum iure reprehenderit voluptatem accusantium doloremque").split(" ");
|
|
45
|
+
function loremWord(i) {
|
|
46
|
+
return LOREM_POOL[Math.abs(i) % LOREM_POOL.length];
|
|
47
|
+
}
|
|
48
|
+
function loremSentence(offset, len) {
|
|
49
|
+
const words = [];
|
|
50
|
+
for (let i = 0; i < len; i++)
|
|
51
|
+
words.push(loremWord(offset + i * 3));
|
|
52
|
+
const s = words.join(" ");
|
|
53
|
+
return s.charAt(0).toUpperCase() + s.slice(1) + ".";
|
|
54
|
+
}
|
|
55
|
+
export function generateLorem(count, unit, startWithLorem) {
|
|
56
|
+
const prefix = startWithLorem ? "Lorem ipsum dolor sit amet. " : "";
|
|
57
|
+
if (unit === "words") {
|
|
58
|
+
const words = [];
|
|
59
|
+
for (let i = 0; i < count; i++)
|
|
60
|
+
words.push(loremWord(i + 5));
|
|
61
|
+
const out = words.join(" ");
|
|
62
|
+
return startWithLorem ? "Lorem ipsum " + out : out;
|
|
63
|
+
}
|
|
64
|
+
if (unit === "sentences") {
|
|
65
|
+
const sentences = [];
|
|
66
|
+
for (let i = 0; i < count; i++) {
|
|
67
|
+
const len = 7 + (i * 4 % 9); // 7–15 words
|
|
68
|
+
sentences.push(loremSentence(i * 11 + 5, len));
|
|
69
|
+
}
|
|
70
|
+
const out = sentences.join(" ");
|
|
71
|
+
return startWithLorem && !out.startsWith("Lorem") ? prefix + out : out;
|
|
72
|
+
}
|
|
73
|
+
// paragraphs
|
|
74
|
+
const paras = [];
|
|
75
|
+
for (let p = 0; p < count; p++) {
|
|
76
|
+
const sentCount = 3 + (p % 3); // 3–5 sentences
|
|
77
|
+
const sentences = [];
|
|
78
|
+
for (let s = 0; s < sentCount; s++) {
|
|
79
|
+
const len = 8 + ((p * 5 + s * 3) % 10); // 8–17 words
|
|
80
|
+
sentences.push(loremSentence(p * 50 + s * 13 + 5, len));
|
|
81
|
+
}
|
|
82
|
+
paras.push(sentences.join(" "));
|
|
83
|
+
}
|
|
84
|
+
const out = paras.join("\n\n");
|
|
85
|
+
return startWithLorem && !out.startsWith("Lorem") ? prefix + out : out;
|
|
86
|
+
}
|
|
87
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
88
|
+
// JWT DECODER
|
|
89
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
90
|
+
export function decodeJwt(token) {
|
|
91
|
+
const parts = token.trim().split(".");
|
|
92
|
+
if (parts.length !== 3) {
|
|
93
|
+
throw new Error("Invalid JWT: expected 3 dot-separated parts");
|
|
94
|
+
}
|
|
95
|
+
const b64urlDecode = (s) => {
|
|
96
|
+
const padded = s.replace(/-/g, "+").replace(/_/g, "/")
|
|
97
|
+
.padEnd(s.length + ((4 - (s.length % 4)) % 4), "=");
|
|
98
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
99
|
+
};
|
|
100
|
+
const header = b64urlDecode(parts[0]);
|
|
101
|
+
const payload = b64urlDecode(parts[1]);
|
|
102
|
+
const result = {
|
|
103
|
+
header,
|
|
104
|
+
payload,
|
|
105
|
+
signature: parts[2],
|
|
106
|
+
warning: "Signature NOT verified — for inspection only",
|
|
107
|
+
};
|
|
108
|
+
if (typeof payload.iat === "number") {
|
|
109
|
+
result.issued_at = new Date(payload.iat * 1000).toISOString();
|
|
110
|
+
}
|
|
111
|
+
if (typeof payload.exp === "number") {
|
|
112
|
+
const expDate = new Date(payload.exp * 1000);
|
|
113
|
+
result.expires_at = expDate.toISOString();
|
|
114
|
+
result.expired = expDate < new Date();
|
|
115
|
+
if (!result.expired) {
|
|
116
|
+
const secsLeft = Math.floor((expDate.getTime() - Date.now()) / 1000);
|
|
117
|
+
result.expires_in_seconds = secsLeft;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (typeof payload.nbf === "number") {
|
|
121
|
+
result.not_before = new Date(payload.nbf * 1000).toISOString();
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
126
|
+
// HTTP STATUS LOOKUP
|
|
127
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
128
|
+
const HTTP_STATUSES = {
|
|
129
|
+
// 1xx Informational
|
|
130
|
+
100: { phrase: "Continue", category: "Informational", description: "The server has received the request headers and the client should proceed to send the request body." },
|
|
131
|
+
101: { phrase: "Switching Protocols", category: "Informational", description: "The requester has asked the server to switch protocols and the server has agreed to do so." },
|
|
132
|
+
102: { phrase: "Processing", category: "Informational", description: "The server has received and is processing the request, but no response is available yet." },
|
|
133
|
+
103: { phrase: "Early Hints", category: "Informational", description: "Used to return some response headers before final HTTP message." },
|
|
134
|
+
// 2xx Success
|
|
135
|
+
200: { phrase: "OK", category: "Success", description: "Standard response for successful HTTP requests." },
|
|
136
|
+
201: { phrase: "Created", category: "Success", description: "The request has been fulfilled, resulting in the creation of a new resource." },
|
|
137
|
+
202: { phrase: "Accepted", category: "Success", description: "The request has been accepted for processing, but the processing has not been completed." },
|
|
138
|
+
203: { phrase: "Non-Authoritative Information", category: "Success", description: "The server is a transforming proxy that received a 200 OK from its origin, but is returning a modified version." },
|
|
139
|
+
204: { phrase: "No Content", category: "Success", description: "The server successfully processed the request and is not returning any content." },
|
|
140
|
+
205: { phrase: "Reset Content", category: "Success", description: "The server successfully processed the request, asks that the requester reset its document view." },
|
|
141
|
+
206: { phrase: "Partial Content", category: "Success", description: "The server is delivering only part of the resource due to a range header sent by the client." },
|
|
142
|
+
207: { phrase: "Multi-Status", category: "Success", description: "The message body that follows is by default an XML message and can contain a number of separate response codes." },
|
|
143
|
+
208: { phrase: "Already Reported", category: "Success", description: "The members of a DAV binding have already been enumerated in a preceding part of the response." },
|
|
144
|
+
226: { phrase: "IM Used", category: "Success", description: "The server has fulfilled a request for the resource, and the response is a representation of the result." },
|
|
145
|
+
// 3xx Redirection
|
|
146
|
+
300: { phrase: "Multiple Choices", category: "Redirection", description: "Indicates multiple options for the resource from which the client may choose." },
|
|
147
|
+
301: { phrase: "Moved Permanently", category: "Redirection", description: "This and all future requests should be directed to the given URI." },
|
|
148
|
+
302: { phrase: "Found", category: "Redirection", description: "The resource is temporarily located at another URI." },
|
|
149
|
+
303: { phrase: "See Other", category: "Redirection", description: "The response to the request can be found under another URI using the GET method." },
|
|
150
|
+
304: { phrase: "Not Modified", category: "Redirection", description: "The resource has not been modified since the version specified by the request headers." },
|
|
151
|
+
307: { phrase: "Temporary Redirect", category: "Redirection", description: "The request should be repeated with another URI; same method must be used." },
|
|
152
|
+
308: { phrase: "Permanent Redirect", category: "Redirection", description: "The request and all future requests should be repeated using another URI; same method must be used." },
|
|
153
|
+
// 4xx Client Error
|
|
154
|
+
400: { phrase: "Bad Request", category: "Client Error", description: "The server cannot or will not process the request due to an apparent client error." },
|
|
155
|
+
401: { phrase: "Unauthorized", category: "Client Error", description: "Authentication is required and has failed or has not yet been provided." },
|
|
156
|
+
402: { phrase: "Payment Required", category: "Client Error", description: "Reserved for future use. Original intention: digital payment systems." },
|
|
157
|
+
403: { phrase: "Forbidden", category: "Client Error", description: "The request contained valid data but the server is refusing action. User may not have necessary permissions." },
|
|
158
|
+
404: { phrase: "Not Found", category: "Client Error", description: "The requested resource could not be found but may be available in the future." },
|
|
159
|
+
405: { phrase: "Method Not Allowed", category: "Client Error", description: "A request method is not supported for the requested resource." },
|
|
160
|
+
406: { phrase: "Not Acceptable", category: "Client Error", description: "The requested resource is capable of generating only content not acceptable according to the Accept headers." },
|
|
161
|
+
407: { phrase: "Proxy Authentication Required", category: "Client Error", description: "The client must first authenticate itself with the proxy." },
|
|
162
|
+
408: { phrase: "Request Timeout", category: "Client Error", description: "The server timed out waiting for the request." },
|
|
163
|
+
409: { phrase: "Conflict", category: "Client Error", description: "Indicates that the request could not be processed because of conflict in the current state of the resource." },
|
|
164
|
+
410: { phrase: "Gone", category: "Client Error", description: "The resource requested is no longer available and will not be available again." },
|
|
165
|
+
411: { phrase: "Length Required", category: "Client Error", description: "The request did not specify the length of its content, which is required by the requested resource." },
|
|
166
|
+
412: { phrase: "Precondition Failed", category: "Client Error", description: "The server does not meet one of the preconditions that the requester put on the request." },
|
|
167
|
+
413: { phrase: "Content Too Large", category: "Client Error", description: "The request is larger than the server is willing or able to process." },
|
|
168
|
+
414: { phrase: "URI Too Long", category: "Client Error", description: "The URI provided was too long for the server to process." },
|
|
169
|
+
415: { phrase: "Unsupported Media Type", category: "Client Error", description: "The request entity has a media type which the server or resource does not support." },
|
|
170
|
+
416: { phrase: "Range Not Satisfiable", category: "Client Error", description: "The client has asked for a portion of the file, but the server cannot supply that portion." },
|
|
171
|
+
417: { phrase: "Expectation Failed", category: "Client Error", description: "The server cannot meet the requirements of the Expect request-header field." },
|
|
172
|
+
418: { phrase: "I'm a Teapot", category: "Client Error", description: "The server refuses the attempt to brew coffee with a teapot. (RFC 2324, April Fools' joke — but real!)" },
|
|
173
|
+
421: { phrase: "Misdirected Request", category: "Client Error", description: "The request was directed at a server that is not able to produce a response." },
|
|
174
|
+
422: { phrase: "Unprocessable Content", category: "Client Error", description: "The request was well-formed but was unable to be followed due to semantic errors." },
|
|
175
|
+
423: { phrase: "Locked", category: "Client Error", description: "The resource that is being accessed is locked." },
|
|
176
|
+
424: { phrase: "Failed Dependency", category: "Client Error", description: "The request failed because it depended on another request and that request failed." },
|
|
177
|
+
425: { phrase: "Too Early", category: "Client Error", description: "Indicates that the server is unwilling to risk processing a request that might be replayed." },
|
|
178
|
+
426: { phrase: "Upgrade Required", category: "Client Error", description: "The client should switch to a different protocol." },
|
|
179
|
+
428: { phrase: "Precondition Required", category: "Client Error", description: "The origin server requires the request to be conditional." },
|
|
180
|
+
429: { phrase: "Too Many Requests", category: "Client Error", description: "The user has sent too many requests in a given amount of time (rate limiting)." },
|
|
181
|
+
431: { phrase: "Request Header Fields Too Large", category: "Client Error", description: "The server is unwilling to process the request because its header fields are too large." },
|
|
182
|
+
451: { phrase: "Unavailable For Legal Reasons", category: "Client Error", description: "A server operator has received a legal demand to deny access to a resource." },
|
|
183
|
+
// 5xx Server Error
|
|
184
|
+
500: { phrase: "Internal Server Error", category: "Server Error", description: "A generic error message when an unexpected condition was encountered on the server." },
|
|
185
|
+
501: { phrase: "Not Implemented", category: "Server Error", description: "The server either does not recognize the request method, or it lacks the ability to fulfill it." },
|
|
186
|
+
502: { phrase: "Bad Gateway", category: "Server Error", description: "The server was acting as a gateway or proxy and received an invalid response from the upstream server." },
|
|
187
|
+
503: { phrase: "Service Unavailable", category: "Server Error", description: "The server cannot handle the request (overloaded or down for maintenance)." },
|
|
188
|
+
504: { phrase: "Gateway Timeout", category: "Server Error", description: "The server was acting as a gateway or proxy and did not receive a timely response from upstream." },
|
|
189
|
+
505: { phrase: "HTTP Version Not Supported", category: "Server Error", description: "The server does not support the HTTP version used in the request." },
|
|
190
|
+
506: { phrase: "Variant Also Negotiates", category: "Server Error", description: "Transparent content negotiation for the request results in a circular reference." },
|
|
191
|
+
507: { phrase: "Insufficient Storage", category: "Server Error", description: "The server is unable to store the representation needed to complete the request." },
|
|
192
|
+
508: { phrase: "Loop Detected", category: "Server Error", description: "The server detected an infinite loop while processing a request." },
|
|
193
|
+
510: { phrase: "Not Extended", category: "Server Error", description: "Further extensions to the request are required for the server to fulfil it." },
|
|
194
|
+
511: { phrase: "Network Authentication Required", category: "Server Error", description: "The client needs to authenticate to gain network access." },
|
|
195
|
+
};
|
|
196
|
+
export function lookupHttpStatus(code) {
|
|
197
|
+
const entry = HTTP_STATUSES[code];
|
|
198
|
+
if (!entry) {
|
|
199
|
+
// Try to give a category hint even for unknown codes
|
|
200
|
+
const known = Object.keys(HTTP_STATUSES).map(Number).filter((c) => c === code);
|
|
201
|
+
if (known.length === 0) {
|
|
202
|
+
const range = Math.floor(code / 100) * 100;
|
|
203
|
+
const categories = {
|
|
204
|
+
100: "1xx Informational", 200: "2xx Success",
|
|
205
|
+
300: "3xx Redirection", 400: "4xx Client Error", 500: "5xx Server Error",
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
code,
|
|
209
|
+
known: false,
|
|
210
|
+
category: categories[range] ?? "Unknown",
|
|
211
|
+
message: `HTTP ${code} is not a well-known status code.`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
code,
|
|
217
|
+
known: true,
|
|
218
|
+
phrase: entry.phrase,
|
|
219
|
+
category: entry.category,
|
|
220
|
+
description: entry.description,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const EMOJI_DB = [
|
|
224
|
+
{ emoji: "😀", name: "grinning face", keywords: ["happy", "smile", "grin", "joy", "laugh"] },
|
|
225
|
+
{ emoji: "😂", name: "face with tears of joy", keywords: ["laugh", "cry", "funny", "lol", "tears"] },
|
|
226
|
+
{ emoji: "🥹", name: "face holding back tears", keywords: ["moved", "grateful", "emotional", "tears"] },
|
|
227
|
+
{ emoji: "😍", name: "smiling face with heart-eyes", keywords: ["love", "crush", "heart", "adore"] },
|
|
228
|
+
{ emoji: "🤔", name: "thinking face", keywords: ["think", "ponder", "wonder", "hmm", "question"] },
|
|
229
|
+
{ emoji: "😎", name: "smiling face with sunglasses", keywords: ["cool", "sunglasses", "awesome", "chill"] },
|
|
230
|
+
{ emoji: "😭", name: "loudly crying face", keywords: ["cry", "sad", "tears", "sob", "upset"] },
|
|
231
|
+
{ emoji: "😤", name: "face with steam from nose", keywords: ["angry", "frustrate", "huff", "triumph"] },
|
|
232
|
+
{ emoji: "🤯", name: "exploding head", keywords: ["mind blown", "shock", "amazed", "wow", "surprised"] },
|
|
233
|
+
{ emoji: "🥳", name: "partying face", keywords: ["party", "celebrate", "birthday", "congrats", "fun"] },
|
|
234
|
+
{ emoji: "😴", name: "sleeping face", keywords: ["sleep", "tired", "bored", "zzz", "rest"] },
|
|
235
|
+
{ emoji: "🤮", name: "face vomiting", keywords: ["sick", "gross", "disgusting", "vomit", "nausea"] },
|
|
236
|
+
{ emoji: "😷", name: "face with medical mask", keywords: ["sick", "mask", "covid", "ill", "health"] },
|
|
237
|
+
{ emoji: "🥶", name: "cold face", keywords: ["cold", "freeze", "ice", "winter", "chill"] },
|
|
238
|
+
{ emoji: "🥵", name: "hot face", keywords: ["hot", "heat", "summer", "sweat", "fire"] },
|
|
239
|
+
{ emoji: "❤️", name: "red heart", keywords: ["love", "heart", "romance", "affection", "like"] },
|
|
240
|
+
{ emoji: "🔥", name: "fire", keywords: ["fire", "hot", "lit", "burn", "flame", "trending"] },
|
|
241
|
+
{ emoji: "⭐", name: "star", keywords: ["star", "favorite", "rating", "excellent", "night"] },
|
|
242
|
+
{ emoji: "✅", name: "check mark button", keywords: ["check", "done", "complete", "yes", "ok", "correct"] },
|
|
243
|
+
{ emoji: "❌", name: "cross mark", keywords: ["no", "wrong", "error", "fail", "cancel", "delete"] },
|
|
244
|
+
{ emoji: "⚠️", name: "warning", keywords: ["warning", "caution", "alert", "danger", "careful"] },
|
|
245
|
+
{ emoji: "🚀", name: "rocket", keywords: ["launch", "rocket", "space", "fast", "startup", "ship"] },
|
|
246
|
+
{ emoji: "💡", name: "light bulb", keywords: ["idea", "light", "tip", "suggest", "bright", "bulb"] },
|
|
247
|
+
{ emoji: "🎉", name: "party popper", keywords: ["celebrate", "party", "congrats", "hooray", "confetti"] },
|
|
248
|
+
{ emoji: "🎯", name: "bullseye", keywords: ["target", "goal", "aim", "focus", "hit", "precise"] },
|
|
249
|
+
{ emoji: "🏆", name: "trophy", keywords: ["win", "award", "trophy", "champion", "first", "best"] },
|
|
250
|
+
{ emoji: "💯", name: "hundred points", keywords: ["100", "perfect", "score", "great", "fire"] },
|
|
251
|
+
{ emoji: "🔑", name: "key", keywords: ["key", "lock", "access", "password", "security"] },
|
|
252
|
+
{ emoji: "🛠️", name: "hammer and wrench", keywords: ["tool", "fix", "build", "repair", "settings", "developer"] },
|
|
253
|
+
{ emoji: "📝", name: "memo", keywords: ["note", "write", "memo", "edit", "list", "document"] },
|
|
254
|
+
{ emoji: "📊", name: "bar chart", keywords: ["chart", "graph", "stats", "data", "analytics", "bar"] },
|
|
255
|
+
{ emoji: "📈", name: "chart increasing", keywords: ["up", "growth", "increase", "profit", "success", "chart"] },
|
|
256
|
+
{ emoji: "📉", name: "chart decreasing", keywords: ["down", "decrease", "loss", "drop", "chart"] },
|
|
257
|
+
{ emoji: "📦", name: "package", keywords: ["box", "package", "shipping", "deliver", "npm", "deploy"] },
|
|
258
|
+
{ emoji: "🔗", name: "link", keywords: ["link", "url", "chain", "connect", "href"] },
|
|
259
|
+
{ emoji: "🐛", name: "bug", keywords: ["bug", "error", "issue", "debug", "insect"] },
|
|
260
|
+
{ emoji: "💻", name: "laptop computer", keywords: ["laptop", "computer", "code", "work", "dev"] },
|
|
261
|
+
{ emoji: "📱", name: "mobile phone", keywords: ["phone", "mobile", "app", "ios", "android", "cell"] },
|
|
262
|
+
{ emoji: "🌐", name: "globe with meridians", keywords: ["web", "internet", "globe", "world", "global", "network"] },
|
|
263
|
+
{ emoji: "🔒", name: "locked", keywords: ["lock", "secure", "private", "closed", "auth"] },
|
|
264
|
+
{ emoji: "🔓", name: "unlocked", keywords: ["unlock", "open", "access", "free"] },
|
|
265
|
+
{ emoji: "👍", name: "thumbs up", keywords: ["like", "good", "agree", "yes", "approve", "ok"] },
|
|
266
|
+
{ emoji: "👎", name: "thumbs down", keywords: ["dislike", "bad", "no", "disagree", "disapprove"] },
|
|
267
|
+
{ emoji: "👀", name: "eyes", keywords: ["look", "see", "watch", "eye", "view"] },
|
|
268
|
+
{ emoji: "💪", name: "flexed biceps", keywords: ["strong", "muscle", "power", "flex", "strength"] },
|
|
269
|
+
{ emoji: "🙏", name: "folded hands", keywords: ["please", "thanks", "pray", "namaste", "grateful"] },
|
|
270
|
+
{ emoji: "🤝", name: "handshake", keywords: ["deal", "agree", "partner", "shake", "cooperate"] },
|
|
271
|
+
{ emoji: "👋", name: "waving hand", keywords: ["wave", "hello", "hi", "bye", "greet"] },
|
|
272
|
+
{ emoji: "🤦", name: "person facepalming", keywords: ["facepalm", "ugh", "frustrated", "embarrass", "duh"] },
|
|
273
|
+
{ emoji: "🤷", name: "person shrugging", keywords: ["shrug", "idk", "dunno", "whatever", "confused"] },
|
|
274
|
+
{ emoji: "🐱", name: "cat face", keywords: ["cat", "kitty", "meow", "pet", "animal"] },
|
|
275
|
+
{ emoji: "🐶", name: "dog face", keywords: ["dog", "puppy", "woof", "pet", "animal"] },
|
|
276
|
+
{ emoji: "🦊", name: "fox", keywords: ["fox", "animal", "clever", "sly"] },
|
|
277
|
+
{ emoji: "🦁", name: "lion", keywords: ["lion", "king", "animal", "brave", "courage"] },
|
|
278
|
+
{ emoji: "🐧", name: "penguin", keywords: ["penguin", "linux", "animal", "bird", "cold"] },
|
|
279
|
+
{ emoji: "🌟", name: "glowing star", keywords: ["star", "glow", "shine", "sparkle", "special"] },
|
|
280
|
+
{ emoji: "🌈", name: "rainbow", keywords: ["rainbow", "color", "pride", "bright", "hope"] },
|
|
281
|
+
{ emoji: "⚡", name: "lightning", keywords: ["lightning", "fast", "electric", "power", "energy", "speed"] },
|
|
282
|
+
{ emoji: "🌊", name: "water wave", keywords: ["wave", "ocean", "sea", "surf", "water"] },
|
|
283
|
+
{ emoji: "🍕", name: "pizza", keywords: ["pizza", "food", "eat", "italian", "slice"] },
|
|
284
|
+
{ emoji: "☕", name: "hot beverage", keywords: ["coffee", "tea", "hot", "drink", "morning", "cafe"] },
|
|
285
|
+
{ emoji: "🍺", name: "beer mug", keywords: ["beer", "drink", "alcohol", "cheers", "mug"] },
|
|
286
|
+
{ emoji: "🎵", name: "musical note", keywords: ["music", "note", "song", "sound", "melody"] },
|
|
287
|
+
{ emoji: "🎮", name: "video game", keywords: ["game", "play", "controller", "gaming", "video"] },
|
|
288
|
+
{ emoji: "📸", name: "camera with flash", keywords: ["photo", "camera", "picture", "snap", "flash"] },
|
|
289
|
+
{ emoji: "🗺️", name: "world map", keywords: ["map", "world", "travel", "navigate", "geography"] },
|
|
290
|
+
{ emoji: "⏰", name: "alarm clock", keywords: ["alarm", "clock", "time", "wake", "schedule"] },
|
|
291
|
+
{ emoji: "📅", name: "calendar", keywords: ["calendar", "date", "schedule", "event", "plan"] },
|
|
292
|
+
{ emoji: "🔔", name: "bell", keywords: ["bell", "notify", "alert", "ring", "notification"] },
|
|
293
|
+
{ emoji: "🗑️", name: "wastebasket", keywords: ["trash", "delete", "garbage", "bin", "remove"] },
|
|
294
|
+
{ emoji: "🔄", name: "counterclockwise arrows button", keywords: ["refresh", "reload", "sync", "update", "repeat"] },
|
|
295
|
+
{ emoji: "➕", name: "plus sign", keywords: ["add", "plus", "new", "create", "more"] },
|
|
296
|
+
{ emoji: "➖", name: "minus sign", keywords: ["remove", "minus", "subtract", "less"] },
|
|
297
|
+
{ emoji: "🔀", name: "shuffle tracks button", keywords: ["shuffle", "random", "mix", "sort"] },
|
|
298
|
+
{ emoji: "📌", name: "pushpin", keywords: ["pin", "mark", "important", "save", "note"] },
|
|
299
|
+
{ emoji: "🏠", name: "house", keywords: ["home", "house", "building", "live", "location"] },
|
|
300
|
+
{ emoji: "🌍", name: "globe showing europe-africa", keywords: ["earth", "world", "global", "planet", "globe"] },
|
|
301
|
+
{ emoji: "🧪", name: "test tube", keywords: ["test", "experiment", "lab", "science", "chemistry"] },
|
|
302
|
+
{ emoji: "🧩", name: "puzzle piece", keywords: ["puzzle", "piece", "fit", "solve", "integration"] },
|
|
303
|
+
{ emoji: "🎁", name: "wrapped gift", keywords: ["gift", "present", "surprise", "wrap", "birthday"] },
|
|
304
|
+
{ emoji: "🚨", name: "police car light", keywords: ["alert", "emergency", "siren", "warning", "critical"] },
|
|
305
|
+
{ emoji: "🏗️", name: "building construction", keywords: ["build", "construct", "work in progress", "wip", "develop"] },
|
|
306
|
+
{ emoji: "🤖", name: "robot", keywords: ["robot", "bot", "ai", "automation", "machine"] },
|
|
307
|
+
];
|
|
308
|
+
export function searchEmoji(keyword, limit = 10) {
|
|
309
|
+
const q = keyword.toLowerCase().trim();
|
|
310
|
+
if (!q)
|
|
311
|
+
return EMOJI_DB.slice(0, limit);
|
|
312
|
+
const scored = [];
|
|
313
|
+
for (const entry of EMOJI_DB) {
|
|
314
|
+
let score = 0;
|
|
315
|
+
if (entry.name === q)
|
|
316
|
+
score += 100;
|
|
317
|
+
else if (entry.name.startsWith(q))
|
|
318
|
+
score += 50;
|
|
319
|
+
else if (entry.name.includes(q))
|
|
320
|
+
score += 30;
|
|
321
|
+
for (const kw of entry.keywords) {
|
|
322
|
+
if (kw === q)
|
|
323
|
+
score += 40;
|
|
324
|
+
else if (kw.startsWith(q))
|
|
325
|
+
score += 20;
|
|
326
|
+
else if (kw.includes(q))
|
|
327
|
+
score += 10;
|
|
328
|
+
}
|
|
329
|
+
if (score > 0)
|
|
330
|
+
scored.push({ entry, score });
|
|
331
|
+
}
|
|
332
|
+
scored.sort((a, b) => b.score - a.score);
|
|
333
|
+
return scored.slice(0, limit).map((s) => s.entry);
|
|
334
|
+
}
|
|
335
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
336
|
+
// USER AGENT PARSER
|
|
337
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
338
|
+
export function parseUserAgent(ua) {
|
|
339
|
+
const s = ua.trim();
|
|
340
|
+
// Device type
|
|
341
|
+
const isBot = /bot|crawl|spider|slurp|baiduspider|yandex|googlebot/i.test(s);
|
|
342
|
+
const isMobile = !isBot && /mobile|android.*mobile|iphone|ipod|blackberry|windows phone/i.test(s);
|
|
343
|
+
const isTablet = !isBot && !isMobile && /tablet|ipad|android(?!.*mobile)/i.test(s);
|
|
344
|
+
const deviceType = isBot ? "bot" : isMobile ? "mobile" : isTablet ? "tablet" : "desktop";
|
|
345
|
+
// OS
|
|
346
|
+
let os = "Unknown";
|
|
347
|
+
if (/windows nt 10/i.test(s))
|
|
348
|
+
os = "Windows 10/11";
|
|
349
|
+
else if (/windows nt 6\.3/i.test(s))
|
|
350
|
+
os = "Windows 8.1";
|
|
351
|
+
else if (/windows nt 6\.2/i.test(s))
|
|
352
|
+
os = "Windows 8";
|
|
353
|
+
else if (/windows nt 6\.1/i.test(s))
|
|
354
|
+
os = "Windows 7";
|
|
355
|
+
else if (/windows/i.test(s))
|
|
356
|
+
os = "Windows";
|
|
357
|
+
else if (/mac os x ([\d_]+)/i.test(s)) {
|
|
358
|
+
const m = s.match(/mac os x ([\d_]+)/i);
|
|
359
|
+
os = `macOS ${m ? m[1].replace(/_/g, ".") : ""}`;
|
|
360
|
+
}
|
|
361
|
+
else if (/iphone os ([\d_]+)/i.test(s)) {
|
|
362
|
+
const m = s.match(/iphone os ([\d_]+)/i);
|
|
363
|
+
os = `iOS ${m ? m[1].replace(/_/g, ".") : ""}`;
|
|
364
|
+
}
|
|
365
|
+
else if (/ipad.*os ([\d_]+)/i.test(s)) {
|
|
366
|
+
const m = s.match(/os ([\d_]+)/i);
|
|
367
|
+
os = `iPadOS ${m ? m[1].replace(/_/g, ".") : ""}`;
|
|
368
|
+
}
|
|
369
|
+
else if (/android ([\d.]+)/i.test(s)) {
|
|
370
|
+
const m = s.match(/android ([\d.]+)/i);
|
|
371
|
+
os = `Android ${m ? m[1] : ""}`;
|
|
372
|
+
}
|
|
373
|
+
else if (/linux/i.test(s))
|
|
374
|
+
os = "Linux";
|
|
375
|
+
else if (/cros/i.test(s))
|
|
376
|
+
os = "ChromeOS";
|
|
377
|
+
// Browser
|
|
378
|
+
let browser = "Unknown";
|
|
379
|
+
let browserVersion = "";
|
|
380
|
+
const extractVersion = (pattern) => {
|
|
381
|
+
const m = s.match(pattern);
|
|
382
|
+
return m ? m[1] : "";
|
|
383
|
+
};
|
|
384
|
+
if (/edg\/([\d.]+)/i.test(s)) {
|
|
385
|
+
browser = "Edge";
|
|
386
|
+
browserVersion = extractVersion(/edg\/([\d.]+)/i);
|
|
387
|
+
}
|
|
388
|
+
else if (/opr\/([\d.]+)|opera\/([\d.]+)/i.test(s)) {
|
|
389
|
+
browser = "Opera";
|
|
390
|
+
browserVersion = extractVersion(/(?:opr|opera)\/([\d.]+)/i);
|
|
391
|
+
}
|
|
392
|
+
else if (/chrome\/([\d.]+)/i.test(s) && !/chromium/i.test(s)) {
|
|
393
|
+
browser = "Chrome";
|
|
394
|
+
browserVersion = extractVersion(/chrome\/([\d.]+)/i);
|
|
395
|
+
}
|
|
396
|
+
else if (/firefox\/([\d.]+)/i.test(s)) {
|
|
397
|
+
browser = "Firefox";
|
|
398
|
+
browserVersion = extractVersion(/firefox\/([\d.]+)/i);
|
|
399
|
+
}
|
|
400
|
+
else if (/safari\/([\d.]+)/i.test(s) && /version\/([\d.]+)/i.test(s)) {
|
|
401
|
+
browser = "Safari";
|
|
402
|
+
browserVersion = extractVersion(/version\/([\d.]+)/i);
|
|
403
|
+
}
|
|
404
|
+
else if (/msie ([\d.]+)|trident.*rv:([\d.]+)/i.test(s)) {
|
|
405
|
+
browser = "Internet Explorer";
|
|
406
|
+
browserVersion = extractVersion(/(?:msie |rv:)([\d.]+)/i);
|
|
407
|
+
}
|
|
408
|
+
else if (/chromium\/([\d.]+)/i.test(s)) {
|
|
409
|
+
browser = "Chromium";
|
|
410
|
+
browserVersion = extractVersion(/chromium\/([\d.]+)/i);
|
|
411
|
+
}
|
|
412
|
+
else if (isBot) {
|
|
413
|
+
const botMatch = s.match(/(\w+bot|\w+spider|\w+crawler)/i);
|
|
414
|
+
browser = botMatch ? botMatch[1] : "Bot";
|
|
415
|
+
}
|
|
416
|
+
// Rendering engine
|
|
417
|
+
let engine = "Unknown";
|
|
418
|
+
if (/gecko\/[\d.]+/i.test(s) && /rv:[\d.]+/i.test(s))
|
|
419
|
+
engine = "Gecko";
|
|
420
|
+
else if (/applewebkit\/([\d.]+)/i.test(s))
|
|
421
|
+
engine = "WebKit/Blink";
|
|
422
|
+
else if (/trident\/([\d.]+)/i.test(s))
|
|
423
|
+
engine = "Trident";
|
|
424
|
+
else if (/presto\/([\d.]+)/i.test(s))
|
|
425
|
+
engine = "Presto";
|
|
426
|
+
return {
|
|
427
|
+
browser,
|
|
428
|
+
browser_version: browserVersion,
|
|
429
|
+
os,
|
|
430
|
+
device_type: deviceType,
|
|
431
|
+
engine,
|
|
432
|
+
is_mobile: isMobile,
|
|
433
|
+
is_tablet: isTablet,
|
|
434
|
+
is_bot: isBot,
|
|
435
|
+
raw: ua,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
439
|
+
// README TEMPLATE GENERATOR
|
|
440
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
441
|
+
export function generateReadme(opts) {
|
|
442
|
+
const { name, description, install, usage, language = "", license = "MIT", repo = "", badges = true, } = opts;
|
|
443
|
+
const lines = [];
|
|
444
|
+
// Title
|
|
445
|
+
lines.push(`# ${name}`);
|
|
446
|
+
lines.push("");
|
|
447
|
+
// Badges
|
|
448
|
+
if (badges && repo) {
|
|
449
|
+
const [owner, repoName] = repo.replace(/^https?:\/\/github\.com\//, "").split("/");
|
|
450
|
+
if (owner && repoName) {
|
|
451
|
+
lines.push(`}-blue.svg)`);
|
|
452
|
+
lines.push(``);
|
|
453
|
+
lines.push("");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Description
|
|
457
|
+
lines.push(`> ${description}`);
|
|
458
|
+
lines.push("");
|
|
459
|
+
// Table of contents
|
|
460
|
+
const sections = ["Installation", "Usage", "License"];
|
|
461
|
+
lines.push("## Table of Contents");
|
|
462
|
+
for (const s of sections) {
|
|
463
|
+
lines.push(`- [${s}](#${s.toLowerCase()})`);
|
|
464
|
+
}
|
|
465
|
+
lines.push("");
|
|
466
|
+
// Installation
|
|
467
|
+
lines.push("## Installation");
|
|
468
|
+
lines.push("");
|
|
469
|
+
if (install) {
|
|
470
|
+
lines.push("```bash");
|
|
471
|
+
lines.push(install);
|
|
472
|
+
lines.push("```");
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
const installCmd = language === "python"
|
|
476
|
+
? `pip install ${name.toLowerCase()}`
|
|
477
|
+
: language === "rust"
|
|
478
|
+
? `cargo add ${name.toLowerCase()}`
|
|
479
|
+
: language === "go"
|
|
480
|
+
? `go get ${repo || `github.com/owner/${name.toLowerCase()}`}`
|
|
481
|
+
: `npm install ${name.toLowerCase()}`;
|
|
482
|
+
lines.push("```bash");
|
|
483
|
+
lines.push(installCmd);
|
|
484
|
+
lines.push("```");
|
|
485
|
+
}
|
|
486
|
+
lines.push("");
|
|
487
|
+
// Usage
|
|
488
|
+
lines.push("## Usage");
|
|
489
|
+
lines.push("");
|
|
490
|
+
if (usage) {
|
|
491
|
+
const ext = language === "python" ? "python"
|
|
492
|
+
: language === "rust" ? "rust"
|
|
493
|
+
: language === "go" ? "go"
|
|
494
|
+
: "javascript";
|
|
495
|
+
lines.push(`\`\`\`${ext}`);
|
|
496
|
+
lines.push(usage);
|
|
497
|
+
lines.push("```");
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
lines.push("<!-- Add usage examples here -->");
|
|
501
|
+
}
|
|
502
|
+
lines.push("");
|
|
503
|
+
// Contributing
|
|
504
|
+
lines.push("## Contributing");
|
|
505
|
+
lines.push("");
|
|
506
|
+
lines.push("Contributions are welcome! Please open an issue or submit a pull request.");
|
|
507
|
+
lines.push("");
|
|
508
|
+
// License
|
|
509
|
+
lines.push("## License");
|
|
510
|
+
lines.push("");
|
|
511
|
+
lines.push(`This project is licensed under the **${license}** License.`);
|
|
512
|
+
if (repo)
|
|
513
|
+
lines.push(`See [LICENSE](${repo}/blob/main/LICENSE) for details.`);
|
|
514
|
+
lines.push("");
|
|
515
|
+
return lines.join("\n");
|
|
516
|
+
}
|
|
517
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
518
|
+
// CHANGELOG ENTRY GENERATOR
|
|
519
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
520
|
+
export function generateChangelog(opts) {
|
|
521
|
+
const { version, date = new Date().toISOString().slice(0, 10), added = [], changed = [], deprecated = [], removed = [], fixed = [], security = [], } = opts;
|
|
522
|
+
const lines = [];
|
|
523
|
+
lines.push(`## [${version}] - ${date}`);
|
|
524
|
+
lines.push("");
|
|
525
|
+
const section = (title, items) => {
|
|
526
|
+
if (items.length === 0)
|
|
527
|
+
return;
|
|
528
|
+
lines.push(`### ${title}`);
|
|
529
|
+
for (const item of items) {
|
|
530
|
+
lines.push(`- ${item.startsWith("- ") ? item.slice(2) : item}`);
|
|
531
|
+
}
|
|
532
|
+
lines.push("");
|
|
533
|
+
};
|
|
534
|
+
section("Added", added);
|
|
535
|
+
section("Changed", changed);
|
|
536
|
+
section("Deprecated", deprecated);
|
|
537
|
+
section("Removed", removed);
|
|
538
|
+
section("Fixed", fixed);
|
|
539
|
+
section("Security", security);
|
|
540
|
+
if (lines[lines.length - 1] === "")
|
|
541
|
+
lines.pop(); // trim trailing blank line
|
|
542
|
+
return lines.join("\n");
|
|
543
|
+
}
|
|
544
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
545
|
+
// FAVICON URL
|
|
546
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
547
|
+
export function getFaviconUrls(input) {
|
|
548
|
+
// Normalise: strip protocol, path, etc.
|
|
549
|
+
let domain = input.trim().replace(/^https?:\/\//i, "").split("/")[0].split("?")[0];
|
|
550
|
+
if (!domain)
|
|
551
|
+
throw new Error("Could not parse domain from input");
|
|
552
|
+
const directUrl = `https://${domain}/favicon.ico`;
|
|
553
|
+
const googleUrl = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=64`;
|
|
554
|
+
const duckUrl = `https://icons.duckduckgo.com/ip3/${encodeURIComponent(domain)}.ico`;
|
|
555
|
+
return {
|
|
556
|
+
domain,
|
|
557
|
+
favicon_ico: directUrl,
|
|
558
|
+
google_favicon_api: googleUrl,
|
|
559
|
+
duckduckgo_favicon_api: duckUrl,
|
|
560
|
+
note: "favicon_ico may return 404 if the site uses a non-standard location. The Google/DuckDuckGo URLs reliably return an icon for most domains.",
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
//# sourceMappingURL=local-tools.js.map
|