json-humanized 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 json-humanized contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,351 @@
1
+ <div align="center">
2
+
3
+ # json-humanized
4
+
5
+ **Transform any JSON / YAML / TOML into natural human language**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/json-humanized?color=00e5ff&style=flat-square)](https://www.npmjs.com/package/json-humanized)
8
+ [![CI](https://img.shields.io/github/actions/workflow/status/AceAnomDev/json-humanized/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/AceAnomDev/json-humanized/actions)
9
+ [![license](https://img.shields.io/npm/l/json-humanized?style=flat-square)](LICENSE)
10
+ [![node](https://img.shields.io/node/v/json-humanized?style=flat-square&color=339933)](package.json)
11
+ [![downloads](https://img.shields.io/npm/dm/json-humanized?style=flat-square&color=00ff9d)](https://www.npmjs.com/package/json-humanized)
12
+
13
+ **[Live Demo](https://aceanomdev.github.io/json-humanized)** · [Installation](#installation) · [Usage](#usage) · [API](#api) · [Config](#config-file)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## What it does
20
+
21
+ ```bash
22
+ $ jh user.json
23
+ ```
24
+
25
+ ```
26
+ This JSON contains a structured object with 6 fields.
27
+ • Identifier: usr_8f3k2
28
+ • Name: "Alice Johnson"
29
+ • Email address: alice@example.com
30
+ • Age: 28 years old
31
+ • Password: *** (hidden for security)
32
+ • Balance: $4.3K
33
+ • Created: March 15, 2024 at 10:30 AM
34
+ ```
35
+
36
+ Works **100% offline** (no API key needed) — or plug in Claude AI / OpenAI / Ollama for smarter descriptions.
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ npm install -g json-humanized
44
+ ```
45
+
46
+ Or use without installing:
47
+
48
+ ```bash
49
+ npx json-humanized data.json
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Usage
55
+
56
+ ### CLI
57
+
58
+ ```bash
59
+ # Basic — local engine (offline, instant)
60
+ jh data.json
61
+
62
+ # YAML and TOML also work
63
+ jh config.yaml
64
+ jh settings.toml
65
+
66
+ # AI-powered (Claude)
67
+ jh data.json --engine ai
68
+
69
+ # AI with OpenAI
70
+ jh data.json --engine ai --provider openai
71
+
72
+ # AI with local Ollama (no API key!)
73
+ jh data.json --engine ai --provider ollama
74
+
75
+ # Output formats
76
+ jh data.json --format markdown --output report.md
77
+ jh data.json --format story
78
+ jh data.json --format json
79
+
80
+ # Diff two files
81
+ jh v1.json --diff v2.json
82
+ jh v1.json --diff v2.json --format markdown
83
+
84
+ # Watch mode — re-runs on every save
85
+ jh data.json --watch
86
+
87
+ # Stdin
88
+ echo '{"name":"Alice","age":30}' | jh --stdin
89
+
90
+ # Custom Handlebars template
91
+ jh data.json --template ./report.hbs
92
+
93
+ # Limit JSON chars sent to AI (saves tokens)
94
+ jh huge.json --engine ai --max-chars 8000
95
+
96
+ # Cache management
97
+ jh --cache-stats
98
+ jh --cache-clear
99
+
100
+ # Init config/template files
101
+ jh --init-config # creates .jh.config.json
102
+ jh --init-template # creates template.hbs
103
+ ```
104
+
105
+ ### All CLI flags
106
+
107
+ | Flag | Default | Description |
108
+ |------|---------|-------------|
109
+ | `--engine <engine>` | `local` | `local` or `ai` |
110
+ | `--provider <provider>` | `anthropic` | `anthropic`, `openai`, `ollama` |
111
+ | `--format <format>` | `plain` | `plain`, `markdown`, `story`, `json` |
112
+ | `--mode <mode>` | `structured` | `structured`, `prose`, `story` |
113
+ | `--lang <lang>` | `English` | Any language (AI only) |
114
+ | `--context <text>` | — | Context hint for AI |
115
+ | `--api-key <key>` | env var | API key override |
116
+ | `--max-chars <n>` | `12000` | Max chars sent to AI |
117
+ | `--output <file>` | stdout | Save output to file |
118
+ | `--template <file>` | — | Handlebars `.hbs` template |
119
+ | `--diff <fileB>` | — | Compare two files |
120
+ | `--watch` | — | Re-run on file changes |
121
+ | `--config <file>` | auto | Explicit config file path |
122
+ | `--cache` / `--no-cache` | `true` | Enable/disable AI caching |
123
+ | `--cache-clear` | — | Delete all cached responses |
124
+ | `--cache-stats` | — | Show cache info |
125
+ | `--init-config` | — | Generate sample config |
126
+ | `--init-template` | — | Generate sample template |
127
+ | `--stdin` | — | Read from stdin |
128
+ | `--silent` | — | No spinner, no banner |
129
+
130
+ ---
131
+
132
+ ## API
133
+
134
+ ```js
135
+ const { humanize, humanizeFile, humanizeString } = require('json-humanized');
136
+
137
+ // Parse a JS value
138
+ const text = await humanize({ name: 'Alice', age: 30 });
139
+
140
+ // From a file (JSON, YAML, or TOML)
141
+ const text = await humanizeFile('./users.yaml', { format: 'markdown' });
142
+
143
+ // From a raw string
144
+ const text = await humanizeString('{"key":"value"}');
145
+
146
+ // AI mode with caching
147
+ const text = await humanize(data, {
148
+ engine: 'ai',
149
+ aiProvider: 'anthropic', // or 'openai' | 'ollama'
150
+ format: 'plain',
151
+ lang: 'Russian',
152
+ cache: true,
153
+ cacheTTL: 3600,
154
+ });
155
+ ```
156
+
157
+ ### Diff
158
+
159
+ ```js
160
+ const { diff } = require('json-humanized');
161
+
162
+ // Compare two objects
163
+ const result = await diff.diff(before, after);
164
+ // "Found 3 differences: …"
165
+
166
+ // Compare two files
167
+ const result = await diff.diffFiles('v1.json', 'v2.json', { format: 'markdown' });
168
+ ```
169
+
170
+ ### TypeScript
171
+
172
+ Full types are included — no `@types/` package needed:
173
+
174
+ ```ts
175
+ import { humanize, humanizeFile, HumanizeOptions } from 'json-humanized';
176
+
177
+ const opts: HumanizeOptions = { engine: 'ai', aiProvider: 'ollama', format: 'markdown' };
178
+ const text = await humanizeFile('./data.json', opts);
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Config file
184
+
185
+ Create `.jh.config.json` (or run `jh --init-config`) to set defaults:
186
+
187
+ ```json
188
+ {
189
+ "engine": "local",
190
+ "format": "plain",
191
+ "lang": "English",
192
+ "maxChars": 12000,
193
+ "cache": true,
194
+ "cacheTTL": 3600,
195
+
196
+ "fieldLabels": {
197
+ "user_id": "User ID",
198
+ "txn_ref": "Transaction reference",
199
+ "internal_*": null
200
+ },
201
+
202
+ "fieldTypes": {
203
+ "invoice_no": "id",
204
+ "balance": "money"
205
+ },
206
+
207
+ "hiddenFields": ["debug_*", "internal_hash"],
208
+
209
+ "aiProvider": "anthropic",
210
+ "ollamaUrl": "http://localhost:11434",
211
+ "ollamaModel": "llama3",
212
+ "openaiModel": "gpt-4o-mini"
213
+ }
214
+ ```
215
+
216
+ Config is searched upward from the current directory (like ESLint / Prettier).
217
+
218
+ ---
219
+
220
+ ## Custom templates
221
+
222
+ Create a Handlebars template (or run `jh --init-template`):
223
+
224
+ ```hbs
225
+ # {{filename}}
226
+ > Generated: {{timestamp}} · Engine: {{engine}}
227
+
228
+ {{humanized}}
229
+
230
+ ---
231
+ *Type: {{stats.type}}, Keys: {{stats.keys}}*
232
+ ```
233
+
234
+ Use it with:
235
+
236
+ ```bash
237
+ jh data.json --template ./report.hbs
238
+ ```
239
+
240
+ Available template variables: `{{humanized}}`, `{{filename}}`, `{{engine}}`, `{{format}}`, `{{timestamp}}`, `{{stats.type}}`, `{{stats.keys}}`, `{{stats.items}}`
241
+
242
+ ---
243
+
244
+ ## AI Providers
245
+
246
+ | Provider | Env Variable | Install |
247
+ |----------|-------------|---------|
248
+ | `anthropic` (default) | `ANTHROPIC_API_KEY` | `npm install @anthropic-ai/sdk` |
249
+ | `openai` | `OPENAI_API_KEY` | `npm install openai` |
250
+ | `ollama` | none needed | [Install Ollama](https://ollama.ai) |
251
+
252
+ Optional dependencies — install only what you use.
253
+
254
+ ---
255
+
256
+ ## Caching
257
+
258
+ AI responses are cached by default in `~/.jh-cache/`.
259
+
260
+ - Same JSON + same options → returns cached result instantly
261
+ - Cache TTL: 1 hour (configurable)
262
+ - Override: `jh data.json --engine ai --no-cache`
263
+ - Clear all: `jh --cache-clear`
264
+ - Custom dir: `JH_CACHE_DIR=/tmp/my-cache jh data.json --engine ai`
265
+
266
+ ---
267
+
268
+ ## Diff
269
+
270
+ ```bash
271
+ $ jh v1.json --diff v2.json
272
+
273
+ Found 3 differences:
274
+
275
+ ➕ Added (1):
276
+ + phone (phone): "555-0100"
277
+
278
+ ✏ Changed (2):
279
+ ~ name (name): "Alice" → "Alice Johnson"
280
+ ~ balance (balance): $4.3K → $4.5K
281
+ ```
282
+
283
+ ```bash
284
+ # AI-powered diff (more natural language)
285
+ $ jh v1.json --diff v2.json --engine ai --lang Russian
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Watch mode
291
+
292
+ ```bash
293
+ $ jh data.json --watch
294
+ ```
295
+
296
+ Watches the file and re-runs humanization on every save. Supports local and AI engines.
297
+
298
+ ---
299
+
300
+ ## Supported file types
301
+
302
+ | Extension | Notes |
303
+ |-----------|-------|
304
+ | `.json` | Always available |
305
+ | `.yaml`, `.yml` | Requires `npm install js-yaml` |
306
+ | `.toml` | Requires `npm install @iarna/toml` |
307
+
308
+ ---
309
+
310
+ ## Project structure
311
+
312
+ ```
313
+ json-humanized/
314
+ ├── bin/
315
+ │ └── cli.js # CLI entry point
316
+ ├── src/
317
+ │ ├── index.js # Public API
318
+ │ ├── humanizer.js # Rule-based engine
319
+ │ ├── config.js # Config file loader
320
+ │ ├── cache.js # AI response cache
321
+ │ ├── diff.js # Diff engine
322
+ │ ├── watch.js # File watcher
323
+ │ ├── parsers/
324
+ │ │ └── index.js # JSON / YAML / TOML parser
325
+ │ ├── formatters/
326
+ │ │ ├── index.js # plain, markdown, story, json
327
+ │ │ └── template.js # Handlebars template renderer
328
+ │ └── strategies/
329
+ │ ├── ai.js # AI provider router
330
+ │ ├── openai.js # OpenAI provider
331
+ │ └── ollama.js # Ollama provider
332
+ ├── docs/
333
+ │ ├── DEMO.html # Live browser demo
334
+ │ └── ARCHITECTURE.md
335
+ ├── examples/
336
+ ├── test/
337
+ ├── index.d.ts # TypeScript types
338
+ └── .jh.config.json # (optional) project config
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Contributing
344
+
345
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and PRs are welcome!
346
+
347
+ ---
348
+
349
+ ## License
350
+
351
+ MIT © json-humanized contributors
package/bin/cli.js ADDED
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // json-humanized · CLI v2.0
6
+ // Usage: jh <file> [options]
7
+ // jh --diff a.json b.json
8
+ // jh --watch file.json
9
+ // echo '{"key":"val"}' | jh --stdin
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ const { program } = require('commander');
13
+ const chalk = require('chalk');
14
+ const ora = require('ora');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const { humanizeFile, humanizeString } = require('../src/index');
19
+ const { diff, diffFiles } = require('../src/diff');
20
+ const { watch } = require('../src/watch');
21
+ const { clearCache, cacheStats } = require('../src/cache');
22
+ const { generateExampleConfig } = require('../src/config');
23
+ const { generateExampleTemplate } = require('../src/formatters/template');
24
+ const pkg = require('../package.json');
25
+
26
+ // ─── banner ──────────────────────────────────────────────────────────────────
27
+
28
+ function printBanner() {
29
+ console.log('');
30
+ console.log(chalk.bold.cyan(' ╔══════════════════════════════════╗'));
31
+ console.log(chalk.bold.cyan(' ║') + chalk.bold.white(' json-humanized v' + pkg.version + ' ') + chalk.bold.cyan('║'));
32
+ console.log(chalk.bold.cyan(' ║') + chalk.gray(' Turn JSON into human language ') + chalk.bold.cyan('║'));
33
+ console.log(chalk.bold.cyan(' ╚══════════════════════════════════╝'));
34
+ console.log('');
35
+ }
36
+
37
+ // ─── CLI definition ──────────────────────────────────────────────────────────
38
+
39
+ program
40
+ .name('json-humanized')
41
+ .alias('jh')
42
+ .version(pkg.version, '-v, --version', 'Show version')
43
+ .description(chalk.cyan('Transform any JSON/YAML/TOML into natural human language\n') +
44
+ chalk.gray(' Supports files, stdin, diff, watch, and AI-powered descriptions'))
45
+
46
+ .argument('[file]', 'JSON/YAML/TOML file to humanize (or use --stdin)')
47
+
48
+ // ── Input
49
+ .option('--stdin', 'Read from stdin instead of a file')
50
+ .option('--diff <fileB>', 'Compare <file> with <fileB> and describe differences')
51
+
52
+ // ── Engine & Provider
53
+ .option('-e, --engine <engine>', 'Engine: local (default) or ai', 'local')
54
+ .option('--provider <provider>', 'AI provider: anthropic, openai, ollama', 'anthropic')
55
+
56
+ // ── Output
57
+ .option('-f, --format <format>', 'Output format: plain, markdown, story, json', 'plain')
58
+ .option('-m, --mode <mode>', 'Description mode: structured, prose, story', 'structured')
59
+ .option('-o, --output <file>', 'Save output to a file')
60
+ .option('--template <file>', 'Handlebars .hbs template for custom output')
61
+
62
+ // ── AI options
63
+ .option('-l, --lang <lang>', 'Output language (AI mode only)', 'English')
64
+ .option('-c, --context <text>', 'Context hint for AI (e.g. "Stripe webhook")', '')
65
+ .option('-k, --api-key <key>', 'API key (overrides env variable)')
66
+ .option('--max-chars <n>', 'Max JSON chars sent to AI (default 12000)', '12000')
67
+
68
+ // ── Watch
69
+ .option('--watch', 'Watch file and re-humanize on every save')
70
+
71
+ // ── Config
72
+ .option('--config <file>', 'Path to .jh.config.json config file')
73
+ .option('--init-config', 'Create a sample .jh.config.json in current directory')
74
+ .option('--init-template', 'Create a sample template.hbs in current directory')
75
+
76
+ // ── Cache
77
+ .option('--no-cache', 'Bypass AI response cache for this run')
78
+ .option('--cache-clear', 'Clear all cached AI responses and exit')
79
+ .option('--cache-stats', 'Show cache statistics and exit')
80
+
81
+ // ── Misc
82
+ .option('--no-banner', 'Suppress the banner')
83
+ .option('--silent', 'No spinner or banner, just output')
84
+
85
+ .addHelpText('after', `
86
+ ${chalk.bold('Examples:')}
87
+ ${chalk.cyan('$')} jh data.json
88
+ ${chalk.cyan('$')} jh data.yaml --format markdown --output report.md
89
+ ${chalk.cyan('$')} jh users.json --engine ai --lang Spanish
90
+ ${chalk.cyan('$')} jh a.json --diff b.json
91
+ ${chalk.cyan('$')} jh config.json --watch
92
+ ${chalk.cyan('$')} echo '{"name":"Alice","age":30}' | jh --stdin
93
+ ${chalk.cyan('$')} jh payload.json --engine ai --provider openai
94
+ ${chalk.cyan('$')} jh payload.json --engine ai --provider ollama
95
+ ${chalk.cyan('$')} jh big.json --engine ai --max-chars 8000
96
+ ${chalk.cyan('$')} jh data.json --template ./report.hbs
97
+
98
+ ${chalk.bold('Engines:')}
99
+ ${chalk.yellow('local')} — Fast, offline, rule-based. No API key needed.
100
+ ${chalk.yellow('ai')} — AI-powered. See --provider for supported backends.
101
+
102
+ ${chalk.bold('AI Providers:')}
103
+ ${chalk.yellow('anthropic')} — Claude (default). Needs ANTHROPIC_API_KEY.
104
+ ${chalk.yellow('openai')} — GPT models. Needs OPENAI_API_KEY.
105
+ ${chalk.yellow('ollama')} — Local models (llama3, mistral…). No key needed.
106
+
107
+ ${chalk.bold('Formats:')}
108
+ ${chalk.yellow('plain')} — Human-readable text (default)
109
+ ${chalk.yellow('markdown')} — Markdown report with headers and table
110
+ ${chalk.yellow('story')} — Narrative storytelling style
111
+ ${chalk.yellow('json')} — JSON output with metadata
112
+
113
+ ${chalk.bold('Environment Variables:')}
114
+ ANTHROPIC_API_KEY — API key for Claude AI engine
115
+ OPENAI_API_KEY — API key for OpenAI engine
116
+ OLLAMA_URL — Ollama base URL (default: http://localhost:11434)
117
+ OLLAMA_MODEL — Ollama model name (default: llama3)
118
+ JH_CACHE_DIR — Custom cache directory (default: ~/.jh-cache)
119
+ `);
120
+
121
+ program.parse(process.argv);
122
+
123
+ // ─── Main ────────────────────────────────────────────────────────────────────
124
+
125
+ async function main() {
126
+ const opts = program.opts();
127
+ const [file] = program.args;
128
+ const silent = opts.silent;
129
+
130
+ if (!silent && opts.banner !== false) printBanner();
131
+
132
+ // ── Cache management commands ────────────────────────────────────────────
133
+ if (opts.cacheStats) {
134
+ const s = cacheStats();
135
+ console.log(chalk.cyan(' Cache statistics:'));
136
+ console.log(` Directory : ${s.dir}`);
137
+ console.log(` Entries : ${s.entries}`);
138
+ console.log(` Size : ${(s.totalBytes / 1024).toFixed(1)} KB`);
139
+ return;
140
+ }
141
+
142
+ if (opts.cacheClear) {
143
+ const n = clearCache();
144
+ console.log(chalk.green(` ✓ Cleared ${n} cached AI response(s)`));
145
+ return;
146
+ }
147
+
148
+ // ── Init commands ────────────────────────────────────────────────────────
149
+ if (opts.initConfig) {
150
+ const target = path.join(process.cwd(), '.jh.config.json');
151
+ fs.writeFileSync(target, generateExampleConfig(), 'utf8');
152
+ console.log(chalk.green(` ✓ Created ${target}`));
153
+ return;
154
+ }
155
+
156
+ if (opts.initTemplate) {
157
+ const target = path.join(process.cwd(), 'template.hbs');
158
+ generateExampleTemplate(target);
159
+ console.log(chalk.green(` ✓ Created ${target}`));
160
+ return;
161
+ }
162
+
163
+ // ── Diff mode ────────────────────────────────────────────────────────────
164
+ if (opts.diff) {
165
+ if (!file) {
166
+ console.error(chalk.red(' ✖ --diff requires a first file argument: jh a.json --diff b.json'));
167
+ process.exit(1);
168
+ }
169
+
170
+ const spinner = !silent ? ora({ text: chalk.cyan('Comparing files…'), color: 'cyan' }).start() : null;
171
+
172
+ try {
173
+ const result = await diffFiles(file, opts.diff, {
174
+ engine: opts.engine,
175
+ format: opts.format,
176
+ apiKey: opts.apiKey || process.env.ANTHROPIC_API_KEY,
177
+ lang: opts.lang,
178
+ });
179
+ spinner && spinner.succeed(chalk.green('Done!'));
180
+ outputResult(result, opts, silent);
181
+ } catch (err) {
182
+ spinner && spinner.fail(chalk.red('Failed'));
183
+ exitError(err);
184
+ }
185
+ return;
186
+ }
187
+
188
+ // ── Watch mode ───────────────────────────────────────────────────────────
189
+ if (opts.watch) {
190
+ if (!file) {
191
+ console.error(chalk.red(' ✖ --watch requires a file argument'));
192
+ process.exit(1);
193
+ }
194
+
195
+ const options = buildOptions(opts);
196
+ const { humanize } = require('../src/index');
197
+
198
+ watch(file, options, async (data, watchOpts) => {
199
+ return humanize(data, watchOpts);
200
+ });
201
+ return; // watch keeps running
202
+ }
203
+
204
+ // ── Standard humanize ────────────────────────────────────────────────────
205
+ if (!file && !opts.stdin) {
206
+ console.error(chalk.red(' ✖ Please provide a JSON/YAML/TOML file or use --stdin'));
207
+ console.error(chalk.gray(' Run `jh --help` for usage information'));
208
+ process.exit(1);
209
+ }
210
+
211
+ validateOpts(opts);
212
+
213
+ const spinner = !silent ? ora({
214
+ text: opts.engine === 'ai'
215
+ ? chalk.cyan(`Sending to ${opts.provider} AI…`)
216
+ : chalk.cyan('Analysing structure…'),
217
+ color: 'cyan',
218
+ }).start() : null;
219
+
220
+ try {
221
+ const options = buildOptions(opts);
222
+ let result;
223
+
224
+ if (opts.stdin) {
225
+ const raw = await readStdin();
226
+ if (!raw.trim()) {
227
+ spinner && spinner.fail('No input received from stdin');
228
+ process.exit(1);
229
+ }
230
+ result = await humanizeString(raw, options);
231
+ } else {
232
+ if (!fs.existsSync(path.resolve(file))) {
233
+ spinner && spinner.fail(`File not found: ${file}`);
234
+ process.exit(1);
235
+ }
236
+ result = await humanizeFile(file, { ...options, filename: path.basename(file) });
237
+ }
238
+
239
+ spinner && spinner.succeed(chalk.green('Done!'));
240
+ outputResult(result, opts, silent);
241
+
242
+ } catch (err) {
243
+ spinner && spinner.fail(chalk.red('Failed'));
244
+ exitError(err);
245
+ }
246
+ }
247
+
248
+ // ─── helpers ─────────────────────────────────────────────────────────────────
249
+
250
+ function buildOptions(opts) {
251
+ return {
252
+ engine: opts.engine,
253
+ aiProvider: opts.provider,
254
+ format: opts.format,
255
+ mode: opts.mode,
256
+ lang: opts.lang,
257
+ context: opts.context,
258
+ apiKey: opts.apiKey || process.env.ANTHROPIC_API_KEY,
259
+ maxChars: parseInt(opts.maxChars, 10) || 12000,
260
+ template: opts.template,
261
+ cache: opts.cache !== false,
262
+ configPath: opts.config,
263
+ };
264
+ }
265
+
266
+ function outputResult(result, opts, silent) {
267
+ if (opts.output) {
268
+ fs.writeFileSync(path.resolve(opts.output), result, 'utf8');
269
+ if (!silent) console.log('\n' + chalk.green(` ✓ Saved to: ${chalk.bold(opts.output)}`));
270
+ } else {
271
+ console.log('\n' + result);
272
+ }
273
+ }
274
+
275
+ function validateOpts(opts) {
276
+ const validEngines = ['local', 'ai'];
277
+ const validFormats = ['plain', 'markdown', 'story', 'json'];
278
+ const validProviders = ['anthropic', 'openai', 'ollama'];
279
+
280
+ if (!validEngines.includes(opts.engine)) {
281
+ console.error(chalk.red(` ✖ Unknown engine: ${opts.engine}. Use: ${validEngines.join(', ')}`));
282
+ process.exit(1);
283
+ }
284
+ if (!validFormats.includes(opts.format)) {
285
+ console.error(chalk.red(` ✖ Unknown format: ${opts.format}. Use: ${validFormats.join(', ')}`));
286
+ process.exit(1);
287
+ }
288
+ if (opts.engine === 'ai' && !validProviders.includes(opts.provider)) {
289
+ console.error(chalk.red(` ✖ Unknown provider: ${opts.provider}. Use: ${validProviders.join(', ')}`));
290
+ process.exit(1);
291
+ }
292
+
293
+ const apiKey = opts.apiKey || process.env.ANTHROPIC_API_KEY;
294
+ if (opts.engine === 'ai' && opts.provider === 'anthropic' && !apiKey) {
295
+ console.error(chalk.red(' ✖ Anthropic engine requires ANTHROPIC_API_KEY'));
296
+ console.error(chalk.yellow(' Set ANTHROPIC_API_KEY env variable, or use --api-key flag'));
297
+ console.error(chalk.gray(' Or switch to local engine: --engine local'));
298
+ process.exit(1);
299
+ }
300
+ }
301
+
302
+ function exitError(err) {
303
+ console.error('\n' + chalk.red(' Error: ') + err.message);
304
+ if (process.env.DEBUG) console.error(err.stack);
305
+ process.exit(1);
306
+ }
307
+
308
+ function readStdin() {
309
+ return new Promise((resolve, reject) => {
310
+ if (process.stdin.isTTY) return resolve('');
311
+ let data = '';
312
+ process.stdin.setEncoding('utf8');
313
+ process.stdin.on('data', chunk => { data += chunk; });
314
+ process.stdin.on('end', () => resolve(data));
315
+ process.stdin.on('error', reject);
316
+ });
317
+ }
318
+
319
+ main();