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,124 @@
|
|
|
1
|
+
# Publishing to npm
|
|
2
|
+
|
|
3
|
+
Step-by-step guide for maintainers.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- You have an account on [npmjs.com](https://www.npmjs.com)
|
|
10
|
+
- You are logged in: `npm whoami` should print your username
|
|
11
|
+
- If not: `npm login`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## First publish
|
|
16
|
+
|
|
17
|
+
### 1. Check the package name is available
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm view json-humanized
|
|
21
|
+
# If it returns "npm error 404" → name is free ✓
|
|
22
|
+
# If it returns package info → name is taken, update "name" in package.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. Review what will be published
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm pack --dry-run
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You should see only:
|
|
32
|
+
- `bin/cli.js`
|
|
33
|
+
- `src/**`
|
|
34
|
+
- `examples/*.json`, `examples/demo.js`
|
|
35
|
+
- `README.md`, `LICENSE`, `package.json`
|
|
36
|
+
|
|
37
|
+
NOT included (via `.npmignore`): `test/`, `.github/`, `docs/`, `node_modules/`
|
|
38
|
+
|
|
39
|
+
### 3. Run tests one final time
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm test
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 4. Publish
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm publish
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That's it. Your package is live at:
|
|
52
|
+
`https://www.npmjs.com/package/json-humanized`
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Publishing an update
|
|
57
|
+
|
|
58
|
+
### 1. Update CHANGELOG.md
|
|
59
|
+
|
|
60
|
+
Add an entry under a new version heading.
|
|
61
|
+
|
|
62
|
+
### 2. Bump the version
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Patch (1.0.0 → 1.0.1): bug fixes
|
|
66
|
+
npm version patch
|
|
67
|
+
|
|
68
|
+
# Minor (1.0.0 → 1.1.0): new features, backwards-compatible
|
|
69
|
+
npm version minor
|
|
70
|
+
|
|
71
|
+
# Major (1.0.0 → 2.0.0): breaking changes
|
|
72
|
+
npm version major
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This command automatically:
|
|
76
|
+
- Updates `version` in `package.json`
|
|
77
|
+
- Creates a git commit: `"1.0.1"`
|
|
78
|
+
- Creates a git tag: `"v1.0.1"`
|
|
79
|
+
|
|
80
|
+
### 3. Push to GitHub
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git push && git push --tags
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. Publish to npm
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm publish
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Publish with a tag (beta)
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm version prerelease --preid=beta # → 1.0.1-beta.0
|
|
98
|
+
npm publish --tag beta
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Users install it with: `npm install json-humanized@beta`
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Unpublish (within 72 hours only)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm unpublish json-humanized@1.0.0
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
After 72 hours npm does not allow unpublishing — deprecate instead:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm deprecate json-humanized@1.0.0 "Critical bug, use 1.0.1"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Checking download stats
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm info json-humanized
|
|
123
|
+
# or visit: https://www.npmjs.com/package/json-humanized
|
|
124
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data": [
|
|
3
|
+
{
|
|
4
|
+
"id": "order_001",
|
|
5
|
+
"status": "delivered",
|
|
6
|
+
"created_at": "2024-06-01T10:00:00Z",
|
|
7
|
+
"total": 129.95,
|
|
8
|
+
"customer_name": "Bob Martinez",
|
|
9
|
+
"items": [
|
|
10
|
+
{ "name": "Wireless Headphones", "qty": 1, "price": 89.99 },
|
|
11
|
+
{ "name": "USB-C Cable", "qty": 2, "price": 19.99 }
|
|
12
|
+
],
|
|
13
|
+
"shipping_address": {
|
|
14
|
+
"city": "Chicago",
|
|
15
|
+
"country": "US"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "order_002",
|
|
20
|
+
"status": "processing",
|
|
21
|
+
"created_at": "2024-06-28T09:15:00Z",
|
|
22
|
+
"total": 249.00,
|
|
23
|
+
"customer_name": "Clara Kim",
|
|
24
|
+
"items": [
|
|
25
|
+
{ "name": "Mechanical Keyboard", "qty": 1, "price": 249.00 }
|
|
26
|
+
],
|
|
27
|
+
"shipping_address": {
|
|
28
|
+
"city": "Austin",
|
|
29
|
+
"country": "US"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"meta": {
|
|
34
|
+
"total": 2,
|
|
35
|
+
"page": 1,
|
|
36
|
+
"per_page": 20
|
|
37
|
+
},
|
|
38
|
+
"links": {
|
|
39
|
+
"self": "https://api.example.com/orders?page=1",
|
|
40
|
+
"next": null
|
|
41
|
+
}
|
|
42
|
+
}
|
package/examples/demo.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// json-humanized — Programmatic API demo
|
|
4
|
+
|
|
5
|
+
const { humanize, humanizeFile, humanizeString } = require('../src/index');
|
|
6
|
+
|
|
7
|
+
async function demo() {
|
|
8
|
+
console.log('═'.repeat(60));
|
|
9
|
+
console.log(' json-humanized · Programmatic API Demo');
|
|
10
|
+
console.log('═'.repeat(60));
|
|
11
|
+
|
|
12
|
+
// ── 1. Humanize inline data ─────────────────────────────────────
|
|
13
|
+
console.log('\n📌 Example 1: Simple object\n');
|
|
14
|
+
const result1 = await humanize({
|
|
15
|
+
name: 'Alice',
|
|
16
|
+
age: 30,
|
|
17
|
+
email: 'alice@example.com',
|
|
18
|
+
premium: true,
|
|
19
|
+
score: 9.2,
|
|
20
|
+
});
|
|
21
|
+
console.log(result1);
|
|
22
|
+
|
|
23
|
+
// ── 2. Humanize from file ───────────────────────────────────────
|
|
24
|
+
console.log('\n📌 Example 2: From file (user-profile.json)\n');
|
|
25
|
+
const result2 = await humanizeFile('./examples/user-profile.json', {
|
|
26
|
+
format: 'plain',
|
|
27
|
+
});
|
|
28
|
+
console.log(result2);
|
|
29
|
+
|
|
30
|
+
// ── 3. Markdown format ──────────────────────────────────────────
|
|
31
|
+
console.log('\n📌 Example 3: Markdown output\n');
|
|
32
|
+
const result3 = await humanizeString(
|
|
33
|
+
JSON.stringify({ errors: [{ code: 404, message: 'Not found', path: '/api/users/999' }] }),
|
|
34
|
+
{ format: 'markdown' }
|
|
35
|
+
);
|
|
36
|
+
console.log(result3);
|
|
37
|
+
|
|
38
|
+
// ── 4. Story format ─────────────────────────────────────────────
|
|
39
|
+
console.log('\n📌 Example 4: Story format\n');
|
|
40
|
+
const result4 = await humanize(
|
|
41
|
+
{ city: 'Paris', population: 2161000, country: 'France', latitude: 48.8566, longitude: 2.3522 },
|
|
42
|
+
{ format: 'story' }
|
|
43
|
+
);
|
|
44
|
+
console.log(result4);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
demo().catch(err => {
|
|
48
|
+
console.error('Demo failed:', err.message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_8f2a1c9d",
|
|
4
|
+
"name": "Alice Rosenberg",
|
|
5
|
+
"email": "alice@example.com",
|
|
6
|
+
"age": 29,
|
|
7
|
+
"phone": "+1-555-0147",
|
|
8
|
+
"premium": true,
|
|
9
|
+
"created_at": "2023-06-15T08:30:00Z",
|
|
10
|
+
"address": {
|
|
11
|
+
"city": "San Francisco",
|
|
12
|
+
"country": "United States",
|
|
13
|
+
"zip": "94105"
|
|
14
|
+
},
|
|
15
|
+
"preferences": {
|
|
16
|
+
"theme": "dark",
|
|
17
|
+
"notifications": true,
|
|
18
|
+
"language": "en"
|
|
19
|
+
},
|
|
20
|
+
"tags": ["designer", "beta-tester", "early-adopter"]
|
|
21
|
+
},
|
|
22
|
+
"subscription": {
|
|
23
|
+
"plan": "Pro",
|
|
24
|
+
"status": "active",
|
|
25
|
+
"price": 49.99,
|
|
26
|
+
"billing_cycle": "monthly",
|
|
27
|
+
"next_billing_date": "2024-07-15T00:00:00Z",
|
|
28
|
+
"features": ["unlimited_projects", "ai_assistant", "priority_support"]
|
|
29
|
+
},
|
|
30
|
+
"stats": {
|
|
31
|
+
"total_projects": 42,
|
|
32
|
+
"storage_used_gb": 3.7,
|
|
33
|
+
"last_login": "2024-06-28T14:22:00Z",
|
|
34
|
+
"rating": 4.8
|
|
35
|
+
}
|
|
36
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Type definitions for json-humanized
|
|
2
|
+
// Project: https://github.com/AceAnomDev/json-humanized
|
|
3
|
+
|
|
4
|
+
export type Engine = 'local' | 'ai';
|
|
5
|
+
export type AIProvider = 'anthropic' | 'openai' | 'ollama';
|
|
6
|
+
export type Format = 'plain' | 'markdown' | 'story' | 'json';
|
|
7
|
+
export type Mode = 'structured' | 'prose' | 'story';
|
|
8
|
+
|
|
9
|
+
export interface HumanizeOptions {
|
|
10
|
+
/** Processing engine. Default: 'local' */
|
|
11
|
+
engine?: Engine;
|
|
12
|
+
|
|
13
|
+
/** AI provider (only used when engine = 'ai'). Default: 'anthropic' */
|
|
14
|
+
aiProvider?: AIProvider;
|
|
15
|
+
|
|
16
|
+
/** API key for Anthropic or OpenAI. Falls back to env variable. */
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
|
|
19
|
+
/** Output format. Default: 'plain' */
|
|
20
|
+
format?: Format;
|
|
21
|
+
|
|
22
|
+
/** Description style. Default: 'structured' */
|
|
23
|
+
mode?: Mode;
|
|
24
|
+
|
|
25
|
+
/** Natural language for output (AI mode). Default: 'English' */
|
|
26
|
+
lang?: string;
|
|
27
|
+
|
|
28
|
+
/** Context hint for the AI (e.g. "This is a Stripe webhook"). */
|
|
29
|
+
context?: string;
|
|
30
|
+
|
|
31
|
+
/** Source filename shown in the output header. */
|
|
32
|
+
filename?: string;
|
|
33
|
+
|
|
34
|
+
/** Max chars of JSON sent to AI. Default: 12000 */
|
|
35
|
+
maxChars?: number;
|
|
36
|
+
|
|
37
|
+
/** Path to a Handlebars (.hbs) template file for custom output. */
|
|
38
|
+
template?: string;
|
|
39
|
+
|
|
40
|
+
/** Enable AI response caching. Default: true */
|
|
41
|
+
cache?: boolean;
|
|
42
|
+
|
|
43
|
+
/** Cache TTL in seconds. Default: 3600 */
|
|
44
|
+
cacheTTL?: number;
|
|
45
|
+
|
|
46
|
+
/** Explicit path to a .jh.config.json file. Auto-detected if omitted. */
|
|
47
|
+
configPath?: string;
|
|
48
|
+
|
|
49
|
+
/** Ollama base URL. Default: 'http://localhost:11434' */
|
|
50
|
+
ollamaUrl?: string;
|
|
51
|
+
|
|
52
|
+
/** Ollama model name. Default: 'llama3' */
|
|
53
|
+
ollamaModel?: string;
|
|
54
|
+
|
|
55
|
+
/** OpenAI model. Default: 'gpt-4o-mini' */
|
|
56
|
+
openaiModel?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DiffOptions {
|
|
60
|
+
/** 'local' for rule-based diff, 'ai' for AI-powered diff. Default: 'local' */
|
|
61
|
+
engine?: Engine;
|
|
62
|
+
|
|
63
|
+
/** Output format. Default: 'plain' */
|
|
64
|
+
format?: 'plain' | 'markdown' | 'json';
|
|
65
|
+
|
|
66
|
+
/** Natural language for AI diff output. Default: 'English' */
|
|
67
|
+
lang?: string;
|
|
68
|
+
|
|
69
|
+
/** Context hint for AI diff. */
|
|
70
|
+
context?: string;
|
|
71
|
+
|
|
72
|
+
/** API key for AI diff mode. */
|
|
73
|
+
apiKey?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CacheStats {
|
|
77
|
+
entries: number;
|
|
78
|
+
totalBytes: number;
|
|
79
|
+
dir: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface DiffModule {
|
|
83
|
+
diff(a: unknown, b: unknown, options?: DiffOptions): Promise<string>;
|
|
84
|
+
diffFiles(fileA: string, fileB: string, options?: DiffOptions): Promise<string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CacheModule {
|
|
88
|
+
buildCacheKey(data: unknown, options?: object): string;
|
|
89
|
+
readCache(key: string, ttl?: number): string | null;
|
|
90
|
+
writeCache(key: string, result: string): void;
|
|
91
|
+
clearCache(): number;
|
|
92
|
+
cacheStats(): CacheStats;
|
|
93
|
+
withCache(data: unknown, options: object, fn: () => Promise<string>, enabled?: boolean): Promise<string>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ConfigModule {
|
|
97
|
+
loadConfig(configPath?: string): { config: object; configPath: string | null };
|
|
98
|
+
resolveLabel(key: string, fieldLabels?: Record<string, string | null>): string | null;
|
|
99
|
+
resolveType(key: string, fieldTypes?: Record<string, string>): string | null;
|
|
100
|
+
isHidden(key: string, hiddenFields?: string[]): boolean;
|
|
101
|
+
generateExampleConfig(): string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Humanize a parsed JavaScript value (object, array, primitive).
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* import { humanize } from 'json-humanized';
|
|
109
|
+
* const text = await humanize({ name: 'Alice', age: 30 });
|
|
110
|
+
*/
|
|
111
|
+
export function humanize(data: unknown, options?: HumanizeOptions): Promise<string>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Read a JSON/YAML/TOML file from disk and humanize it.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* import { humanizeFile } from 'json-humanized';
|
|
118
|
+
* const text = await humanizeFile('./users.json', { format: 'markdown' });
|
|
119
|
+
*/
|
|
120
|
+
export function humanizeFile(filePath: string, options?: HumanizeOptions): Promise<string>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse and humanize a raw JSON/YAML/TOML string.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* import { humanizeString } from 'json-humanized';
|
|
127
|
+
* const text = await humanizeString('{"key":"value"}');
|
|
128
|
+
*/
|
|
129
|
+
export function humanizeString(rawString: string, options?: HumanizeOptions): Promise<string>;
|
|
130
|
+
|
|
131
|
+
/** Diff utilities */
|
|
132
|
+
export const diff: DiffModule;
|
|
133
|
+
|
|
134
|
+
/** Cache utilities */
|
|
135
|
+
export const cache: CacheModule;
|
|
136
|
+
|
|
137
|
+
/** Config utilities */
|
|
138
|
+
export const config: ConfigModule;
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "json-humanized",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Transform any JSON/YAML/TOML into human-readable prose — powered by Claude AI, OpenAI, Ollama, or built-in rule-based engine",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"json-humanized": "./bin/cli.js",
|
|
9
|
+
"jh": "./bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"type": "commonjs",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node test/index.test.js",
|
|
14
|
+
"example": "node examples/demo.js",
|
|
15
|
+
"lint": "echo 'No linter configured'",
|
|
16
|
+
"cache:clear": "node -e \"require('./src/cache').clearCache(); console.log('Cache cleared')\"",
|
|
17
|
+
"cache:stats": "node -e \"const s=require('./src/cache').cacheStats(); console.log(s)\""
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"json",
|
|
21
|
+
"yaml",
|
|
22
|
+
"toml",
|
|
23
|
+
"human-readable",
|
|
24
|
+
"nlp",
|
|
25
|
+
"cli",
|
|
26
|
+
"ai",
|
|
27
|
+
"claude",
|
|
28
|
+
"openai",
|
|
29
|
+
"ollama",
|
|
30
|
+
"text-generation",
|
|
31
|
+
"data-description",
|
|
32
|
+
"json-to-text",
|
|
33
|
+
"diff",
|
|
34
|
+
"watch",
|
|
35
|
+
"developer-tools"
|
|
36
|
+
],
|
|
37
|
+
"author": "json-humanized contributors",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"chalk": "4.1.2",
|
|
41
|
+
"commander": "11.1.0",
|
|
42
|
+
"ora": "5.4.1"
|
|
43
|
+
},
|
|
44
|
+
"optionalDependencies": {
|
|
45
|
+
"@anthropic-ai/sdk": "^0.20.0",
|
|
46
|
+
"openai": "^4.0.0",
|
|
47
|
+
"js-yaml": "^4.1.0",
|
|
48
|
+
"@iarna/toml": "^2.2.5",
|
|
49
|
+
"handlebars": "^4.7.8"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=14.0.0"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"bin/",
|
|
56
|
+
"src/",
|
|
57
|
+
"examples/",
|
|
58
|
+
"docs/",
|
|
59
|
+
"index.d.ts",
|
|
60
|
+
"README.md",
|
|
61
|
+
"LICENSE"
|
|
62
|
+
],
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "https://github.com/AceAnomDev/json-humanized.git"
|
|
66
|
+
},
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/AceAnomDev/json-humanized/issues"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://github.com/AceAnomDev/json-humanized#readme"
|
|
71
|
+
}
|
package/src/cache.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cache.js — file-based cache for AI responses.
|
|
5
|
+
*
|
|
6
|
+
* Saves API tokens by reusing results for identical (JSON + options) combos.
|
|
7
|
+
* Cache files are stored in ~/.jh-cache/ (or $JH_CACHE_DIR).
|
|
8
|
+
* Each entry expires after `ttl` seconds (default 3600 = 1 hour).
|
|
9
|
+
*
|
|
10
|
+
* No external dependencies — uses built-in crypto + fs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
|
|
18
|
+
// ─── cache directory ─────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function getCacheDir() {
|
|
21
|
+
return process.env.JH_CACHE_DIR || path.join(os.homedir(), '.jh-cache');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ensureCacheDir() {
|
|
25
|
+
const dir = getCacheDir();
|
|
26
|
+
if (!fs.existsSync(dir)) {
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── cache key ───────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a stable cache key from JSON content + relevant options.
|
|
36
|
+
* We hash the combination so filenames stay short and filesystem-safe.
|
|
37
|
+
*/
|
|
38
|
+
function buildCacheKey(data, options = {}) {
|
|
39
|
+
const { engine, format, lang, context, aiProvider, mode } = options;
|
|
40
|
+
|
|
41
|
+
const payload = JSON.stringify({
|
|
42
|
+
data,
|
|
43
|
+
engine: engine || 'local',
|
|
44
|
+
format: format || 'plain',
|
|
45
|
+
lang: lang || 'English',
|
|
46
|
+
context: context || '',
|
|
47
|
+
aiProvider: aiProvider || 'anthropic',
|
|
48
|
+
mode: mode || 'structured',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── read / write ────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Try to read a cached result.
|
|
58
|
+
* Returns the cached string, or null if miss / expired.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} key from buildCacheKey()
|
|
61
|
+
* @param {number} ttl seconds; 0 = never expire
|
|
62
|
+
* @returns {string|null}
|
|
63
|
+
*/
|
|
64
|
+
function readCache(key, ttl = 3600) {
|
|
65
|
+
try {
|
|
66
|
+
const file = path.join(getCacheDir(), `${key}.json`);
|
|
67
|
+
if (!fs.existsSync(file)) return null;
|
|
68
|
+
|
|
69
|
+
const entry = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
70
|
+
|
|
71
|
+
if (ttl > 0) {
|
|
72
|
+
const ageSeconds = (Date.now() - entry.savedAt) / 1000;
|
|
73
|
+
if (ageSeconds > ttl) {
|
|
74
|
+
fs.unlinkSync(file); // clean up expired entry
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return entry.result;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Write a result to the cache.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} key
|
|
89
|
+
* @param {string} result the humanized text to cache
|
|
90
|
+
*/
|
|
91
|
+
function writeCache(key, result) {
|
|
92
|
+
try {
|
|
93
|
+
ensureCacheDir();
|
|
94
|
+
const file = path.join(getCacheDir(), `${key}.json`);
|
|
95
|
+
const entry = { savedAt: Date.now(), result };
|
|
96
|
+
fs.writeFileSync(file, JSON.stringify(entry), 'utf8');
|
|
97
|
+
} catch {
|
|
98
|
+
// Cache write failures are silent — the caller still gets a valid result
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── cache management ────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Delete all cache entries.
|
|
106
|
+
* @returns {number} number of files deleted
|
|
107
|
+
*/
|
|
108
|
+
function clearCache() {
|
|
109
|
+
const dir = getCacheDir();
|
|
110
|
+
if (!fs.existsSync(dir)) return 0;
|
|
111
|
+
|
|
112
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
113
|
+
for (const f of files) {
|
|
114
|
+
try { fs.unlinkSync(path.join(dir, f)); } catch {}
|
|
115
|
+
}
|
|
116
|
+
return files.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Return cache statistics.
|
|
121
|
+
* @returns {{ entries: number, totalBytes: number, dir: string }}
|
|
122
|
+
*/
|
|
123
|
+
function cacheStats() {
|
|
124
|
+
const dir = getCacheDir();
|
|
125
|
+
if (!fs.existsSync(dir)) return { entries: 0, totalBytes: 0, dir };
|
|
126
|
+
|
|
127
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
128
|
+
let totalBytes = 0;
|
|
129
|
+
|
|
130
|
+
for (const f of files) {
|
|
131
|
+
try {
|
|
132
|
+
totalBytes += fs.statSync(path.join(dir, f)).size;
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { entries: files.length, totalBytes, dir };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── wrapped fetch helper ────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Call `fn` with caching: return cached result if available, otherwise
|
|
143
|
+
* call `fn`, cache its output, and return it.
|
|
144
|
+
*
|
|
145
|
+
* @param {*} data JSON data (used to build the cache key)
|
|
146
|
+
* @param {object} options humanize options (used to build the cache key)
|
|
147
|
+
* @param {Function} fn async () => string — the real API call
|
|
148
|
+
* @param {boolean} [enabled] pass false to bypass cache entirely
|
|
149
|
+
* @returns {Promise<string>}
|
|
150
|
+
*/
|
|
151
|
+
async function withCache(data, options, fn, enabled = true) {
|
|
152
|
+
if (!enabled) return fn();
|
|
153
|
+
|
|
154
|
+
const ttl = options.cacheTTL != null ? options.cacheTTL : 3600;
|
|
155
|
+
const key = buildCacheKey(data, options);
|
|
156
|
+
|
|
157
|
+
const cached = readCache(key, ttl);
|
|
158
|
+
if (cached !== null) return cached;
|
|
159
|
+
|
|
160
|
+
const result = await fn();
|
|
161
|
+
writeCache(key, result);
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
buildCacheKey,
|
|
167
|
+
readCache,
|
|
168
|
+
writeCache,
|
|
169
|
+
clearCache,
|
|
170
|
+
cacheStats,
|
|
171
|
+
withCache,
|
|
172
|
+
};
|