@utilix-tech/sdk 0.2.0 → 0.3.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 CHANGED
@@ -26,13 +26,13 @@ yarn add @utilix-tech/sdk
26
26
  pnpm add @utilix-tech/sdk
27
27
  ```
28
28
 
29
- **Requires Node.js 18 or later.** TypeScript types are bundled no `@types/` package needed.
29
+ **Requires Node.js 18 or later.** The public npm package includes bundled TypeScript types, so no `@types/` package is needed.
30
30
 
31
31
  ---
32
32
 
33
33
  ## Quick Start
34
34
 
35
- Each of the 14 modules is available as a dedicated subpath import. Import only what you use; bundlers automatically tree-shake the rest.
35
+ Each of the 16 modules is available as a dedicated subpath import. Import only what you use; bundlers automatically tree-shake the rest.
36
36
 
37
37
  ```ts
38
38
  // Pick exactly the modules you need
@@ -127,7 +127,7 @@ generateHtpasswdFile([{ username: "alice", password: "secret" }]);
127
127
 
128
128
  ```ts
129
129
  import { convertCase, slugify, countWords, generateWords, generateParagraphs,
130
- escapeString, htmlToMarkdown, applyOps } from "@utilix-tech/sdk/text";
130
+ escapeString, htmlToMarkdown, applyOps, detectPassiveVoice } from "@utilix-tech/sdk/text";
131
131
 
132
132
  convertCase("hello world", "camelCase"); // "helloWorld"
133
133
  convertCase("hello world", "PascalCase"); // "HelloWorld"
@@ -145,6 +145,9 @@ generateParagraphs(3); // lorem ipsum paragraphs
145
145
  applyOps(text, ["sort", "deduplicate", "trim"]);
146
146
 
147
147
  htmlToMarkdown("<h1>Hello</h1><p>World</p>");
148
+
149
+ // Flag passive-voice sentences, e.g. "was written", "were approved by the team"
150
+ detectPassiveVoice("The report was reviewed by the committee. They approved it.");
148
151
  ```
149
152
 
150
153
  ---
@@ -397,6 +400,22 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
397
400
 
398
401
  ---
399
402
 
403
+ ### `/media` — Image Header Parsing
404
+
405
+ ```ts
406
+ import { readImageInfo } from "@utilix-tech/sdk/media";
407
+
408
+ // Reads format, dimensions, bit depth, and alpha channel straight from
409
+ // file bytes — no decoding, no canvas, no image library.
410
+ const bytes = await fs.promises.readFile("photo.png");
411
+ readImageInfo(new Uint8Array(bytes));
412
+ // { format: "png", width: 1920, height: 1080, bitDepth: 8, colorType: "rgba", hasAlpha: true }
413
+ ```
414
+
415
+ Supports PNG, JPEG, GIF, WebP, and BMP.
416
+
417
+ ---
418
+
400
419
  ## All Modules at a Glance
401
420
 
402
421
  | Import | What it does |
@@ -415,6 +434,8 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
415
434
  | `@utilix-tech/sdk/color` | Hex/RGB/HSL/HSV/CMYK convert, palette generation, contrast check |
416
435
  | `@utilix-tech/sdk/css` | CSS gradient, box-shadow, border-radius, keyframe animation generators |
417
436
  | `@utilix-tech/sdk/misc` | SVG optimize/sanitize, file size format, Unicode inspector |
