@stll/anonymize-cli 0.0.1-placeholder.0 → 1.4.9

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/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stella labs, s.r.o.
4
+
5
+ Permission is hereby granted, free of charge, to any
6
+ person obtaining a copy of this software and associated
7
+ documentation files (the "Software"), to deal in the
8
+ Software without restriction, including without
9
+ limitation the rights to use, copy, modify, merge,
10
+ publish, distribute, sublicense, and/or sell copies of
11
+ the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the
13
+ following conditions:
14
+
15
+ The above copyright notice and this permission notice
16
+ shall be included in all copies or substantial portions
17
+ of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
20
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
21
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
22
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
23
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
26
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27
+ DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @stll/anonymize-cli
2
+
3
+ Command-line PII detection and anonymization powered by
4
+ [`@stll/anonymize`](https://github.com/stella/anonymize).
5
+ Fully offline: no network calls, ever.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ # No install needed
11
+ echo "Contact Jan Novák at jan.novak@example.com" | bunx @stll/anonymize-cli
12
+ # Contact [PERSON_1] at [EMAIL_ADDRESS_1]
13
+
14
+ # Or with npx / a global install (bin name: anonymize)
15
+ npx @stll/anonymize-cli contract.txt > contract.anon.txt
16
+ ```
17
+
18
+ Reversible round-trip for LLM workflows — anonymize, send the
19
+ redacted text to a model, restore names in the answer:
20
+
21
+ ```bash
22
+ anonymize -k key.json -o redacted.txt input.txt
23
+ # ... send redacted.txt to the LLM, save reply as reply.txt ...
24
+ anonymize -d key.json reply.txt
25
+ ```
26
+
27
+ ## Options
28
+
29
+ | Flag | Meaning |
30
+ | ------------------------- | ----------------------------------------------- |
31
+ | `-o, --output <path>` | Output file, or directory for multiple inputs |
32
+ | `-m, --mode <mode>` | `replace` (reversible placeholders) or `redact` |
33
+ | `-k, --key <path>` | Write the redaction key JSON (replace mode) |
34
+ | `-d, --deanonymise <key>` | Restore text using a redaction key |
35
+ | `--labels <list>` | Entity labels to detect (default: all) |
36
+ | `--languages <list>` | Name-corpus languages, e.g. `cs,de,en` |
37
+ | `--countries <list>` | ISO 3166-1 alpha-2 deny-list/city scope |
38
+ | `--threshold <n>` | Minimum confidence score 0-1 (default 0.3) |
39
+ | `--redact-string <s>` | Replacement text in redact mode |
40
+ | `--json` | Emit entities + redacted text as JSON |
41
+ | `--quiet` | Suppress the stderr summary |
42
+
43
+ Run `anonymize --help` for the full reference, including the
44
+ `--json` schema and exit codes.
45
+
46
+ ## Scripting and agents
47
+
48
+ - Exit codes: `0` success, `1` runtime error, `2` usage error.
49
+ - The stderr summary contains entity-label counts only, never
50
+ detected text.
51
+ - The interactive locale prompt appears only when stdin and
52
+ stderr are TTYs and no scope flags are given; piped runs
53
+ never block.
54
+ - `--json` offsets are UTF-16 code-unit indexes into the input.
55
+
56
+ ## Standalone binary
57
+
58
+ `bun run compile` produces a self-contained executable (WASM
59
+ engine, dictionaries embedded as a gzip blob) that runs without
60
+ Node, Bun, or npm. Cross-compile with `--target=bun-linux-x64`
61
+ etc.
62
+
63
+ ## License
64
+
65
+ MIT
package/dist/cli.mjs ADDED
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env node
2
+ import * as anonymize from "@stll/anonymize";
3
+ import { ALL_DICTIONARY_IDS, DICTIONARY_META, loadCityDictionary, loadDictionary, loadNameDictionaries } from "@stll/anonymize-data";
4
+ import { parseArgs } from "node:util";
5
+ import { realpathSync } from "node:fs";
6
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
7
+ import { basename, join, resolve } from "node:path";
8
+ import { createInterface } from "node:readline/promises";
9
+ import { DEFAULT_ENTITY_LABELS } from "@stll/anonymize/constants";
10
+ //#region src/args.ts
11
+ const CLI_MODES = ["replace", "redact"];
12
+ const DEFAULT_THRESHOLD = .3;
13
+ const DEFAULT_REDACT_STRING = "[REDACTED]";
14
+ /** Invalid invocation; printed with usage hint, exit code 2. */
15
+ var UsageError = class extends Error {};
16
+ const HELP = `Usage: anonymize [options] [file ...]
17
+
18
+ Detect and anonymize PII in text. Reads the given files, or stdin
19
+ when no files are given. Writes to stdout, or to --output.
20
+ All processing is local; the CLI makes no network calls.
21
+
22
+ Options:
23
+ -o, --output <path> Output file, or directory when multiple
24
+ input files are given
25
+ -m, --mode <mode> "replace" (reversible [PERSON_1]
26
+ placeholders) or "redact"
27
+ (default: replace)
28
+ -k, --key <path> Write the redaction key as JSON
29
+ (single input, replace mode)
30
+ -d, --deanonymise <path> Restore redacted text using the
31
+ redaction key at <path>
32
+ --labels <list> Comma-separated entity labels to detect
33
+ (default: all)
34
+ --languages <list> Name-corpus languages, e.g. "cs,de,en"
35
+ (default: all bundled)
36
+ --countries <list> ISO 3166-1 alpha-2 codes scoping deny
37
+ lists and city data, e.g. "CZ,DE,GB"
38
+ (default: all deny lists; city data
39
+ for a 30-country default set)
40
+ --threshold <n> Minimum confidence score, 0-1
41
+ (default: ${DEFAULT_THRESHOLD})
42
+ --redact-string <s> Replacement text in redact mode
43
+ (default: "${DEFAULT_REDACT_STRING}")
44
+ --json Emit JSON (entities + redacted text) to
45
+ stdout (single input only)
46
+ --quiet Suppress the summary on stderr
47
+ -h, --help Show this help
48
+ -v, --version Show the version
49
+
50
+ Interactive prompt:
51
+ When run on files from a terminal without --countries or
52
+ --languages, the CLI asks once which country scope to load.
53
+ Piped stdin/stderr or --quiet skips the prompt, so scripts
54
+ and agents never block on input.
55
+
56
+ Exit codes:
57
+ 0 success
58
+ 1 runtime error (message on stderr)
59
+ 2 usage error (message on stderr)
60
+
61
+ JSON output (--json):
62
+ { "entityCount": number,
63
+ "entities": [{ "start": number, "end": number,
64
+ "label": string, "text": string,
65
+ "score": number, "source": string }],
66
+ "redactedText": string }
67
+ Offsets are UTF-16 code-unit indexes into the input.
68
+ The stderr summary contains entity counts only, never
69
+ the detected text.
70
+
71
+ Examples:
72
+ anonymize contract.txt > contract.anon.txt
73
+ anonymize -k contract.key.json -o contract.anon.txt contract.txt
74
+ anonymize -d contract.key.json contract.anon.txt
75
+ cat notes.md | anonymize --countries CZ,SK --languages cs,sk
76
+ anonymize --json --quiet input.txt | jq '.entities[].label'
77
+ `;
78
+ const splitList = (value) => [...new Set(value.split(",").map((part) => part.trim()).filter((part) => part.length > 0))];
79
+ const parseThreshold = (raw) => {
80
+ const value = Number(raw);
81
+ if (!Number.isFinite(value) || value < 0 || value > 1) throw new UsageError(`--threshold must be a number between 0 and 1, got "${raw}"`);
82
+ return value;
83
+ };
84
+ const parseMode = (raw) => {
85
+ const mode = CLI_MODES.find((candidate) => candidate === raw);
86
+ if (!mode) throw new UsageError(`--mode must be one of: ${CLI_MODES.join(", ")}; got "${raw}"`);
87
+ return mode;
88
+ };
89
+ const COUNTRY_CODE_RE = /^[A-Za-z]{2}$/;
90
+ const parseCountries = (raw) => {
91
+ const countries = [...new Set(splitList(raw).map((code) => code.toUpperCase()))];
92
+ const invalid = countries.find((code) => !COUNTRY_CODE_RE.test(code));
93
+ if (invalid) throw new UsageError(`--countries expects ISO 3166-1 alpha-2 codes (e.g. "CZ,DE"), got "${invalid}"`);
94
+ return countries;
95
+ };
96
+ const parseCliArgs = (argv) => {
97
+ let parsed;
98
+ try {
99
+ parsed = parseArgs({
100
+ ...PARSE_CONFIG,
101
+ args: argv
102
+ });
103
+ } catch (err) {
104
+ throw new UsageError(err instanceof Error ? err.message : String(err));
105
+ }
106
+ const { values, positionals } = parsed;
107
+ return {
108
+ files: positionals,
109
+ output: values.output,
110
+ mode: values.mode === void 0 ? "replace" : parseMode(values.mode),
111
+ keyPath: values.key,
112
+ deanonymiseKeyPath: values.deanonymise,
113
+ labels: values.labels === void 0 ? void 0 : splitList(values.labels),
114
+ languages: values.languages === void 0 ? void 0 : splitList(values.languages),
115
+ countries: values.countries === void 0 ? void 0 : parseCountries(values.countries),
116
+ threshold: values.threshold === void 0 ? DEFAULT_THRESHOLD : parseThreshold(values.threshold),
117
+ redactString: values["redact-string"] ?? "[REDACTED]",
118
+ json: values.json === true,
119
+ quiet: values.quiet === true,
120
+ help: values.help === true,
121
+ version: values.version === true
122
+ };
123
+ };
124
+ const PARSE_CONFIG = {
125
+ allowPositionals: true,
126
+ strict: true,
127
+ options: {
128
+ output: {
129
+ type: "string",
130
+ short: "o"
131
+ },
132
+ mode: {
133
+ type: "string",
134
+ short: "m"
135
+ },
136
+ key: {
137
+ type: "string",
138
+ short: "k"
139
+ },
140
+ deanonymise: {
141
+ type: "string",
142
+ short: "d"
143
+ },
144
+ labels: { type: "string" },
145
+ languages: { type: "string" },
146
+ countries: { type: "string" },
147
+ threshold: { type: "string" },
148
+ "redact-string": { type: "string" },
149
+ json: { type: "boolean" },
150
+ quiet: { type: "boolean" },
151
+ help: {
152
+ type: "boolean",
153
+ short: "h"
154
+ },
155
+ version: {
156
+ type: "boolean",
157
+ short: "v"
158
+ }
159
+ }
160
+ };
161
+ //#endregion
162
+ //#region src/dictionary-scope.ts
163
+ const NAME_DICTIONARY_PREFIXES = ["names/first/", "names/surnames/"];
164
+ /** Language code of a name dictionary id, or null. */
165
+ const nameLanguageOfDictionary = (id) => {
166
+ const prefix = NAME_DICTIONARY_PREFIXES.find((p) => id.startsWith(p));
167
+ return prefix ? id.slice(prefix.length) : null;
168
+ };
169
+ //#endregion
170
+ //#region src/dictionaries.ts
171
+ /**
172
+ * Countries with bundled city dictionaries that are
173
+ * loaded when no --countries scope is given.
174
+ */
175
+ const DEFAULT_CITY_COUNTRIES = [
176
+ "AT",
177
+ "AU",
178
+ "BE",
179
+ "BG",
180
+ "BR",
181
+ "CA",
182
+ "CH",
183
+ "CZ",
184
+ "DE",
185
+ "DK",
186
+ "ES",
187
+ "FI",
188
+ "FR",
189
+ "GB",
190
+ "GR",
191
+ "HR",
192
+ "HU",
193
+ "IE",
194
+ "IT",
195
+ "LU",
196
+ "NL",
197
+ "NO",
198
+ "NZ",
199
+ "PL",
200
+ "PT",
201
+ "RO",
202
+ "SE",
203
+ "SI",
204
+ "SK",
205
+ "US"
206
+ ];
207
+ const availableNameLanguages = () => ALL_DICTIONARY_IDS.filter((id) => id.startsWith(NAME_DICTIONARY_PREFIXES[0])).map((id) => id.slice(NAME_DICTIONARY_PREFIXES[0].length));
208
+ const validateLanguages = (languages) => {
209
+ const available = availableNameLanguages();
210
+ const invalid = languages.find((lang) => !available.includes(lang));
211
+ if (invalid) throw new UsageError(`--languages: no name dictionary for "${invalid}"; available: ${available.join(", ")}`);
212
+ return languages;
213
+ };
214
+ /**
215
+ * Load the bundled @stll/anonymize-data dictionaries,
216
+ * scoped to the requested languages and countries.
217
+ */
218
+ const loadCliDictionaries = async ({ languages, countries }) => {
219
+ const nameLanguages = languages === void 0 ? void 0 : validateLanguages(languages);
220
+ const denyIds = ALL_DICTIONARY_IDS.filter((id) => {
221
+ const meta = DICTIONARY_META[id];
222
+ if (countries && meta.country !== null && !countries.includes(meta.country)) return false;
223
+ const nameLang = nameLanguageOfDictionary(id);
224
+ if (nameLang !== null && nameLanguages !== void 0) return nameLanguages.includes(nameLang);
225
+ return true;
226
+ });
227
+ const cityCountries = countries ?? DEFAULT_CITY_COUNTRIES;
228
+ const [names, denyEntries, cityEntries] = await Promise.all([
229
+ loadNameDictionaries(nameLanguages),
230
+ Promise.all(denyIds.map(async (id) => ({
231
+ id,
232
+ entries: await loadDictionary(id)
233
+ }))),
234
+ Promise.all(cityCountries.map(async (country) => ({
235
+ country,
236
+ entries: await loadCityDictionary(country)
237
+ })))
238
+ ]);
239
+ const denyList = {};
240
+ const denyListMeta = {};
241
+ for (const { id, entries } of denyEntries) {
242
+ denyList[id] = entries;
243
+ denyListMeta[id] = DICTIONARY_META[id];
244
+ }
245
+ const citiesByCountry = {};
246
+ for (const { country, entries } of cityEntries) if (entries.length > 0) citiesByCountry[country] = entries;
247
+ return {
248
+ firstNames: names.firstNames,
249
+ surnames: names.surnames,
250
+ denyList,
251
+ denyListMeta,
252
+ citiesByCountry
253
+ };
254
+ };
255
+ //#endregion
256
+ //#region package.json
257
+ var version = "1.4.9";
258
+ //#endregion
259
+ //#region src/main.ts
260
+ const cliVersion = () => version;
261
+ /**
262
+ * Filesystem identity of a path: realpath when it exists
263
+ * (so symlinks to the same file compare equal), lexical
264
+ * resolution otherwise (the file may not exist yet).
265
+ */
266
+ const canonicalPath = (path) => {
267
+ try {
268
+ return realpathSync(path);
269
+ } catch {
270
+ return resolve(path);
271
+ }
272
+ };
273
+ const readStdin = async () => {
274
+ process.stdin.setEncoding("utf8");
275
+ let text = "";
276
+ for await (const chunk of process.stdin) text += chunk;
277
+ return text;
278
+ };
279
+ const readInputs = async (files) => {
280
+ if (files.length === 0) {
281
+ if (process.stdin.isTTY) throw new UsageError("no input files and stdin is a terminal (see --help)");
282
+ return [{
283
+ path: null,
284
+ text: await readStdin()
285
+ }];
286
+ }
287
+ return Promise.all(files.map(async (path) => ({
288
+ path,
289
+ text: await readFile(path, "utf8")
290
+ })));
291
+ };
292
+ const validateLabels = (labels) => {
293
+ const known = DEFAULT_ENTITY_LABELS;
294
+ const invalid = labels.find((label) => !known.includes(label));
295
+ if (invalid) throw new UsageError(`--labels: unknown label "${invalid}"; available: ${DEFAULT_ENTITY_LABELS.join(", ")}`);
296
+ return [...labels];
297
+ };
298
+ const buildPipelineConfig = async (opts, loadDictionaries) => {
299
+ const dictionaries = await loadDictionaries({
300
+ languages: opts.languages,
301
+ countries: opts.countries
302
+ });
303
+ return {
304
+ threshold: opts.threshold,
305
+ enableTriggerPhrases: true,
306
+ enableRegex: true,
307
+ enableLegalForms: true,
308
+ enableNameCorpus: true,
309
+ ...opts.languages === void 0 ? {} : { nameCorpusLanguages: [...opts.languages] },
310
+ enableDenyList: true,
311
+ ...opts.countries === void 0 ? {} : { denyListCountries: [...opts.countries] },
312
+ enableGazetteer: false,
313
+ enableCountries: true,
314
+ enableNer: false,
315
+ enableConfidenceBoost: true,
316
+ enableCoreference: true,
317
+ enableZoneClassification: true,
318
+ enableHotwordRules: true,
319
+ labels: opts.labels === void 0 ? [...DEFAULT_ENTITY_LABELS] : validateLabels(opts.labels),
320
+ workspaceId: "cli",
321
+ dictionaries
322
+ };
323
+ };
324
+ const buildOperatorConfig = (opts, entities) => {
325
+ const operators = {};
326
+ if (opts.mode === "redact") for (const entity of entities) operators[entity.label] = "redact";
327
+ return {
328
+ operators,
329
+ redactString: opts.redactString
330
+ };
331
+ };
332
+ const writeOutput = async (path, content) => {
333
+ if (path === void 0) {
334
+ process.stdout.write(content);
335
+ return;
336
+ }
337
+ await writeFile(path, content, "utf8");
338
+ };
339
+ const parseRedactionKey = (raw) => {
340
+ let parsed;
341
+ try {
342
+ parsed = JSON.parse(raw);
343
+ } catch {
344
+ throw new UsageError("redaction key is not valid JSON");
345
+ }
346
+ if (typeof parsed !== "object" || parsed === null || !("entries" in parsed)) throw new UsageError("redaction key must be an object with an \"entries\" field");
347
+ const { entries } = parsed;
348
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) throw new UsageError("redaction key \"entries\" must be an object");
349
+ const map = /* @__PURE__ */ new Map();
350
+ for (const [placeholder, entry] of Object.entries(entries)) {
351
+ if (typeof entry?.original !== "string") throw new UsageError(`redaction key entry "${placeholder}" has no original text`);
352
+ map.set(placeholder, entry.original);
353
+ }
354
+ return map;
355
+ };
356
+ /**
357
+ * Ask for a country scope when running interactively on
358
+ * files with no scope flags. Skipped for piped stdin so
359
+ * the CLI stays scriptable.
360
+ */
361
+ const shouldPromptForScope = (opts, tty) => opts.countries === void 0 && opts.languages === void 0 && !opts.quiet && opts.files.length > 0 && tty.stdinIsTTY && tty.stderrIsTTY;
362
+ const promptForCountries = async () => {
363
+ const rl = createInterface({
364
+ input: process.stdin,
365
+ output: process.stderr
366
+ });
367
+ try {
368
+ const trimmed = (await rl.question("Country scope (ISO codes like CZ,DE,GB; Enter loads all): ")).trim();
369
+ return trimmed === "" ? void 0 : parseCountries(trimmed);
370
+ } finally {
371
+ rl.close();
372
+ }
373
+ };
374
+ const runDeanonymise = async (opts, api) => {
375
+ if (opts.keyPath !== void 0) throw new UsageError("--key cannot be combined with --deanonymise");
376
+ const keyPath = opts.deanonymiseKeyPath;
377
+ if (keyPath === void 0) throw new UsageError("missing redaction key path");
378
+ const redactionMap = parseRedactionKey(await readFile(keyPath, "utf8"));
379
+ const inputs = await readInputs(opts.files);
380
+ if (inputs.length > 1) throw new UsageError("--deanonymise accepts a single input");
381
+ const input = inputs[0];
382
+ if (!input) throw new UsageError("no input to deanonymise");
383
+ if (opts.output !== void 0) guardWriteTargets(input.path === null ? [] : [input.path], [{
384
+ path: opts.output,
385
+ flag: "--output"
386
+ }]);
387
+ await writeOutput(opts.output, api.deanonymise(input.text, redactionMap));
388
+ };
389
+ const outputPathFor = (input, opts, multi) => {
390
+ if (opts.output === void 0) return void 0;
391
+ if (!multi) return opts.output;
392
+ if (input.path === null) throw new UsageError("stdin cannot be combined with multiple files");
393
+ return join(opts.output, basename(input.path));
394
+ };
395
+ /**
396
+ * Reject any write target (output or key file) whose
397
+ * filesystem identity collides with an input file or with
398
+ * another write target. Symlinks count as collisions.
399
+ */
400
+ const guardWriteTargets = (inputPaths, writeTargets) => {
401
+ const inputs = new Set(inputPaths.map(canonicalPath));
402
+ const seen = /* @__PURE__ */ new Map();
403
+ for (const target of writeTargets) {
404
+ const canonical = canonicalPath(target.path);
405
+ if (inputs.has(canonical)) throw new UsageError(`refusing to overwrite input file "${target.path}" (${target.flag})`);
406
+ const clash = seen.get(canonical);
407
+ if (clash !== void 0) throw new UsageError(`${target.flag} "${target.path}" collides with ${clash}`);
408
+ seen.set(canonical, `${target.flag} "${target.path}"`);
409
+ }
410
+ };
411
+ const summarize = (entities) => {
412
+ const counts = /* @__PURE__ */ new Map();
413
+ for (const entity of entities) counts.set(entity.label, (counts.get(entity.label) ?? 0) + 1);
414
+ const parts = [...counts.entries()].toSorted((a, b) => b[1] - a[1]).map(([label, count]) => `${label}: ${count}`);
415
+ return parts.length > 0 ? parts.join(", ") : "none";
416
+ };
417
+ const runAnonymise = async (opts, { api, loadDictionaries }) => {
418
+ const multi = opts.files.length > 1;
419
+ if (multi && opts.output === void 0) throw new UsageError("multiple input files require --output <directory>");
420
+ if (multi && opts.keyPath !== void 0) throw new UsageError("--key works with a single input only");
421
+ if (multi && opts.json) throw new UsageError("--json works with a single input only");
422
+ if (opts.keyPath !== void 0 && opts.mode !== "replace") throw new UsageError("--key requires --mode \"replace\"");
423
+ const scoped = shouldPromptForScope(opts, {
424
+ stdinIsTTY: process.stdin.isTTY === true,
425
+ stderrIsTTY: process.stderr.isTTY === true
426
+ }) ? {
427
+ ...opts,
428
+ countries: await promptForCountries()
429
+ } : opts;
430
+ const inputs = await readInputs(scoped.files);
431
+ const outputPaths = inputs.map((input) => outputPathFor(input, opts, multi));
432
+ const writeTargets = [];
433
+ for (const path of outputPaths) if (path !== void 0) writeTargets.push({
434
+ path,
435
+ flag: "--output"
436
+ });
437
+ if (opts.keyPath !== void 0) writeTargets.push({
438
+ path: opts.keyPath,
439
+ flag: "--key"
440
+ });
441
+ guardWriteTargets(inputs.flatMap((input) => input.path === null ? [] : [input.path]), writeTargets);
442
+ const config = await buildPipelineConfig(scoped, loadDictionaries);
443
+ const context = api.createPipelineContext();
444
+ if (multi && opts.output !== void 0) await mkdir(opts.output, { recursive: true });
445
+ for (const [index, input] of inputs.entries()) {
446
+ const outputPath = outputPaths[index];
447
+ const entities = await api.runPipeline({
448
+ fullText: input.text,
449
+ config,
450
+ gazetteerEntries: [],
451
+ context
452
+ });
453
+ const result = api.redactText(input.text, entities, buildOperatorConfig(opts, entities), context);
454
+ if (opts.json) {
455
+ const payload = {
456
+ entityCount: result.entityCount,
457
+ entities,
458
+ redactedText: result.redactedText
459
+ };
460
+ await writeOutput(outputPath, `${JSON.stringify(payload, null, 2)}\n`);
461
+ } else await writeOutput(outputPath, result.redactedText);
462
+ if (opts.keyPath !== void 0) await writeFile(opts.keyPath, api.exportRedactionKey(result.redactionMap, result.operatorMap), "utf8");
463
+ if (!opts.quiet) {
464
+ const source = input.path ?? "stdin";
465
+ process.stderr.write(`anonymize: ${source}: ${summarize(entities)}\n`);
466
+ }
467
+ }
468
+ };
469
+ const dispatch = async (engine) => {
470
+ const opts = parseCliArgs(process.argv.slice(2));
471
+ if (opts.help) {
472
+ process.stdout.write(HELP);
473
+ return;
474
+ }
475
+ if (opts.version) {
476
+ process.stdout.write(`${cliVersion()}\n`);
477
+ return;
478
+ }
479
+ if (opts.deanonymiseKeyPath !== void 0) {
480
+ await runDeanonymise(opts, engine.api);
481
+ return;
482
+ }
483
+ await runAnonymise(opts, engine);
484
+ };
485
+ /**
486
+ * Run the CLI against the given engine and set the
487
+ * process exit code (0 ok, 1 runtime error, 2 usage).
488
+ */
489
+ const runCli = async (engine) => {
490
+ try {
491
+ await dispatch(engine);
492
+ } catch (err) {
493
+ if (err instanceof UsageError) {
494
+ process.stderr.write(`anonymize: ${err.message}\n`);
495
+ process.stderr.write(`Try "anonymize --help" for usage.\n`);
496
+ process.exitCode = 2;
497
+ } else {
498
+ const message = err instanceof Error ? err.message : String(err);
499
+ process.stderr.write(`anonymize: ${message}\n`);
500
+ process.exitCode = 1;
501
+ }
502
+ }
503
+ };
504
+ //#endregion
505
+ //#region src/cli.ts
506
+ await runCli({
507
+ api: anonymize,
508
+ loadDictionaries: loadCliDictionaries
509
+ });
510
+ //#endregion
511
+ export {};
512
+
513
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":["pkg.version"],"sources":["../src/args.ts","../src/dictionary-scope.ts","../src/dictionaries.ts","../package.json","../src/main.ts","../src/cli.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\n\nexport const CLI_MODES = [\"replace\", \"redact\"] as const;\nexport type CliMode = (typeof CLI_MODES)[number];\n\nexport const DEFAULT_THRESHOLD = 0.3;\nexport const DEFAULT_REDACT_STRING = \"[REDACTED]\";\n\n/** Invalid invocation; printed with usage hint, exit code 2. */\nexport class UsageError extends Error {}\n\nexport type CliOptions = {\n files: string[];\n output?: string | undefined;\n mode: CliMode;\n keyPath?: string | undefined;\n deanonymiseKeyPath?: string | undefined;\n labels?: string[] | undefined;\n languages?: string[] | undefined;\n countries?: string[] | undefined;\n threshold: number;\n redactString: string;\n json: boolean;\n quiet: boolean;\n help: boolean;\n version: boolean;\n};\n\nexport const HELP = `Usage: anonymize [options] [file ...]\n\nDetect and anonymize PII in text. Reads the given files, or stdin\nwhen no files are given. Writes to stdout, or to --output.\nAll processing is local; the CLI makes no network calls.\n\nOptions:\n -o, --output <path> Output file, or directory when multiple\n input files are given\n -m, --mode <mode> \"replace\" (reversible [PERSON_1]\n placeholders) or \"redact\"\n (default: replace)\n -k, --key <path> Write the redaction key as JSON\n (single input, replace mode)\n -d, --deanonymise <path> Restore redacted text using the\n redaction key at <path>\n --labels <list> Comma-separated entity labels to detect\n (default: all)\n --languages <list> Name-corpus languages, e.g. \"cs,de,en\"\n (default: all bundled)\n --countries <list> ISO 3166-1 alpha-2 codes scoping deny\n lists and city data, e.g. \"CZ,DE,GB\"\n (default: all deny lists; city data\n for a 30-country default set)\n --threshold <n> Minimum confidence score, 0-1\n (default: ${DEFAULT_THRESHOLD})\n --redact-string <s> Replacement text in redact mode\n (default: \"${DEFAULT_REDACT_STRING}\")\n --json Emit JSON (entities + redacted text) to\n stdout (single input only)\n --quiet Suppress the summary on stderr\n -h, --help Show this help\n -v, --version Show the version\n\nInteractive prompt:\n When run on files from a terminal without --countries or\n --languages, the CLI asks once which country scope to load.\n Piped stdin/stderr or --quiet skips the prompt, so scripts\n and agents never block on input.\n\nExit codes:\n 0 success\n 1 runtime error (message on stderr)\n 2 usage error (message on stderr)\n\nJSON output (--json):\n { \"entityCount\": number,\n \"entities\": [{ \"start\": number, \"end\": number,\n \"label\": string, \"text\": string,\n \"score\": number, \"source\": string }],\n \"redactedText\": string }\n Offsets are UTF-16 code-unit indexes into the input.\n The stderr summary contains entity counts only, never\n the detected text.\n\nExamples:\n anonymize contract.txt > contract.anon.txt\n anonymize -k contract.key.json -o contract.anon.txt contract.txt\n anonymize -d contract.key.json contract.anon.txt\n cat notes.md | anonymize --countries CZ,SK --languages cs,sk\n anonymize --json --quiet input.txt | jq '.entities[].label'\n`;\n\nconst splitList = (value: string): string[] => [\n ...new Set(\n value\n .split(\",\")\n .map((part) => part.trim())\n .filter((part) => part.length > 0),\n ),\n];\n\nconst parseThreshold = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n throw new UsageError(\n `--threshold must be a number between 0 and 1, got \"${raw}\"`,\n );\n }\n return value;\n};\n\nconst parseMode = (raw: string): CliMode => {\n const mode = CLI_MODES.find((candidate) => candidate === raw);\n if (!mode) {\n throw new UsageError(\n `--mode must be one of: ${CLI_MODES.join(\", \")}; got \"${raw}\"`,\n );\n }\n return mode;\n};\n\nconst COUNTRY_CODE_RE = /^[A-Za-z]{2}$/;\n\nexport const parseCountries = (raw: string): string[] => {\n const countries = [\n ...new Set(splitList(raw).map((code) => code.toUpperCase())),\n ];\n const invalid = countries.find((code) => !COUNTRY_CODE_RE.test(code));\n if (invalid) {\n throw new UsageError(\n `--countries expects ISO 3166-1 alpha-2 codes (e.g. \"CZ,DE\"), got \"${invalid}\"`,\n );\n }\n return countries;\n};\n\nexport const parseCliArgs = (argv: string[]): CliOptions => {\n let parsed: ReturnType<typeof parseArgs<typeof PARSE_CONFIG>>;\n try {\n parsed = parseArgs({ ...PARSE_CONFIG, args: argv });\n } catch (err) {\n throw new UsageError(err instanceof Error ? err.message : String(err));\n }\n const { values, positionals } = parsed;\n\n return {\n files: positionals,\n output: values.output,\n mode: values.mode === undefined ? \"replace\" : parseMode(values.mode),\n keyPath: values.key,\n deanonymiseKeyPath: values.deanonymise,\n labels: values.labels === undefined ? undefined : splitList(values.labels),\n languages:\n values.languages === undefined ? undefined : splitList(values.languages),\n countries:\n values.countries === undefined\n ? undefined\n : parseCountries(values.countries),\n threshold:\n values.threshold === undefined\n ? DEFAULT_THRESHOLD\n : parseThreshold(values.threshold),\n redactString: values[\"redact-string\"] ?? DEFAULT_REDACT_STRING,\n json: values.json === true,\n quiet: values.quiet === true,\n help: values.help === true,\n version: values.version === true,\n };\n};\n\nconst PARSE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: {\n output: { type: \"string\", short: \"o\" },\n mode: { type: \"string\", short: \"m\" },\n key: { type: \"string\", short: \"k\" },\n deanonymise: { type: \"string\", short: \"d\" },\n labels: { type: \"string\" },\n languages: { type: \"string\" },\n countries: { type: \"string\" },\n threshold: { type: \"string\" },\n \"redact-string\": { type: \"string\" },\n json: { type: \"boolean\" },\n quiet: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n },\n} as const;\n","/* Pure helpers shared by the npm and embedded dictionary\n * loaders. Must stay free of @stll/anonymize-data imports\n * so the compiled binary's bundle excludes the raw JSON\n * dictionary modules. */\nimport type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\n\nimport { UsageError } from \"./args\";\n\nexport const NAME_DICTIONARY_PREFIXES = [\n \"names/first/\",\n \"names/surnames/\",\n] as const;\n\n/** Language code of a name dictionary id, or null. */\nexport const nameLanguageOfDictionary = (id: string): string | null => {\n const prefix = NAME_DICTIONARY_PREFIXES.find((p) => id.startsWith(p));\n return prefix ? id.slice(prefix.length) : null;\n};\n\nexport type DictionaryScope = {\n languages?: readonly string[] | undefined;\n countries?: readonly string[] | undefined;\n};\n\nconst pickKeys = <T>(\n record: Record<string, T>,\n keep: (key: string) => boolean,\n): Record<string, T> => {\n const result: Record<string, T> = {};\n for (const [key, value] of Object.entries(record)) {\n if (keep(key)) result[key] = value;\n }\n return result;\n};\n\n/** Dictionaries with every section present (possibly empty). */\nexport type ScopedDictionaries = Dictionaries & {\n firstNames: Record<string, readonly string[]>;\n surnames: Record<string, readonly string[]>;\n denyList: Record<string, readonly string[]>;\n denyListMeta: Record<string, DictionaryMeta>;\n citiesByCountry: Record<string, readonly string[]>;\n};\n\n/**\n * Scope a fully loaded dictionary set to the requested\n * languages and countries. Mirrors the pre-load scoping\n * the npm loader does in dictionaries.ts; used by the\n * embedded loader, which always starts from the full set.\n */\nexport const filterDictionaries = (\n all: Dictionaries,\n { languages, countries }: DictionaryScope,\n): ScopedDictionaries => {\n const firstNames = all.firstNames ?? {};\n const surnames = all.surnames ?? {};\n const allDenyList = all.denyList ?? {};\n const allDenyListMeta = all.denyListMeta ?? {};\n\n if (languages !== undefined) {\n const available = Object.keys(firstNames);\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n }\n const keepLanguage = (lang: string): boolean =>\n languages === undefined || languages.includes(lang);\n const keepCountry = (country: string | null): boolean =>\n countries === undefined || country === null || countries.includes(country);\n\n const denyListMeta: Record<string, DictionaryMeta> = {};\n const denyList: Record<string, readonly string[]> = {};\n for (const [id, meta] of Object.entries(allDenyListMeta)) {\n if (!keepCountry(meta.country)) continue;\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && !keepLanguage(nameLang)) continue;\n const entries = allDenyList[id];\n if (entries === undefined) continue;\n denyListMeta[id] = meta;\n denyList[id] = entries;\n }\n\n return {\n firstNames: pickKeys(firstNames, keepLanguage),\n surnames: pickKeys(surnames, keepLanguage),\n denyList,\n denyListMeta,\n citiesByCountry: pickKeys(all.citiesByCountry ?? {}, (country) =>\n keepCountry(country),\n ),\n };\n};\n","import type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\nimport {\n ALL_DICTIONARY_IDS,\n DICTIONARY_META,\n loadCityDictionary,\n loadDictionary,\n loadNameDictionaries,\n type NameLanguage,\n} from \"@stll/anonymize-data\";\n\nimport { UsageError } from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\nimport {\n NAME_DICTIONARY_PREFIXES,\n nameLanguageOfDictionary,\n} from \"./dictionary-scope\";\n\n/**\n * Countries with bundled city dictionaries that are\n * loaded when no --countries scope is given.\n */\nconst DEFAULT_CITY_COUNTRIES = [\n \"AT\",\n \"AU\",\n \"BE\",\n \"BG\",\n \"BR\",\n \"CA\",\n \"CH\",\n \"CZ\",\n \"DE\",\n \"DK\",\n \"ES\",\n \"FI\",\n \"FR\",\n \"GB\",\n \"GR\",\n \"HR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LU\",\n \"NL\",\n \"NO\",\n \"NZ\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SE\",\n \"SI\",\n \"SK\",\n \"US\",\n] as const;\n\nconst availableNameLanguages = (): readonly string[] =>\n ALL_DICTIONARY_IDS.filter((id) =>\n id.startsWith(NAME_DICTIONARY_PREFIXES[0]),\n ).map((id) => id.slice(NAME_DICTIONARY_PREFIXES[0].length));\n\nconst validateLanguages = (\n languages: readonly string[],\n): readonly NameLanguage[] => {\n const available = availableNameLanguages();\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n // SAFETY: every entry was checked against the bundled\n // name dictionary ids, which define NameLanguage.\n return languages as readonly NameLanguage[];\n};\n\nexport type LoadCliDictionariesOptions = DictionaryScope;\n\n/**\n * Load the bundled @stll/anonymize-data dictionaries,\n * scoped to the requested languages and countries.\n */\nexport const loadCliDictionaries = async ({\n languages,\n countries,\n}: LoadCliDictionariesOptions): Promise<Dictionaries> => {\n const nameLanguages =\n languages === undefined ? undefined : validateLanguages(languages);\n\n const denyIds = ALL_DICTIONARY_IDS.filter((id) => {\n const meta = DICTIONARY_META[id];\n if (\n countries &&\n meta.country !== null &&\n !countries.includes(meta.country)\n ) {\n return false;\n }\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && nameLanguages !== undefined) {\n return nameLanguages.includes(\n // SAFETY: nameLang comes from a bundled dictionary\n // id, which defines NameLanguage.\n nameLang as NameLanguage,\n );\n }\n return true;\n });\n\n const cityCountries = countries ?? DEFAULT_CITY_COUNTRIES;\n\n const [names, denyEntries, cityEntries] = await Promise.all([\n loadNameDictionaries(nameLanguages),\n Promise.all(\n denyIds.map(async (id) => ({ id, entries: await loadDictionary(id) })),\n ),\n Promise.all(\n cityCountries.map(async (country) => ({\n country,\n entries: await loadCityDictionary(country),\n })),\n ),\n ]);\n\n const denyList: Record<string, readonly string[]> = {};\n const denyListMeta: Record<string, DictionaryMeta> = {};\n for (const { id, entries } of denyEntries) {\n denyList[id] = entries;\n // SAFETY: anonymize-data categories match\n // DenyListCategory at runtime.\n denyListMeta[id] = DICTIONARY_META[id] as DictionaryMeta;\n }\n\n const citiesByCountry: Record<string, readonly string[]> = {};\n for (const { country, entries } of cityEntries) {\n if (entries.length > 0) citiesByCountry[country] = entries;\n }\n\n return {\n firstNames: names.firstNames,\n surnames: names.surnames,\n denyList,\n denyListMeta,\n citiesByCountry,\n };\n};\n","","import { realpathSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { basename, join, resolve } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\n\nimport type {\n createPipelineContext,\n deanonymise,\n Dictionaries,\n Entity,\n exportRedactionKey,\n OperatorConfig,\n OperatorType,\n PipelineConfig,\n redactText,\n runPipeline,\n} from \"@stll/anonymize\";\nimport { DEFAULT_ENTITY_LABELS } from \"@stll/anonymize/constants\";\n\nimport pkg from \"../package.json\" with { type: \"json\" };\n\nimport type { CliOptions } from \"./args\";\nimport { HELP, parseCliArgs, parseCountries, UsageError } from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\n\n/**\n * The pipeline functions the CLI needs. Satisfied by both\n * @stll/anonymize (native) and @stll/anonymize-wasm, so the\n * entry point decides which engine backs the binary.\n */\nexport type AnonymizeApi = {\n createPipelineContext: typeof createPipelineContext;\n deanonymise: typeof deanonymise;\n exportRedactionKey: typeof exportRedactionKey;\n redactText: typeof redactText;\n runPipeline: typeof runPipeline;\n};\n\n/**\n * Everything an entry point injects: the pipeline engine\n * and the dictionary source (npm data package for the\n * Node bin, embedded gzip blob for the compiled binary).\n */\nexport type CliEngine = {\n api: AnonymizeApi;\n loadDictionaries: (scope: DictionaryScope) => Promise<Dictionaries>;\n};\n\n// Statically imported so the version is baked into both\n// the npm bundle and the compiled binary; a runtime\n// package.json lookup would fail inside the binary's\n// virtual filesystem.\nconst cliVersion = (): string => pkg.version;\n\n/**\n * Filesystem identity of a path: realpath when it exists\n * (so symlinks to the same file compare equal), lexical\n * resolution otherwise (the file may not exist yet).\n */\nconst canonicalPath = (path: string): string => {\n try {\n return realpathSync(path);\n } catch {\n return resolve(path);\n }\n};\n\nconst readStdin = async (): Promise<string> => {\n process.stdin.setEncoding(\"utf8\");\n let text = \"\";\n for await (const chunk of process.stdin) text += chunk;\n return text;\n};\n\ntype NamedInput = {\n /** Source path, or null when reading stdin. */\n path: string | null;\n text: string;\n};\n\nconst readInputs = async (files: string[]): Promise<NamedInput[]> => {\n if (files.length === 0) {\n if (process.stdin.isTTY) {\n throw new UsageError(\n \"no input files and stdin is a terminal (see --help)\",\n );\n }\n return [{ path: null, text: await readStdin() }];\n }\n return Promise.all(\n files.map(async (path) => ({ path, text: await readFile(path, \"utf8\") })),\n );\n};\n\nconst validateLabels = (labels: readonly string[]): string[] => {\n // Widening the literal tuple to string[] is safe here;\n // we only test membership.\n const known: readonly string[] = DEFAULT_ENTITY_LABELS;\n const invalid = labels.find((label) => !known.includes(label));\n if (invalid) {\n throw new UsageError(\n `--labels: unknown label \"${invalid}\"; available: ${DEFAULT_ENTITY_LABELS.join(\", \")}`,\n );\n }\n return [...labels];\n};\n\nconst buildPipelineConfig = async (\n opts: CliOptions,\n loadDictionaries: CliEngine[\"loadDictionaries\"],\n): Promise<PipelineConfig> => {\n const dictionaries = await loadDictionaries({\n languages: opts.languages,\n countries: opts.countries,\n });\n return {\n threshold: opts.threshold,\n enableTriggerPhrases: true,\n enableRegex: true,\n enableLegalForms: true,\n enableNameCorpus: true,\n ...(opts.languages === undefined\n ? {}\n : { nameCorpusLanguages: [...opts.languages] }),\n enableDenyList: true,\n ...(opts.countries === undefined\n ? {}\n : { denyListCountries: [...opts.countries] }),\n enableGazetteer: false,\n enableCountries: true,\n enableNer: false,\n enableConfidenceBoost: true,\n enableCoreference: true,\n enableZoneClassification: true,\n enableHotwordRules: true,\n labels:\n opts.labels === undefined\n ? [...DEFAULT_ENTITY_LABELS]\n : validateLabels(opts.labels),\n workspaceId: \"cli\",\n dictionaries,\n };\n};\n\nconst buildOperatorConfig = (\n opts: CliOptions,\n entities: Entity[],\n): OperatorConfig => {\n const operators: Record<string, OperatorType> = {};\n if (opts.mode === \"redact\") {\n for (const entity of entities) operators[entity.label] = \"redact\";\n }\n return { operators, redactString: opts.redactString };\n};\n\nconst writeOutput = async (\n path: string | undefined,\n content: string,\n): Promise<void> => {\n if (path === undefined) {\n process.stdout.write(content);\n return;\n }\n await writeFile(path, content, \"utf8\");\n};\n\ntype RedactionKeyFile = {\n entries: Record<string, { original: string; operator: string }>;\n};\n\nconst parseRedactionKey = (raw: string): Map<string, string> => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new UsageError(\"redaction key is not valid JSON\");\n }\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new UsageError(\n 'redaction key must be an object with an \"entries\" field',\n );\n }\n const { entries } = parsed as RedactionKeyFile;\n if (\n typeof entries !== \"object\" ||\n entries === null ||\n Array.isArray(entries)\n ) {\n throw new UsageError('redaction key \"entries\" must be an object');\n }\n const map = new Map<string, string>();\n for (const [placeholder, entry] of Object.entries(entries)) {\n if (typeof entry?.original !== \"string\") {\n throw new UsageError(\n `redaction key entry \"${placeholder}\" has no original text`,\n );\n }\n map.set(placeholder, entry.original);\n }\n return map;\n};\n\n/**\n * Ask for a country scope when running interactively on\n * files with no scope flags. Skipped for piped stdin so\n * the CLI stays scriptable.\n */\nexport const shouldPromptForScope = (\n opts: CliOptions,\n tty: { stdinIsTTY: boolean; stderrIsTTY: boolean },\n): boolean =>\n opts.countries === undefined &&\n opts.languages === undefined &&\n !opts.quiet &&\n opts.files.length > 0 &&\n tty.stdinIsTTY &&\n tty.stderrIsTTY;\n\nconst promptForCountries = async (): Promise<string[] | undefined> => {\n const rl = createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n try {\n const answer = await rl.question(\n \"Country scope (ISO codes like CZ,DE,GB; Enter loads all): \",\n );\n const trimmed = answer.trim();\n return trimmed === \"\" ? undefined : parseCountries(trimmed);\n } finally {\n rl.close();\n }\n};\n\nconst runDeanonymise = async (\n opts: CliOptions,\n api: AnonymizeApi,\n): Promise<void> => {\n if (opts.keyPath !== undefined) {\n throw new UsageError(\"--key cannot be combined with --deanonymise\");\n }\n const keyPath = opts.deanonymiseKeyPath;\n if (keyPath === undefined) throw new UsageError(\"missing redaction key path\");\n const redactionMap = parseRedactionKey(await readFile(keyPath, \"utf8\"));\n\n const inputs = await readInputs(opts.files);\n if (inputs.length > 1) {\n throw new UsageError(\"--deanonymise accepts a single input\");\n }\n const input = inputs[0];\n if (!input) throw new UsageError(\"no input to deanonymise\");\n if (opts.output !== undefined) {\n guardWriteTargets(input.path === null ? [] : [input.path], [\n { path: opts.output, flag: \"--output\" },\n ]);\n }\n await writeOutput(opts.output, api.deanonymise(input.text, redactionMap));\n};\n\nconst outputPathFor = (\n input: NamedInput,\n opts: CliOptions,\n multi: boolean,\n): string | undefined => {\n if (opts.output === undefined) return undefined;\n if (!multi) return opts.output;\n if (input.path === null)\n throw new UsageError(\"stdin cannot be combined with multiple files\");\n return join(opts.output, basename(input.path));\n};\n\n/**\n * Reject any write target (output or key file) whose\n * filesystem identity collides with an input file or with\n * another write target. Symlinks count as collisions.\n */\nconst guardWriteTargets = (\n inputPaths: readonly string[],\n writeTargets: readonly { path: string; flag: string }[],\n): void => {\n const inputs = new Set(inputPaths.map(canonicalPath));\n const seen = new Map<string, string>();\n for (const target of writeTargets) {\n const canonical = canonicalPath(target.path);\n if (inputs.has(canonical)) {\n throw new UsageError(\n `refusing to overwrite input file \"${target.path}\" (${target.flag})`,\n );\n }\n const clash = seen.get(canonical);\n if (clash !== undefined) {\n throw new UsageError(\n `${target.flag} \"${target.path}\" collides with ${clash}`,\n );\n }\n seen.set(canonical, `${target.flag} \"${target.path}\"`);\n }\n};\n\nconst summarize = (entities: Entity[]): string => {\n const counts = new Map<string, number>();\n for (const entity of entities) {\n counts.set(entity.label, (counts.get(entity.label) ?? 0) + 1);\n }\n const parts = [...counts.entries()]\n .toSorted((a, b) => b[1] - a[1])\n .map(([label, count]) => `${label}: ${count}`);\n return parts.length > 0 ? parts.join(\", \") : \"none\";\n};\n\nconst runAnonymise = async (\n opts: CliOptions,\n { api, loadDictionaries }: CliEngine,\n): Promise<void> => {\n const multi = opts.files.length > 1;\n if (multi && opts.output === undefined) {\n throw new UsageError(\"multiple input files require --output <directory>\");\n }\n if (multi && opts.keyPath !== undefined) {\n throw new UsageError(\"--key works with a single input only\");\n }\n if (multi && opts.json) {\n throw new UsageError(\"--json works with a single input only\");\n }\n if (opts.keyPath !== undefined && opts.mode !== \"replace\") {\n throw new UsageError('--key requires --mode \"replace\"');\n }\n\n const scoped = shouldPromptForScope(opts, {\n stdinIsTTY: process.stdin.isTTY === true,\n stderrIsTTY: process.stderr.isTTY === true,\n })\n ? { ...opts, countries: await promptForCountries() }\n : opts;\n\n const inputs = await readInputs(scoped.files);\n\n // Validate every write target before any work: output\n // collisions (same basename from different input dirs,\n // symlinks to an input, --key hitting the output) fail\n // fast instead of silently clobbering files mid-batch.\n const outputPaths = inputs.map((input) => outputPathFor(input, opts, multi));\n const writeTargets: { path: string; flag: string }[] = [];\n for (const path of outputPaths) {\n if (path !== undefined) writeTargets.push({ path, flag: \"--output\" });\n }\n if (opts.keyPath !== undefined) {\n writeTargets.push({ path: opts.keyPath, flag: \"--key\" });\n }\n guardWriteTargets(\n inputs.flatMap((input) => (input.path === null ? [] : [input.path])),\n writeTargets,\n );\n\n const config = await buildPipelineConfig(scoped, loadDictionaries);\n\n // One shared context for the whole batch: the first\n // runPipeline builds the search automaton after its own\n // init steps (hotword rules included) and caches it on\n // the context for the remaining files. Cross-document\n // reuse is supported — coref links travel on entities.\n const context = api.createPipelineContext();\n\n if (multi && opts.output !== undefined) {\n await mkdir(opts.output, { recursive: true });\n }\n\n for (const [index, input] of inputs.entries()) {\n const outputPath = outputPaths[index];\n\n const entities = await api.runPipeline({\n fullText: input.text,\n config,\n gazetteerEntries: [],\n context,\n });\n const result = api.redactText(\n input.text,\n entities,\n buildOperatorConfig(opts, entities),\n context,\n );\n\n if (opts.json) {\n const payload = {\n entityCount: result.entityCount,\n entities,\n redactedText: result.redactedText,\n };\n await writeOutput(outputPath, `${JSON.stringify(payload, null, 2)}\\n`);\n } else {\n await writeOutput(outputPath, result.redactedText);\n }\n\n if (opts.keyPath !== undefined) {\n await writeFile(\n opts.keyPath,\n api.exportRedactionKey(result.redactionMap, result.operatorMap),\n \"utf8\",\n );\n }\n\n if (!opts.quiet) {\n const source = input.path ?? \"stdin\";\n process.stderr.write(`anonymize: ${source}: ${summarize(entities)}\\n`);\n }\n }\n};\n\nconst dispatch = async (engine: CliEngine): Promise<void> => {\n const opts = parseCliArgs(process.argv.slice(2));\n if (opts.help) {\n process.stdout.write(HELP);\n return;\n }\n if (opts.version) {\n process.stdout.write(`${cliVersion()}\\n`);\n return;\n }\n if (opts.deanonymiseKeyPath !== undefined) {\n await runDeanonymise(opts, engine.api);\n return;\n }\n await runAnonymise(opts, engine);\n};\n\n/**\n * Run the CLI against the given engine and set the\n * process exit code (0 ok, 1 runtime error, 2 usage).\n */\nexport const runCli = async (engine: CliEngine): Promise<void> => {\n try {\n await dispatch(engine);\n } catch (err) {\n if (err instanceof UsageError) {\n process.stderr.write(`anonymize: ${err.message}\\n`);\n process.stderr.write(`Try \"anonymize --help\" for usage.\\n`);\n process.exitCode = 2;\n } else {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`anonymize: ${message}\\n`);\n process.exitCode = 1;\n }\n }\n};\n","#!/usr/bin/env node\n/* npm-distributed entry point — backs the CLI with the\n * native engine (@stll/text-search napi bindings) and\n * the @stll/anonymize-data dictionary package. */\nimport * as anonymize from \"@stll/anonymize\";\n\nimport { loadCliDictionaries } from \"./dictionaries\";\nimport { runCli } from \"./main\";\n\nawait runCli({ api: anonymize, loadDictionaries: loadCliDictionaries });\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,39 @@
1
1
  {
2
2
  "name": "@stll/anonymize-cli",
3
- "version": "0.0.1-placeholder.0",
4
- "description": "Placeholder to bootstrap npm trusted publishing. The real CLI ships with the next @stll/anonymize release.",
5
- "license": "MIT",
3
+ "version": "1.4.9",
4
+ "description": "Command-line PII detection and anonymization powered by @stll/anonymize",
5
+ "type": "module",
6
+ "bin": {
7
+ "anonymize": "./dist/cli.mjs"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
6
15
  "repository": {
7
16
  "type": "git",
8
17
  "url": "git+https://github.com/stella/anonymize.git"
9
18
  },
10
- "publishConfig": {
11
- "access": "public"
19
+ "license": "MIT",
20
+ "scripts": {
21
+ "build": "tsdown",
22
+ "compile": "bun scripts/copy-wasm-payloads.ts && bun scripts/embed-data.ts && bun build --compile '--asset-naming=[name].[ext]' src/compile.ts --outfile dist/anonymize",
23
+ "prepublishOnly": "bun run build",
24
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
25
+ "test": "bun test --timeout 60000",
26
+ "format": "oxfmt ."
27
+ },
28
+ "dependencies": {
29
+ "@stll/anonymize": "^1.4.9",
30
+ "@stll/anonymize-data": "^0.0.6"
31
+ },
32
+ "devDependencies": {
33
+ "@stll/anonymize-wasm": "workspace:*",
34
+ "@types/node": "^25.9.2",
35
+ "bun-types": "^1.3.14",
36
+ "tsdown": "^0.22.0",
37
+ "typescript": "^6.0.3"
12
38
  }
13
39
  }