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 +21 -0
- package/README.md +351 -0
- package/bin/cli.js +319 -0
- package/docs/ARCHITECTURE.md +139 -0
- package/docs/DEMO.html +461 -0
- package/docs/PUBLISHING.md +124 -0
- package/examples/api-response.json +42 -0
- package/examples/demo.js +50 -0
- package/examples/user-profile.json +36 -0
- package/index.d.ts +138 -0
- package/package.json +71 -0
- package/src/cache.js +172 -0
- package/src/config.js +259 -0
- package/src/diff.js +284 -0
- package/src/formatters/index.js +113 -0
- package/src/formatters/template.js +132 -0
- package/src/humanizer.js +307 -0
- package/src/index.js +157 -0
- package/src/parsers/index.js +119 -0
- package/src/strategies/ai.js +108 -0
- package/src/strategies/ollama.js +135 -0
- package/src/strategies/openai.js +82 -0
- package/src/watch.js +133 -0
|
@@ -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 };
|