437
+ | `@utilix-tech/sdk/ai_agent` | Token estimate/trim, chunk text, extract URLs/JSON/keywords, sanitize HTML, flatten/merge JSON, dedupe lines, validate schema, PII/secret/injection detect |
438
+ | `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes — PNG, JPEG, GIF, WebP, BMP |
418
439
 
419
440
  ---
420
441
 
@@ -430,14 +451,30 @@ Both SDKs return plain JSON-serializable objects so you can share test fixtures
430
451
 
431
452
  ---
432
453
 
433
- ## Surface A vs Surface B
454
+ ## REST API
434
455
 
435
456
  **This package is Surface A** — everything runs in-process in your Node.js runtime. No outbound requests, no API key, no rate limits.
436
457
 
437
- A **Surface B REST API** (`api.utilix.tech`) is coming soon for use cases where you can't ship a Node.js or Python runtime. Same tools, callable from any language over HTTP.
458
+ The same 100+ tools are also available as a hosted **REST API** at `https://api.utilix.tech/v1` for environments where you can't ship a Node.js runtime, or for cross-language teams.
459
+
460
+ ```bash
461
+ curl -X POST https://api.utilix.tech/v1/tools/hash \
462
+ -H "Authorization: Bearer utx_live_..." \
463
+ -H "Content-Type: application/json" \
464
+ -d '{"input": "hello world", "algorithm": "sha256"}'
465
+ ```
466
+
467
+ - **Free**: 1,000 requests/day — no credit card required
468
+ - **Pro**: 10,000 requests/day — $9/month
469
+ - Try it live at **[utilix.tech/api](https://utilix.tech/api)** — no signup needed for the first 10 endpoints
470
+ - Get your API key at **[utilix.tech/dashboard](https://utilix.tech/dashboard)**
438
471
 
439
472
  ---
440
473
 
474
+ ## Publishing (maintainers)
475
+
476
+ CI publishes automatically via npm Trusted Publishing (OIDC) on every push to `main` that bumps `version`. Requires npm **>=11.5.1** — older versions (e.g. the npm bundled with Node 20) sign provenance but skip the OIDC auth exchange, so `npm publish` fails with a 404 as if unauthenticated.
477
+
441
478
  ## License
442
479
 
443
480
  MIT — see [LICENSE](./LICENSE) for details.
@@ -0,0 +1,134 @@
1
+ import { __export } from './chunk-MLKGABMK.js';
2
+
3
+ // src/tools/media.ts
4
+ var media_exports = {};
5
+ __export(media_exports, {
6
+ readImageInfo: () => readImageInfo
7
+ });
8
+ function readU32BE(bytes, offset) {
9
+ return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
10
+ }
11
+ function readI32LE(bytes, offset) {
12
+ return bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24;
13
+ }
14
+ var PNG_COLOR_TYPES = {
15
+ 0: "grayscale",
16
+ 2: "rgb",
17
+ 3: "palette",
18
+ 4: "grayscale+alpha",
19
+ 6: "rgba"
20
+ };
21
+ function readPng(bytes) {
22
+ if (bytes.length < 33) return { error: "Truncated PNG file" };
23
+ const width = readU32BE(bytes, 16);
24
+ const height = readU32BE(bytes, 20);
25
+ const bitDepth = bytes[24];
26
+ const colorTypeNum = bytes[25];
27
+ const colorType = PNG_COLOR_TYPES[colorTypeNum] ?? "unknown";
28
+ const hasAlpha = colorTypeNum === 4 || colorTypeNum === 6;
29
+ return { format: "png", width, height, bitDepth, colorType, hasAlpha };
30
+ }
31
+ function readGif(bytes) {
32
+ if (bytes.length < 10) return { error: "Truncated GIF file" };
33
+ const width = bytes[6] | bytes[7] << 8;
34
+ const height = bytes[8] | bytes[9] << 8;
35
+ return { format: "gif", width, height };
36
+ }
37
+ function readBmp(bytes) {
38
+ if (bytes.length < 30) return { error: "Truncated BMP file" };
39
+ const width = readI32LE(bytes, 18);
40
+ const heightRaw = readI32LE(bytes, 22);
41
+ const bitDepth = bytes[28] | bytes[29] << 8;
42
+ return { format: "bmp", width: Math.abs(width), height: Math.abs(heightRaw), bitDepth };
43
+ }
44
+ function readWebp(bytes) {
45
+ if (bytes.length < 16) return { error: "Truncated WebP file" };
46
+ const fourCC = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
47
+ if (fourCC === "VP8 ") {
48
+ if (bytes.length < 30) return { error: "Truncated WebP file" };
49
+ const width = (bytes[26] | bytes[27] << 8) & 16383;
50
+ const height = (bytes[28] | bytes[29] << 8) & 16383;
51
+ return { format: "webp", width, height };
52
+ }
53
+ if (fourCC === "VP8L") {
54
+ if (bytes.length < 25) return { error: "Truncated WebP file" };
55
+ const bits = bytes[21] | bytes[22] << 8 | bytes[23] << 16 | bytes[24] << 24;
56
+ const width = (bits & 16383) + 1;
57
+ const height = (bits >>> 14 & 16383) + 1;
58
+ const hasAlpha = !!(bits >>> 28 & 1);
59
+ return { format: "webp", width, height, hasAlpha };
60
+ }
61
+ if (fourCC === "VP8X") {
62
+ if (bytes.length < 30) return { error: "Truncated WebP file" };
63
+ const flags = bytes[20];
64
+ const hasAlpha = !!(flags & 16);
65
+ const width = (bytes[24] | bytes[25] << 8 | bytes[26] << 16) + 1;
66
+ const height = (bytes[27] | bytes[28] << 8 | bytes[29] << 16) + 1;
67
+ return { format: "webp", width, height, hasAlpha };
68
+ }
69
+ return { error: "Unrecognized WebP chunk format" };
70
+ }
71
+ var JPEG_SOF_MARKERS = /* @__PURE__ */ new Set([
72
+ 192,
73
+ 193,
74
+ 194,
75
+ 195,
76
+ 197,
77
+ 198,
78
+ 199,
79
+ 201,
80
+ 202,
81
+ 203,
82
+ 205,
83
+ 206,
84
+ 207
85
+ ]);
86
+ function readJpeg(bytes) {
87
+ let offset = 2;
88
+ while (offset < bytes.length - 1) {
89
+ if (bytes[offset] !== 255) {
90
+ offset++;
91
+ continue;
92
+ }
93
+ const marker = bytes[offset + 1];
94
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
95
+ offset += 2;
96
+ continue;
97
+ }
98
+ if (marker === 217) break;
99
+ if (offset + 3 >= bytes.length) break;
100
+ const segmentLength = bytes[offset + 2] << 8 | bytes[offset + 3];
101
+ if (JPEG_SOF_MARKERS.has(marker)) {
102
+ if (offset + 9 >= bytes.length) return { error: "Truncated JPEG SOF segment" };
103
+ const bitDepth = bytes[offset + 4];
104
+ const height = bytes[offset + 5] << 8 | bytes[offset + 6];
105
+ const width = bytes[offset + 7] << 8 | bytes[offset + 8];
106
+ const components = bytes[offset + 9];
107
+ const colorType = components === 1 ? "grayscale" : components === 3 ? "rgb" : components === 4 ? "cmyk" : "unknown";
108
+ return { format: "jpeg", width, height, bitDepth, colorType };
109
+ }
110
+ offset += 2 + segmentLength;
111
+ }
112
+ return { error: "No SOF marker found in JPEG (file may be corrupt)" };
113
+ }
114
+ function readImageInfo(bytes) {
115
+ if (bytes.length < 10) return { error: "File too small to determine format" };
116
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
117
+ return readPng(bytes);
118
+ }
119
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) {
120
+ return readGif(bytes);
121
+ }
122
+ if (bytes[0] === 66 && bytes[1] === 77) {
123
+ return readBmp(bytes);
124
+ }
125
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
126
+ return readWebp(bytes);
127
+ }
128
+ if (bytes[0] === 255 && bytes[1] === 216) {
129
+ return readJpeg(bytes);
130
+ }
131
+ return { error: "Unrecognized image format \u2014 supported: PNG, JPEG, GIF, WebP, BMP" };
132
+ }
133
+
134
+ export { media_exports, readImageInfo };
@@ -20,6 +20,7 @@ __export(text_exports, {
20
20
  csvToHtml: () => csvToHtml,
21
21
  csvToMarkdown: () => csvToMarkdown,
22
22
  detectDelimiter: () => detectDelimiter,
23
+ detectPassiveVoice: () => detectPassiveVoice,
23
24
  diffLines: () => diffLines,
24
25
  diffLinesSimple: () => diffLinesSimple,
25
26
  diffStats: () => diffStats,
@@ -2223,5 +2224,170 @@ function getMorseTiming(morse) {
2223
2224
  }
2224
2225
  return result;
2225
2226
  }
2227
+ var BE_VERBS = /* @__PURE__ */ new Set(["am", "is", "are", "was", "were", "be", "been", "being"]);
2228
+ var IRREGULAR_PARTICIPLES = /* @__PURE__ */ new Set([
2229
+ "awoken",
2230
+ "been",
2231
+ "become",
2232
+ "begun",
2233
+ "bent",
2234
+ "bet",
2235
+ "bitten",
2236
+ "bled",
2237
+ "blown",
2238
+ "born",
2239
+ "bought",
2240
+ "bound",
2241
+ "bred",
2242
+ "broken",
2243
+ "brought",
2244
+ "built",
2245
+ "burnt",
2246
+ "burst",
2247
+ "caught",
2248
+ "chosen",
2249
+ "clung",
2250
+ "come",
2251
+ "cost",
2252
+ "crept",
2253
+ "cut",
2254
+ "dealt",
2255
+ "dived",
2256
+ "done",
2257
+ "drawn",
2258
+ "dreamt",
2259
+ "driven",
2260
+ "drunk",
2261
+ "dug",
2262
+ "eaten",
2263
+ "fallen",
2264
+ "fed",
2265
+ "felt",
2266
+ "fit",
2267
+ "fled",
2268
+ "flown",
2269
+ "forbidden",
2270
+ "forgiven",
2271
+ "forgotten",
2272
+ "fought",
2273
+ "found",
2274
+ "frozen",
2275
+ "given",
2276
+ "gone",
2277
+ "gotten",
2278
+ "grown",
2279
+ "hidden",
2280
+ "held",
2281
+ "hung",
2282
+ "hurt",
2283
+ "kept",
2284
+ "knelt",
2285
+ "knit",
2286
+ "known",
2287
+ "laid",
2288
+ "lain",
2289
+ "led",
2290
+ "left",
2291
+ "lent",
2292
+ "let",
2293
+ "lit",
2294
+ "lost",
2295
+ "made",
2296
+ "meant",
2297
+ "met",
2298
+ "mistaken",
2299
+ "paid",
2300
+ "proven",
2301
+ "put",
2302
+ "quit",
2303
+ "read",
2304
+ "rid",
2305
+ "ridden",
2306
+ "risen",
2307
+ "run",
2308
+ "said",
2309
+ "sat",
2310
+ "sawn",
2311
+ "seen",
2312
+ "sent",
2313
+ "set",
2314
+ "sewn",
2315
+ "shaken",
2316
+ "shed",
2317
+ "shone",
2318
+ "shot",
2319
+ "shown",
2320
+ "shrunk",
2321
+ "shut",
2322
+ "sold",
2323
+ "sought",
2324
+ "sown",
2325
+ "spent",
2326
+ "spoken",
2327
+ "spread",
2328
+ "sprung",
2329
+ "stolen",
2330
+ "stood",
2331
+ "stuck",
2332
+ "stung",
2333
+ "struck",
2334
+ "strung",
2335
+ "sung",
2336
+ "sunk",
2337
+ "swept",
2338
+ "sworn",
2339
+ "swum",
2340
+ "swung",
2341
+ "taken",
2342
+ "taught",
2343
+ "thought",
2344
+ "thrown",
2345
+ "told",
2346
+ "torn",
2347
+ "understood",
2348
+ "upset",
2349
+ "woken",
2350
+ "won",
2351
+ "worn",
2352
+ "wound",
2353
+ "woven",
2354
+ "written"
2355
+ ]);
2356
+ function isPastParticiple(word) {
2357
+ const w = word.toLowerCase();
2358
+ if (IRREGULAR_PARTICIPLES.has(w)) return true;
2359
+ return w.length > 3 && /^[a-z]+ed$/.test(w);
2360
+ }
2361
+ function splitIntoSentences(text) {
2362
+ const matches = text.match(/[^.!?]+[.!?]*/g);
2363
+ if (!matches) return [];
2364
+ return matches.map((s) => s.trim()).filter((s) => s.length > 0);
2365
+ }
2366
+ function detectPassiveVoice(text) {
2367
+ const sentences = splitIntoSentences(text);
2368
+ const analyzed = sentences.map((sentence, index) => {
2369
+ const words = sentence.match(/[a-zA-Z']+/g) ?? [];
2370
+ const matches = [];
2371
+ for (let i = 0; i < words.length; i++) {
2372
+ if (!BE_VERBS.has(words[i].toLowerCase())) continue;
2373
+ let j = i + 1;
2374
+ let adverbCount = 0;
2375
+ while (j < words.length && adverbCount < 2 && /ly$/i.test(words[j]) && !isPastParticiple(words[j])) {
2376
+ j++;
2377
+ adverbCount++;
2378
+ }
2379
+ if (j < words.length && isPastParticiple(words[j])) {
2380
+ const hasAgent = words.slice(j + 1, j + 5).some((w) => w.toLowerCase() === "by");
2381
+ matches.push({ phrase: words.slice(i, j + 1).join(" "), hasAgent });
2382
+ i = j;
2383
+ }
2384
+ }
2385
+ return { index, text: sentence, isPassive: matches.length > 0, matches };
2386
+ });
2387
+ const totalSentences = analyzed.length;
2388
+ const passiveSentences = analyzed.filter((s) => s.isPassive).length;
2389
+ const passivePercent = totalSentences > 0 ? Math.round(passiveSentences / totalSentences * 1e3) / 10 : 0;
2390
+ return { sentences: analyzed, totalSentences, passiveSentences, passivePercent };
2391
+ }
2226
2392
 
2227
- export { CASE_FORMATS, DEFAULT_CONVERT_OPTIONS, ESCAPE_MODES, LINE_OPS, MORSE_CODE, addBorder, analyzeText, applyOp, applyOps, centerText, convertAll, convertCase, countWords, csvToHtml, csvToMarkdown, detectDelimiter, diffLines, diffLinesSimple, diffStats, escapeAll, escapeString, extractHeadings, formatReadingTime, generate, generateDiff, generateHtmlTable, generateMarkdownTable, generateParagraphs, generateSentences, generateWords, getAvailableFonts, getMorseForChar, getMorseTiming, htmlToMarkdown, isValidMorse, markdownToHtml, morseToText, numberToOrdinal, numberToRoman, numberToWords, ordinalWords, parseDiff, parseTableData, readabilityScore, renderMarkdown, romanToNumber, sentenceCount, slugify, slugifyAll, syllableCount, textToAscii, textToMorse, text_exports, unescapeString, wordFrequency };
2393
+ export { CASE_FORMATS, DEFAULT_CONVERT_OPTIONS, ESCAPE_MODES, LINE_OPS, MORSE_CODE, addBorder, analyzeText, applyOp, applyOps, centerText, convertAll, convertCase, countWords, csvToHtml, csvToMarkdown, detectDelimiter, detectPassiveVoice, diffLines, diffLinesSimple, diffStats, escapeAll, escapeString, extractHeadings, formatReadingTime, generate, generateDiff, generateHtmlTable, generateMarkdownTable, generateParagraphs, generateSentences, generateWords, getAvailableFonts, getMorseForChar, getMorseTiming, htmlToMarkdown, isValidMorse, markdownToHtml, morseToText, numberToOrdinal, numberToRoman, numberToWords, ordinalWords, parseDiff, parseTableData, readabilityScore, renderMarkdown, romanToNumber, sentenceCount, slugify, slugifyAll, syllableCount, textToAscii, textToMorse, text_exports, unescapeString, wordFrequency };