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.
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * parsers/index.js — unified parser for JSON, YAML, and TOML.
5
+ *
6
+ * JSON is always available (built-in).
7
+ * YAML requires: npm install js-yaml
8
+ * TOML requires: npm install @iarna/toml
9
+ *
10
+ * If a peer dependency is missing, a clear error is thrown with install hint.
11
+ */
12
+
13
+ const path = require('path');
14
+
15
+ // ─── helpers ─────────────────────────────────────────────────────────────────
16
+
17
+ function requireOptional(pkg, installHint) {
18
+ try {
19
+ return require(pkg);
20
+ } catch {
21
+ throw new Error(
22
+ `Optional dependency "${pkg}" is required to parse this file.\n` +
23
+ `Install it with: npm install ${installHint || pkg}`
24
+ );
25
+ }
26
+ }
27
+
28
+ function detectFormatFromExt(filePath) {
29
+ const ext = path.extname(filePath).toLowerCase();
30
+ if (ext === '.json') return 'json';
31
+ if (ext === '.yaml' || ext === '.yml') return 'yaml';
32
+ if (ext === '.toml') return 'toml';
33
+ return null;
34
+ }
35
+
36
+ function detectFormatFromContent(text) {
37
+ const trimmed = text.trimStart();
38
+ // TOML starts with [section] or key = value
39
+ if (/^\[[\w.]+\]/.test(trimmed) || /^\w[\w.-]*\s*=/.test(trimmed)) return 'toml';
40
+ // JSON starts with { or [
41
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) return 'json';
42
+ // Fallback: try YAML (it's a superset of JSON anyway)
43
+ return 'yaml';
44
+ }
45
+
46
+ // ─── parsers ─────────────────────────────────────────────────────────────────
47
+
48
+ function parseJSON(text) {
49
+ try {
50
+ return JSON.parse(text);
51
+ } catch (err) {
52
+ throw new Error(`Invalid JSON: ${err.message}`);
53
+ }
54
+ }
55
+
56
+ function parseYAML(text) {
57
+ const yaml = requireOptional('js-yaml', 'js-yaml');
58
+ try {
59
+ return yaml.load(text);
60
+ } catch (err) {
61
+ throw new Error(`Invalid YAML: ${err.message}`);
62
+ }
63
+ }
64
+
65
+ function parseTOML(text) {
66
+ const toml = requireOptional('@iarna/toml', '@iarna/toml');
67
+ try {
68
+ return toml.parse(text);
69
+ } catch (err) {
70
+ throw new Error(`Invalid TOML: ${err.message}`);
71
+ }
72
+ }
73
+
74
+ // ─── public API ──────────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Parse text in JSON, YAML, or TOML format.
78
+ *
79
+ * @param {string} text raw file contents
80
+ * @param {string} [filePath] optional path hint for format detection
81
+ * @param {string} [format] explicit format override: 'json'|'yaml'|'toml'
82
+ * @returns {*} parsed JavaScript value
83
+ */
84
+ function parseAny(text, filePath, format) {
85
+ const fmt =
86
+ format ||
87
+ (filePath ? detectFormatFromExt(filePath) : null) ||
88
+ detectFormatFromContent(text);
89
+
90
+ switch (fmt) {
91
+ case 'json': return parseJSON(text);
92
+ case 'yaml': return parseYAML(text);
93
+ case 'toml': return parseTOML(text);
94
+ default:
95
+ // Last resort: try JSON, then YAML
96
+ try { return parseJSON(text); } catch {}
97
+ return parseYAML(text);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Read and parse a file from disk.
103
+ *
104
+ * @param {string} filePath
105
+ * @param {string} [formatOverride]
106
+ * @returns {*}
107
+ */
108
+ function parseFile(filePath, formatOverride) {
109
+ const fs = require('fs');
110
+ const text = fs.readFileSync(filePath, 'utf8');
111
+ return parseAny(text, filePath, formatOverride);
112
+ }
113
+
114
+ /**
115
+ * List supported extensions.
116
+ */
117
+ const SUPPORTED_EXTENSIONS = ['.json', '.yaml', '.yml', '.toml'];
118
+
119
+ module.exports = { parseAny, parseFile, SUPPORTED_EXTENSIONS };
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // json-humanized · AI Strategy router
5
+ // Supports: anthropic (default), openai, ollama
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+
8
+ const SYSTEM_PROMPT = `You are a JSON data interpreter. Your job is to transform raw JSON into clear, natural, human-readable prose that anyone can understand — even without technical knowledge.
9
+
10
+ Rules:
11
+ 1. Write in plain English. No jargon, no code, no JSON syntax in your output.
12
+ 2. Describe what the data MEANS, not just what it contains.
13
+ 3. Use a warm, professional tone.
14
+ 4. Identify patterns: if it's a list of users, say "This is a list of 5 users…"
15
+ 5. Highlight important or unusual values.
16
+ 6. Group related fields logically in your description.
17
+ 7. If a field looks sensitive (password, token, secret), say it's hidden — don't echo it.
18
+ 8. Keep it concise but complete. Aim for 3–10 sentences for simple objects, more for complex ones.
19
+ 9. Never use bullet points or markdown — write flowing paragraphs.
20
+ 10. If there's a list, summarize the pattern and give 1–2 concrete examples.`;
21
+
22
+ async function humanizeWithAnthropic(data, options = {}) {
23
+ const {
24
+ apiKey = process.env.ANTHROPIC_API_KEY,
25
+ model = 'claude-opus-4-5',
26
+ mode = 'prose',
27
+ lang = 'English',
28
+ context = '',
29
+ maxChars = 12000,
30
+ } = options;
31
+
32
+ if (!apiKey) {
33
+ throw new Error(
34
+ 'ANTHROPIC_API_KEY is required for AI mode. ' +
35
+ 'Set it via environment variable or --api-key flag. ' +
36
+ 'Alternatively, use --engine local for offline processing.'
37
+ );
38
+ }
39
+
40
+ let Anthropic;
41
+ try { Anthropic = require('@anthropic-ai/sdk'); } catch {
42
+ throw new Error(
43
+ 'The @anthropic-ai/sdk package is required for AI mode.\n' +
44
+ 'Install it with: npm install @anthropic-ai/sdk\n' +
45
+ 'Or use --engine local for offline processing.'
46
+ );
47
+ }
48
+
49
+ const client = new Anthropic.default({ apiKey });
50
+ const jsonStr = JSON.stringify(data, null, 2);
51
+ const truncated = jsonStr.length > maxChars;
52
+ const payload = truncated ? jsonStr.slice(0, maxChars) + '\n\n… (truncated for length)' : jsonStr;
53
+
54
+ const userMessage = [
55
+ context ? `Context: ${context}` : '',
56
+ `Output language: ${lang}`,
57
+ `Output style: ${mode === 'story' ? 'narrative story' : mode === 'markdown' ? 'markdown report' : 'clear prose paragraphs'}`,
58
+ truncated ? `Note: This JSON was truncated (${jsonStr.length} chars total, showing first ${maxChars}).` : '',
59
+ '',
60
+ 'Here is the JSON to humanize:',
61
+ '```json',
62
+ payload,
63
+ '```',
64
+ ].filter(Boolean).join('\n');
65
+
66
+ const message = await client.messages.create({
67
+ model,
68
+ max_tokens: 1024,
69
+ system: SYSTEM_PROMPT,
70
+ messages: [{ role: 'user', content: userMessage }],
71
+ });
72
+
73
+ return message.content
74
+ .filter(b => b.type === 'text')
75
+ .map(b => b.text)
76
+ .join('')
77
+ .trim();
78
+ }
79
+
80
+ /**
81
+ * Route AI humanization to the correct provider.
82
+ * @param {any} data
83
+ * @param {object} options
84
+ * @param {'anthropic'|'openai'|'ollama'} [options.aiProvider='anthropic']
85
+ */
86
+ async function humanizeWithAI(data, options = {}) {
87
+ const provider = options.aiProvider || options.provider || 'anthropic';
88
+
89
+ switch (provider) {
90
+ case 'anthropic':
91
+ return humanizeWithAnthropic(data, options);
92
+
93
+ case 'openai': {
94
+ const { humanizeWithOpenAI } = require('./openai');
95
+ return humanizeWithOpenAI(data, options);
96
+ }
97
+
98
+ case 'ollama': {
99
+ const { humanizeWithOllama } = require('./ollama');
100
+ return humanizeWithOllama(data, options);
101
+ }
102
+
103
+ default:
104
+ throw new Error(`Unknown AI provider: "${provider}". Use: anthropic, openai, or ollama.`);
105
+ }
106
+ }
107
+
108
+ module.exports = { humanizeWithAI, humanizeWithAnthropic };
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // json-humanized · Ollama Strategy (local AI, no API key needed)
5
+ // Requires: Ollama running at http://localhost:11434
6
+ // Install: https://ollama.ai
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ const SYSTEM_PROMPT = `You are a JSON data interpreter. Your job is to transform raw JSON into clear, natural, human-readable prose that anyone can understand — even without technical knowledge.
10
+
11
+ Rules:
12
+ 1. Write in plain English. No jargon, no code, no JSON syntax in your output.
13
+ 2. Describe what the data MEANS, not just what it contains.
14
+ 3. Use a warm, professional tone.
15
+ 4. Identify patterns: if it's a list of users, say "This is a list of 5 users…"
16
+ 5. Highlight important or unusual values.
17
+ 6. Group related fields logically in your description.
18
+ 7. If a field looks sensitive (password, token, secret), say it's hidden — don't echo it.
19
+ 8. Keep it concise but complete.
20
+ 9. Never use bullet points or markdown — write flowing paragraphs.`;
21
+
22
+ /**
23
+ * Humanize JSON using a local Ollama model
24
+ * @param {any} data
25
+ * @param {object} options
26
+ * @param {string} [options.ollamaUrl='http://localhost:11434']
27
+ * @param {string} [options.ollamaModel='llama3']
28
+ * @param {string} [options.lang='English']
29
+ * @param {string} [options.context='']
30
+ * @param {number} [options.maxChars=12000]
31
+ */
32
+ async function humanizeWithOllama(data, options = {}) {
33
+ const {
34
+ ollamaUrl = process.env.OLLAMA_URL || 'http://localhost:11434',
35
+ ollamaModel = process.env.OLLAMA_MODEL || 'llama3',
36
+ lang = 'English',
37
+ context = '',
38
+ maxChars = 12000,
39
+ } = options;
40
+
41
+ const jsonStr = JSON.stringify(data, null, 2);
42
+ const truncated = jsonStr.length > maxChars;
43
+ const payload = truncated ? jsonStr.slice(0, maxChars) + '\n\n… (truncated)' : jsonStr;
44
+
45
+ const prompt = [
46
+ SYSTEM_PROMPT,
47
+ '',
48
+ context ? `Context: ${context}` : '',
49
+ `Output language: ${lang}`,
50
+ truncated ? `Note: JSON was truncated (${jsonStr.length} chars, showing first ${maxChars}).` : '',
51
+ '',
52
+ 'Here is the JSON to humanize:',
53
+ payload,
54
+ ].filter(Boolean).join('\n');
55
+
56
+ // Use native fetch (Node 18+) or fall back to https module
57
+ const url = `${ollamaUrl.replace(/\/$/, '')}/api/generate`;
58
+
59
+ let responseText;
60
+
61
+ try {
62
+ const res = await fetchJSON(url, {
63
+ model: ollamaModel,
64
+ prompt,
65
+ stream: false,
66
+ });
67
+ responseText = res.response;
68
+ } catch (err) {
69
+ if (err.code === 'ECONNREFUSED' || err.message.includes('ECONNREFUSED')) {
70
+ throw new Error(
71
+ `Cannot connect to Ollama at ${ollamaUrl}.\n` +
72
+ 'Make sure Ollama is running: https://ollama.ai\n' +
73
+ `And that the model is pulled: ollama pull ${ollamaModel}`
74
+ );
75
+ }
76
+ throw err;
77
+ }
78
+
79
+ return (responseText || '').trim();
80
+ }
81
+
82
+ /**
83
+ * Check if Ollama is reachable and the model is available.
84
+ * @param {string} baseUrl
85
+ * @param {string} model
86
+ * @returns {Promise<boolean>}
87
+ */
88
+ async function checkOllamaHealth(baseUrl = 'http://localhost:11434', model = 'llama3') {
89
+ try {
90
+ const res = await fetchJSON(`${baseUrl.replace(/\/$/, '')}/api/tags`, null, 'GET');
91
+ const models = (res.models || []).map(m => m.name);
92
+ return models.some(m => m.startsWith(model));
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ // ─── tiny fetch helper (no deps) ─────────────────────────────────────────────
99
+
100
+ function fetchJSON(url, body, method = 'POST') {
101
+ return new Promise((resolve, reject) => {
102
+ const parsed = new URL(url);
103
+ const lib = parsed.protocol === 'https:' ? require('https') : require('http');
104
+ const data = body ? JSON.stringify(body) : null;
105
+
106
+ const opts = {
107
+ hostname: parsed.hostname,
108
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
109
+ path: parsed.pathname + parsed.search,
110
+ method,
111
+ headers: {
112
+ 'Content-Type': 'application/json',
113
+ 'Content-Length': data ? Buffer.byteLength(data) : 0,
114
+ },
115
+ };
116
+
117
+ const req = lib.request(opts, (res) => {
118
+ let raw = '';
119
+ res.on('data', c => { raw += c; });
120
+ res.on('end', () => {
121
+ try {
122
+ resolve(JSON.parse(raw));
123
+ } catch {
124
+ resolve({ response: raw });
125
+ }
126
+ });
127
+ });
128
+
129
+ req.on('error', reject);
130
+ if (data) req.write(data);
131
+ req.end();
132
+ });
133
+ }
134
+
135
+ module.exports = { humanizeWithOllama, checkOllamaHealth };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // json-humanized · OpenAI Strategy
5
+ // Requires: npm install openai
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+
8
+ const SYSTEM_PROMPT = `You are a JSON data interpreter. Your job is to transform raw JSON into clear, natural, human-readable prose that anyone can understand — even without technical knowledge.
9
+
10
+ Rules:
11
+ 1. Write in plain English. No jargon, no code, no JSON syntax in your output.
12
+ 2. Describe what the data MEANS, not just what it contains.
13
+ 3. Use a warm, professional tone.
14
+ 4. Identify patterns: if it's a list of users, say "This is a list of 5 users…"
15
+ 5. Highlight important or unusual values.
16
+ 6. Group related fields logically in your description.
17
+ 7. If a field looks sensitive (password, token, secret), say it's hidden — don't echo it.
18
+ 8. Keep it concise but complete. Aim for 3–10 sentences for simple objects, more for complex ones.
19
+ 9. Never use bullet points or markdown — write flowing paragraphs.
20
+ 10. If there's a list, summarize the pattern and give 1–2 concrete examples.`;
21
+
22
+ /**
23
+ * Humanize JSON using the OpenAI API
24
+ */
25
+ async function humanizeWithOpenAI(data, options = {}) {
26
+ const {
27
+ apiKey = process.env.OPENAI_API_KEY,
28
+ model = 'gpt-4o-mini',
29
+ mode = 'prose',
30
+ lang = 'English',
31
+ context = '',
32
+ maxChars = 12000,
33
+ } = options;
34
+
35
+ if (!apiKey) {
36
+ throw new Error(
37
+ 'OPENAI_API_KEY is required for OpenAI provider. ' +
38
+ 'Set it via environment variable or --api-key flag.'
39
+ );
40
+ }
41
+
42
+ let OpenAI;
43
+ try {
44
+ OpenAI = require('openai');
45
+ } catch {
46
+ throw new Error(
47
+ 'The "openai" package is required for OpenAI provider.\n' +
48
+ 'Install it with: npm install openai'
49
+ );
50
+ }
51
+
52
+ const client = new OpenAI.default({ apiKey });
53
+
54
+ const jsonStr = JSON.stringify(data, null, 2);
55
+ const truncated = jsonStr.length > maxChars;
56
+ const payload = truncated ? jsonStr.slice(0, maxChars) + '\n\n… (truncated)' : jsonStr;
57
+
58
+ const userMessage = [
59
+ context ? `Context: ${context}` : '',
60
+ `Output language: ${lang}`,
61
+ `Output style: ${mode === 'story' ? 'narrative story' : 'clear prose paragraphs'}`,
62
+ truncated ? `Note: JSON was truncated (${jsonStr.length} chars, showing first ${maxChars}).` : '',
63
+ '',
64
+ 'Here is the JSON to humanize:',
65
+ '```json',
66
+ payload,
67
+ '```',
68
+ ].filter(Boolean).join('\n');
69
+
70
+ const response = await client.chat.completions.create({
71
+ model,
72
+ messages: [
73
+ { role: 'system', content: SYSTEM_PROMPT },
74
+ { role: 'user', content: userMessage },
75
+ ],
76
+ max_tokens: 1024,
77
+ });
78
+
79
+ return response.choices[0].message.content.trim();
80
+ }
81
+
82
+ module.exports = { humanizeWithOpenAI };
package/src/watch.js ADDED
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * watch.js — watch a file for changes and re-run humanization on every save.
5
+ * Uses Node.js built-in fs.watch; no extra dependencies.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const { parseAny } = require('./parsers');
12
+
13
+ // ─── ANSI helpers ────────────────────────────────────────────────────────────
14
+
15
+ const ESC = '\x1b[';
16
+ const RESET = '\x1b[0m';
17
+ const BOLD = '\x1b[1m';
18
+ const DIM = '\x1b[2m';
19
+ const GREEN = '\x1b[32m';
20
+ const CYAN = '\x1b[36m';
21
+ const YELLOW = '\x1b[33m';
22
+ const RED = '\x1b[31m';
23
+
24
+ function clearScreen() {
25
+ process.stdout.write('\x1bc');
26
+ }
27
+
28
+ function timestamp() {
29
+ return new Date().toLocaleTimeString();
30
+ }
31
+
32
+ function header(filePath) {
33
+ const name = path.basename(filePath);
34
+ const line = '─'.repeat(52);
35
+ return [
36
+ `${CYAN}${line}${RESET}`,
37
+ `${BOLD} 👁 Watching: ${name}${RESET} ${DIM}(Ctrl+C to stop)${RESET}`,
38
+ `${CYAN}${line}${RESET}`,
39
+ '',
40
+ ].join('\n');
41
+ }
42
+
43
+ // ─── debounce ────────────────────────────────────────────────────────────────
44
+
45
+ function debounce(fn, delay) {
46
+ let timer;
47
+ return (...args) => {
48
+ clearTimeout(timer);
49
+ timer = setTimeout(() => fn(...args), delay);
50
+ };
51
+ }
52
+
53
+ // ─── core watch function ─────────────────────────────────────────────────────
54
+
55
+ /**
56
+ * Watch a file and re-humanize on every change.
57
+ *
58
+ * @param {string} filePath path to the JSON/YAML/TOML file
59
+ * @param {object} options same options as humanize()
60
+ * @param {Function} humanizeFn async (data, options) => string
61
+ * @returns {{ stop: Function }} call stop() to end watching
62
+ */
63
+ function watch(filePath, options = {}, humanizeFn) {
64
+ const absPath = path.resolve(filePath);
65
+
66
+ if (!fs.existsSync(absPath)) {
67
+ throw new Error(`File not found: ${absPath}`);
68
+ }
69
+
70
+ let runCount = 0;
71
+
72
+ async function run() {
73
+ runCount++;
74
+ clearScreen();
75
+ process.stdout.write(header(absPath));
76
+ process.stdout.write(`${DIM}Run #${runCount} · ${timestamp()}${RESET}\n\n`);
77
+
78
+ let raw;
79
+ try {
80
+ raw = fs.readFileSync(absPath, 'utf8');
81
+ } catch (err) {
82
+ process.stdout.write(`${RED}Error reading file: ${err.message}${RESET}\n`);
83
+ return;
84
+ }
85
+
86
+ let data;
87
+ try {
88
+ data = parseAny(raw, absPath);
89
+ } catch (err) {
90
+ process.stdout.write(`${RED}Parse error: ${err.message}${RESET}\n`);
91
+ return;
92
+ }
93
+
94
+ try {
95
+ const result = await humanizeFn(data, { ...options, filename: path.basename(absPath) });
96
+ process.stdout.write(result + '\n');
97
+ } catch (err) {
98
+ process.stdout.write(`${RED}Humanization error: ${err.message}${RESET}\n`);
99
+ }
100
+
101
+ process.stdout.write(`\n${DIM}───── Watching for changes…${RESET}\n`);
102
+ }
103
+
104
+ // Run immediately
105
+ run().catch(console.error);
106
+
107
+ // Watch for changes (debounced 200ms to avoid double-fire on some editors)
108
+ const debouncedRun = debounce(() => run().catch(console.error), 200);
109
+
110
+ const watcher = fs.watch(absPath, { persistent: true }, (event) => {
111
+ if (event === 'change' || event === 'rename') {
112
+ debouncedRun();
113
+ }
114
+ });
115
+
116
+ watcher.on('error', (err) => {
117
+ process.stderr.write(`${RED}Watch error: ${err.message}${RESET}\n`);
118
+ });
119
+
120
+ // Graceful shutdown
121
+ const cleanup = () => {
122
+ watcher.close();
123
+ process.stdout.write(`\n${YELLOW}Stopped watching.${RESET}\n`);
124
+ process.exit(0);
125
+ };
126
+
127
+ process.on('SIGINT', cleanup);
128
+ process.on('SIGTERM', cleanup);
129
+
130
+ return { stop: () => watcher.close() };
131
+ }
132
+
133
+ module.exports = { watch };