exai 1.6.2 → 1.7.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/dist/ai/cache.d.ts +65 -0
- package/dist/ai/cache.d.ts.map +1 -0
- package/dist/ai/cache.js +197 -0
- package/dist/ai/cache.js.map +1 -0
- package/dist/ai/config.d.ts +3 -0
- package/dist/ai/config.d.ts.map +1 -1
- package/dist/ai/config.js +8 -2
- package/dist/ai/config.js.map +1 -1
- package/dist/ai/context-gatherer.d.ts +6 -6
- package/dist/ai/context-gatherer.d.ts.map +1 -1
- package/dist/ai/context-gatherer.js +15 -52
- package/dist/ai/context-gatherer.js.map +1 -1
- package/dist/ai/folder-filter.d.ts +1 -1
- package/dist/ai/folder-filter.d.ts.map +1 -1
- package/dist/ai/folder-filter.js +2 -1
- package/dist/ai/folder-filter.js.map +1 -1
- package/dist/ai/openrouter.d.ts +7 -3
- package/dist/ai/openrouter.d.ts.map +1 -1
- package/dist/ai/openrouter.js +140 -86
- package/dist/ai/openrouter.js.map +1 -1
- package/dist/ai/query-cache.d.ts +9 -24
- package/dist/ai/query-cache.d.ts.map +1 -1
- package/dist/ai/query-cache.js +21 -270
- package/dist/ai/query-cache.js.map +1 -1
- package/dist/cli.js +111 -41
- package/dist/cli.js.map +1 -1
- package/dist/generator/excalidraw-generator.d.ts.map +1 -1
- package/dist/generator/excalidraw-generator.js +41 -2
- package/dist/generator/excalidraw-generator.js.map +1 -1
- package/dist/layout/arrow-router.d.ts +8 -0
- package/dist/layout/arrow-router.d.ts.map +1 -1
- package/dist/layout/arrow-router.js +23 -0
- package/dist/layout/arrow-router.js.map +1 -1
- package/package.json +1 -1
package/dist/ai/query-cache.js
CHANGED
|
@@ -1,274 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Query Cache -
|
|
2
|
+
* Query Cache - compatibility wrapper around the unified ExaiCache.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*/
|
|
25
|
-
function generateCacheKey(prompt, model, temperature, format, context) {
|
|
26
|
-
const hash = createHash('sha256');
|
|
27
|
-
hash.update(prompt);
|
|
28
|
-
hash.update(model);
|
|
29
|
-
hash.update(String(temperature));
|
|
30
|
-
hash.update(format);
|
|
31
|
-
if (context) {
|
|
32
|
-
hash.update(context);
|
|
33
|
-
}
|
|
34
|
-
return hash.digest('hex');
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Generate shorter hash for context (for logging)
|
|
38
|
-
*/
|
|
39
|
-
function generateContextHash(context) {
|
|
40
|
-
return createHash('sha256').update(context).digest('hex').slice(0, 8);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Ensure cache directory exists
|
|
44
|
-
*/
|
|
45
|
-
function ensureCacheDir(cacheDir) {
|
|
46
|
-
if (!existsSync(cacheDir)) {
|
|
47
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Get cache file path for a given key
|
|
52
|
-
*/
|
|
53
|
-
function getCacheFilePath(cacheDir, key) {
|
|
54
|
-
return join(cacheDir, `${key}.cache`);
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Check if a cache entry is expired
|
|
58
|
-
*/
|
|
59
|
-
function isExpired(timestamp, ttlDays) {
|
|
60
|
-
const now = Date.now();
|
|
61
|
-
const age = now - timestamp;
|
|
62
|
-
const maxAge = ttlDays * 24 * 60 * 60 * 1000; // Convert days to milliseconds
|
|
63
|
-
return age > maxAge;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Get cached response if it exists and is not expired
|
|
67
|
-
*/
|
|
68
|
-
export function getCachedResponse(prompt, model, temperature, format, context, options = {}) {
|
|
69
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
70
|
-
const key = generateCacheKey(prompt, model, temperature, format, context);
|
|
71
|
-
ensureCacheDir(opts.cacheDir);
|
|
72
|
-
const cacheFile = getCacheFilePath(opts.cacheDir, key);
|
|
73
|
-
if (opts.verbose) {
|
|
74
|
-
console.log(` Cache directory: ${opts.cacheDir}`);
|
|
75
|
-
console.log(` Cache key: ${key.slice(0, 16)}...`);
|
|
76
|
-
console.log(` Cache file: ${cacheFile}`);
|
|
77
|
-
}
|
|
78
|
-
if (!existsSync(cacheFile)) {
|
|
79
|
-
if (opts.verbose) {
|
|
80
|
-
console.log(' Cache miss: No cached response found');
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
const content = readFileSync(cacheFile, 'utf-8');
|
|
86
|
-
const entry = JSON.parse(content);
|
|
87
|
-
// Check if expired
|
|
88
|
-
if (isExpired(entry.timestamp, opts.ttlDays)) {
|
|
89
|
-
if (opts.verbose) {
|
|
90
|
-
console.log(' Cache miss: Entry expired');
|
|
91
|
-
}
|
|
92
|
-
// Clean up expired entry
|
|
93
|
-
try {
|
|
94
|
-
unlinkSync(cacheFile);
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// Ignore cleanup errors
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
// Validate entry matches
|
|
102
|
-
if (entry.prompt !== prompt ||
|
|
103
|
-
entry.model !== model ||
|
|
104
|
-
entry.temperature !== temperature ||
|
|
105
|
-
entry.format !== format) {
|
|
106
|
-
if (opts.verbose) {
|
|
107
|
-
console.log(' Cache miss: Parameters mismatch');
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
const age = Math.floor((Date.now() - entry.timestamp) / 1000 / 60); // minutes
|
|
112
|
-
if (opts.verbose) {
|
|
113
|
-
console.log(` Cache hit: Response found (age: ${age}m)`);
|
|
114
|
-
}
|
|
115
|
-
return entry.response;
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
if (opts.verbose) {
|
|
119
|
-
console.log(` Cache error: ${error instanceof Error ? error.message : error}`);
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Cache a response
|
|
126
|
-
*/
|
|
127
|
-
export function cacheResponse(prompt, model, temperature, format, response, context, options = {}) {
|
|
128
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
129
|
-
const key = generateCacheKey(prompt, model, temperature, format, context);
|
|
130
|
-
ensureCacheDir(opts.cacheDir);
|
|
131
|
-
const entry = {
|
|
132
|
-
key,
|
|
133
|
-
prompt,
|
|
134
|
-
model,
|
|
135
|
-
temperature,
|
|
136
|
-
format,
|
|
137
|
-
response,
|
|
138
|
-
timestamp: Date.now(),
|
|
139
|
-
contextHash: context ? generateContextHash(context) : undefined,
|
|
140
|
-
};
|
|
141
|
-
const cacheFile = getCacheFilePath(opts.cacheDir, key);
|
|
142
|
-
try {
|
|
143
|
-
writeFileSync(cacheFile, JSON.stringify(entry, null, 2), 'utf-8');
|
|
144
|
-
if (opts.verbose) {
|
|
145
|
-
console.log(` Response cached successfully`);
|
|
146
|
-
console.log(` Cache file: ${cacheFile}`);
|
|
147
|
-
}
|
|
148
|
-
// Clean up old entries if we exceed maxEntries
|
|
149
|
-
cleanupOldEntries(opts.cacheDir, opts.maxEntries, opts.verbose);
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
if (opts.verbose) {
|
|
153
|
-
console.log(` Cache write error: ${error instanceof Error ? error.message : error}`);
|
|
154
|
-
}
|
|
155
|
-
// Don't fail if caching fails
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Clean up old cache entries to stay within maxEntries limit
|
|
160
|
-
*/
|
|
161
|
-
function cleanupOldEntries(cacheDir, maxEntries, verbose) {
|
|
162
|
-
try {
|
|
163
|
-
const files = readdirSync(cacheDir)
|
|
164
|
-
.filter(f => f.endsWith('.cache'))
|
|
165
|
-
.map(f => ({
|
|
166
|
-
path: join(cacheDir, f),
|
|
167
|
-
mtime: statSync(join(cacheDir, f)).mtime.getTime(),
|
|
168
|
-
}))
|
|
169
|
-
.sort((a, b) => b.mtime - a.mtime); // Sort by modified time, newest first
|
|
170
|
-
if (files.length <= maxEntries) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
// Remove oldest entries
|
|
174
|
-
const toRemove = files.slice(maxEntries);
|
|
175
|
-
for (const file of toRemove) {
|
|
176
|
-
try {
|
|
177
|
-
unlinkSync(file.path);
|
|
178
|
-
if (verbose) {
|
|
179
|
-
console.log(` Cleaned up old cache entry: ${file.path}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
// Ignore cleanup errors
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
if (verbose) {
|
|
189
|
-
console.log(` Cache cleanup error: ${error instanceof Error ? error.message : error}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Clear all cache entries
|
|
195
|
-
*/
|
|
196
|
-
export function clearCache(options = {}) {
|
|
197
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
198
|
-
if (!existsSync(opts.cacheDir)) {
|
|
199
|
-
return 0;
|
|
200
|
-
}
|
|
201
|
-
try {
|
|
202
|
-
const files = readdirSync(opts.cacheDir).filter(f => f.endsWith('.cache'));
|
|
203
|
-
let cleared = 0;
|
|
204
|
-
for (const file of files) {
|
|
205
|
-
try {
|
|
206
|
-
unlinkSync(join(opts.cacheDir, file));
|
|
207
|
-
cleared++;
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
// Ignore errors
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return cleared;
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
if (opts.verbose) {
|
|
217
|
-
console.log(` Cache clear error: ${error instanceof Error ? error.message : error}`);
|
|
218
|
-
}
|
|
219
|
-
return 0;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Get cache statistics
|
|
224
|
-
*/
|
|
225
|
-
export function getCacheStats(options = {}) {
|
|
226
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
227
|
-
if (!existsSync(opts.cacheDir)) {
|
|
228
|
-
return {
|
|
229
|
-
totalEntries: 0,
|
|
230
|
-
totalSize: 0,
|
|
231
|
-
oldestEntry: null,
|
|
232
|
-
newestEntry: null,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
try {
|
|
236
|
-
const files = readdirSync(opts.cacheDir)
|
|
237
|
-
.filter(f => f.endsWith('.cache'))
|
|
238
|
-
.map(f => join(opts.cacheDir, f));
|
|
239
|
-
let totalSize = 0;
|
|
240
|
-
let oldestEntry = null;
|
|
241
|
-
let newestEntry = null;
|
|
242
|
-
for (const file of files) {
|
|
243
|
-
try {
|
|
244
|
-
const stats = statSync(file);
|
|
245
|
-
totalSize += stats.size;
|
|
246
|
-
const mtime = stats.mtime.getTime();
|
|
247
|
-
if (oldestEntry === null || mtime < oldestEntry) {
|
|
248
|
-
oldestEntry = mtime;
|
|
249
|
-
}
|
|
250
|
-
if (newestEntry === null || mtime > newestEntry) {
|
|
251
|
-
newestEntry = mtime;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
// Ignore errors
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return {
|
|
259
|
-
totalEntries: files.length,
|
|
260
|
-
totalSize,
|
|
261
|
-
oldestEntry,
|
|
262
|
-
newestEntry,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
return {
|
|
267
|
-
totalEntries: 0,
|
|
268
|
-
totalSize: 0,
|
|
269
|
-
oldestEntry: null,
|
|
270
|
-
newestEntry: null,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
4
|
+
* Public API is unchanged so existing call-sites continue to compile.
|
|
5
|
+
* Configuration (TTL, maxEntries, verbose) is now managed on the shared
|
|
6
|
+
* `cache` singleton in cache.ts — the options parameters below are accepted
|
|
7
|
+
* for backward compatibility but are no longer used.
|
|
8
|
+
*/
|
|
9
|
+
import { cache, makeKey } from './cache.js';
|
|
10
|
+
// ── Delegating functions ──────────────────────────────────────────────────────
|
|
11
|
+
export function getCachedResponse(prompt, model, temperature, format, context, _options = {}) {
|
|
12
|
+
const key = makeKey(prompt, model, temperature, format, context ?? '');
|
|
13
|
+
return cache.get('llm', key);
|
|
14
|
+
}
|
|
15
|
+
export function cacheResponse(prompt, model, temperature, format, response, context, _options = {}) {
|
|
16
|
+
const key = makeKey(prompt, model, temperature, format, context ?? '');
|
|
17
|
+
cache.set('llm', key, response);
|
|
18
|
+
}
|
|
19
|
+
export function clearCache(_options = {}) {
|
|
20
|
+
return cache.clear();
|
|
21
|
+
}
|
|
22
|
+
export function getCacheStats(_options = {}) {
|
|
23
|
+
return cache.stats();
|
|
273
24
|
}
|
|
274
25
|
//# sourceMappingURL=query-cache.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-cache.js","sourceRoot":"","sources":["../../src/ai/query-cache.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"query-cache.js","sourceRoot":"","sources":["../../src/ai/query-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAsB5C,iFAAiF;AAEjF,MAAM,UAAU,iBAAiB,CAC7B,MAAc,EACd,KAAa,EACb,WAAmB,EACnB,MAAc,EACd,OAAgB,EAChB,WAAyB,EAAE;IAE3B,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;IACvE,OAAO,KAAK,CAAC,GAAG,CAAS,KAAK,EAAE,GAAG,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,aAAa,CACzB,MAAc,EACd,KAAa,EACb,WAAmB,EACnB,MAAc,EACd,QAAgB,EAChB,OAAgB,EAChB,WAAyB,EAAE;IAE3B,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;IACvE,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,WAAyB,EAAE;IAClD,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,WAAyB,EAAE;IAMrD,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC;AACzB,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { layoutGraph } from './layout/elk-layout.js';
|
|
|
16
16
|
import { generateExcalidraw, serializeExcalidraw } from './generator/excalidraw-generator.js';
|
|
17
17
|
import { generateFlowchartInput } from './ai/openrouter.js';
|
|
18
18
|
import { gatherContext } from './ai/context-gatherer.js';
|
|
19
|
-
import {
|
|
19
|
+
import { cache } from './ai/cache.js';
|
|
20
20
|
import { loadConfig, CONFIG_TEMPLATE } from './ai/config.js';
|
|
21
21
|
const require = createRequire(import.meta.url);
|
|
22
22
|
const pkg = require('../package.json');
|
|
@@ -230,22 +230,34 @@ program
|
|
|
230
230
|
program
|
|
231
231
|
.command('parse')
|
|
232
232
|
.description('Parse and validate input without generating output')
|
|
233
|
-
.argument('
|
|
233
|
+
.argument('[input]', 'Input file path')
|
|
234
234
|
.option('-f, --format <type>', 'Input format: dsl, json, dot (default: dsl)', 'dsl')
|
|
235
|
+
.option('--inline <dsl>', 'Inline DSL/DOT string')
|
|
236
|
+
.option('--stdin', 'Read input from stdin')
|
|
235
237
|
.action((inputFile, options, command) => {
|
|
236
238
|
try {
|
|
237
|
-
|
|
239
|
+
let input;
|
|
238
240
|
let format = options.format;
|
|
239
241
|
const formatExplicitlySet = command.getOptionValueSource('format') === 'cli';
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
if (options.inline) {
|
|
243
|
+
input = options.inline;
|
|
244
|
+
}
|
|
245
|
+
else if (options.stdin) {
|
|
246
|
+
input = readFileSync(0, 'utf-8');
|
|
247
|
+
}
|
|
248
|
+
else if (inputFile) {
|
|
249
|
+
input = readFileSync(inputFile, 'utf-8');
|
|
250
|
+
if (!formatExplicitlySet) {
|
|
251
|
+
if (inputFile.endsWith('.json'))
|
|
252
|
+
format = 'json';
|
|
253
|
+
else if (inputFile.endsWith('.dot') || inputFile.endsWith('.gv'))
|
|
254
|
+
format = 'dot';
|
|
247
255
|
}
|
|
248
256
|
}
|
|
257
|
+
else {
|
|
258
|
+
console.error('Error: No input provided. Use --inline, --stdin, or provide an input file.');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
249
261
|
// Parse input
|
|
250
262
|
let graph;
|
|
251
263
|
if (format === 'json') {
|
|
@@ -260,6 +272,8 @@ program
|
|
|
260
272
|
console.log('Parse successful!');
|
|
261
273
|
console.log(` Nodes: ${graph.nodes.length}`);
|
|
262
274
|
console.log(` Edges: ${graph.edges.length}`);
|
|
275
|
+
if (graph.groups?.length)
|
|
276
|
+
console.log(` Groups: ${graph.groups.length}`);
|
|
263
277
|
console.log(` Direction: ${graph.options.direction}`);
|
|
264
278
|
console.log('\nNodes:');
|
|
265
279
|
for (const node of graph.nodes) {
|
|
@@ -272,6 +286,15 @@ program
|
|
|
272
286
|
const label = edge.label ? ` "${edge.label}"` : '';
|
|
273
287
|
console.log(` - ${sourceNode?.label} ->${label} ${targetNode?.label}`);
|
|
274
288
|
}
|
|
289
|
+
if (graph.groups?.length) {
|
|
290
|
+
console.log('\nGroups:');
|
|
291
|
+
for (const group of graph.groups) {
|
|
292
|
+
const memberLabels = group.nodeIds
|
|
293
|
+
.map((id) => graph.nodes.find((n) => n.id === id)?.label ?? id)
|
|
294
|
+
.join(', ');
|
|
295
|
+
console.log(` - [${group.id}] "${group.label}" (${memberLabels})`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
275
298
|
}
|
|
276
299
|
catch (error) {
|
|
277
300
|
console.error('Parse error:', error instanceof Error ? error.message : error);
|
|
@@ -297,7 +320,9 @@ program
|
|
|
297
320
|
.option('--allow-test-files', 'Include test files in context (default: excluded)')
|
|
298
321
|
.option('--no-compress', 'Disable context compression')
|
|
299
322
|
.option('--compress-mode <mode>', 'Compression mode: balanced, aggressive, minimal (default: balanced)', 'balanced')
|
|
300
|
-
.option('--no-cache', 'Disable LLM response
|
|
323
|
+
.option('--no-cache', 'Disable LLM response cache')
|
|
324
|
+
.option('--no-context-cache', 'Disable context-gather cache (re-gather files every run)')
|
|
325
|
+
.option('--redraw', 'Re-render using cached context + cached LLM response — no API call (fails if either cache is cold)')
|
|
301
326
|
.option('--only-context', 'Only gather and display context, do not generate diagram')
|
|
302
327
|
.option('--config-path <path>', 'Path to config JSON file')
|
|
303
328
|
.option('--verbose', 'Verbose output')
|
|
@@ -307,6 +332,13 @@ program
|
|
|
307
332
|
// Load config file if provided and merge with CLI options
|
|
308
333
|
// Priority: CLI flags > env/.env > config file > hardcoded defaults
|
|
309
334
|
let config = {};
|
|
335
|
+
const DEFAULT_CONFIG_NAME = 'exai.config.json';
|
|
336
|
+
const autoConfigPath = resolve(DEFAULT_CONFIG_NAME);
|
|
337
|
+
let configAutoDetected = false;
|
|
338
|
+
if (!options.configPath && existsSync(autoConfigPath)) {
|
|
339
|
+
options.configPath = autoConfigPath;
|
|
340
|
+
configAutoDetected = true;
|
|
341
|
+
}
|
|
310
342
|
if (options.configPath) {
|
|
311
343
|
config = loadConfig(options.configPath);
|
|
312
344
|
// Helper: use CLI value if explicitly set, otherwise config value
|
|
@@ -340,13 +372,26 @@ program
|
|
|
340
372
|
// Cache
|
|
341
373
|
if (config.cache !== undefined && src('cache') !== 'cli')
|
|
342
374
|
options.cache = config.cache;
|
|
375
|
+
if (config.contextCache !== undefined && src('contextCache') !== 'cli')
|
|
376
|
+
options.contextCache = config.contextCache;
|
|
343
377
|
// Misc
|
|
344
378
|
if (config.verbose !== undefined && src('verbose') !== 'cli')
|
|
345
379
|
options.verbose = config.verbose;
|
|
346
380
|
if (options.verbose) {
|
|
347
|
-
|
|
381
|
+
const label = configAutoDetected ? '📄 Config auto-detected' : '📄 Config loaded';
|
|
382
|
+
console.log(`${label}: ${resolve(options.configPath)}`);
|
|
383
|
+
const applied = Object.keys(config).filter(k => k !== 'apiKey');
|
|
384
|
+
if (applied.length > 0) {
|
|
385
|
+
console.log(` Keys applied: ${applied.join(', ')}`);
|
|
386
|
+
}
|
|
348
387
|
}
|
|
349
388
|
}
|
|
389
|
+
// Configure the shared cache singleton (TTL, maxEntries, verbose)
|
|
390
|
+
cache.configure({
|
|
391
|
+
ttlDays: config.cacheTtlDays,
|
|
392
|
+
maxEntries: config.cacheMaxEntries,
|
|
393
|
+
verbose: options.verbose,
|
|
394
|
+
});
|
|
350
395
|
const format = options.format;
|
|
351
396
|
// Validate format
|
|
352
397
|
if (format !== 'dsl' && format !== 'json') {
|
|
@@ -387,11 +432,21 @@ program
|
|
|
387
432
|
console.log(`🧪 Test files: included`);
|
|
388
433
|
}
|
|
389
434
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
435
|
+
// --redraw requires context paths
|
|
436
|
+
if (options.redraw && (!options.context || options.context.length === 0)) {
|
|
437
|
+
console.error('Error: --redraw requires at least one context path via -c or config "context".');
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
390
440
|
// Gather context if provided
|
|
391
441
|
let contextString;
|
|
442
|
+
if (options.onlyContext && options.context.length === 0) {
|
|
443
|
+
console.error('Error: --only-context requires at least one context path via -c or config "context".');
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
392
446
|
if (options.context && options.context.length > 0) {
|
|
393
447
|
try {
|
|
394
|
-
|
|
448
|
+
const isRedraw = options.redraw === true;
|
|
449
|
+
console.log(`📂 [1/5] ${isRedraw ? 'Loading Context from cache...' : 'Gathering Context...'}`);
|
|
395
450
|
if (options.verbose) {
|
|
396
451
|
console.log(` CLI received context paths:`);
|
|
397
452
|
options.context.forEach((p) => console.log(` - ${p}`));
|
|
@@ -403,41 +458,45 @@ program
|
|
|
403
458
|
const compressionOptions = config.compressOptions
|
|
404
459
|
? { ...modeDefaults, ...config.compressOptions }
|
|
405
460
|
: modeDefaults;
|
|
461
|
+
// context cache: disabled by --no-context-cache, forced cache-only by --redraw
|
|
462
|
+
const useContextCache = options.contextCache !== false && !isRedraw;
|
|
463
|
+
const contextCacheOnly = isRedraw;
|
|
406
464
|
const contextResult = await gatherContext(options.context, {
|
|
407
465
|
apiKey: options.apiKey,
|
|
408
466
|
filterModel: config.filterModel,
|
|
409
467
|
verbose: options.verbose,
|
|
410
468
|
compress: options.compress !== false,
|
|
411
469
|
compressOptions: compressionOptions,
|
|
412
|
-
useCache:
|
|
470
|
+
useCache: useContextCache,
|
|
471
|
+
cacheOnly: contextCacheOnly,
|
|
413
472
|
excludePatterns: options.exclude,
|
|
414
473
|
allowTestFiles: options.allowTestFiles ?? false,
|
|
415
474
|
maxFileSize: config.maxFileSize,
|
|
416
475
|
maxDepth: config.maxDepth,
|
|
417
476
|
maxTreeItems: config.maxTreeItems,
|
|
418
|
-
|
|
419
|
-
cacheMaxEntries: config.cacheMaxEntries,
|
|
477
|
+
timeoutMs: config.timeoutSecs !== undefined ? config.timeoutSecs * 1000 : undefined,
|
|
420
478
|
});
|
|
479
|
+
if (contextResult === null) {
|
|
480
|
+
console.error('❌ --redraw failed: context cache is cold for these paths.');
|
|
481
|
+
console.error(' Run without --redraw first to populate the cache.');
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
421
484
|
contextString = contextResult.markdown;
|
|
422
485
|
const contextTime = Date.now() - contextStart;
|
|
423
|
-
let contextMsg = `✓ Context gathered (${(contextString.length / 1024).toFixed(1)}KB in ${contextTime}ms)`;
|
|
486
|
+
let contextMsg = `✓ Context ${contextResult.fromCache ? '[CACHE]' : 'gathered'} (${(contextString.length / 1024).toFixed(1)}KB in ${contextTime}ms)`;
|
|
424
487
|
if (contextResult.compression && options.compress !== false) {
|
|
425
488
|
contextMsg += ` - ${contextResult.compression.ratio.toFixed(1)}% compression`;
|
|
426
489
|
}
|
|
427
|
-
if (contextResult.fromCache) {
|
|
428
|
-
contextMsg += ` [FROM CACHE]`;
|
|
429
|
-
}
|
|
430
490
|
console.log(contextMsg);
|
|
431
491
|
if (options.verbose) {
|
|
432
492
|
const t = contextResult.timing;
|
|
433
|
-
let summary = `Context
|
|
493
|
+
let summary = `Context: ${(contextString.length / 1024).toFixed(1)}KB `;
|
|
434
494
|
if (contextResult.compression) {
|
|
435
495
|
summary += `(${contextResult.compression.ratio.toFixed(1)}% compressed) `;
|
|
436
496
|
}
|
|
437
497
|
summary += `(tree: ${t.treeMs}ms, filter: ${t.filterMs}ms, read: ${t.readMs}ms`;
|
|
438
|
-
if (t.compressMs)
|
|
498
|
+
if (t.compressMs)
|
|
439
499
|
summary += `, compress: ${t.compressMs}ms`;
|
|
440
|
-
}
|
|
441
500
|
summary += `)`;
|
|
442
501
|
console.log(` ${summary}`);
|
|
443
502
|
console.log(` Cache key: ${contextResult.cacheKey}`);
|
|
@@ -453,7 +512,7 @@ program
|
|
|
453
512
|
console.log(`✅ Context gathering complete!`);
|
|
454
513
|
console.log(`📦 Size: ${(contextString.length / 1024).toFixed(1)}KB`);
|
|
455
514
|
if (contextResult.cacheKey) {
|
|
456
|
-
const cachePath = join(tmpdir(), 'exai-cache', contextResult.cacheKey);
|
|
515
|
+
const cachePath = join(tmpdir(), 'exai-cache', `context__${contextResult.cacheKey}.json`);
|
|
457
516
|
console.log(`🔑 Cache key: ${contextResult.cacheKey}`);
|
|
458
517
|
console.log(`📁 Cache path: ${cachePath}`);
|
|
459
518
|
}
|
|
@@ -466,23 +525,34 @@ program
|
|
|
466
525
|
process.exit(1);
|
|
467
526
|
}
|
|
468
527
|
}
|
|
469
|
-
// Generate input using AI
|
|
470
|
-
|
|
528
|
+
// Generate input using AI (or load from LLM cache on --redraw)
|
|
529
|
+
const isRedraw = options.redraw === true;
|
|
530
|
+
console.log(`🤖 [2/5] ${isRedraw ? 'Loading diagram from LLM cache...' : 'Calling AI to generate flowchart...'}`);
|
|
471
531
|
const aiStart = Date.now();
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
532
|
+
let input;
|
|
533
|
+
try {
|
|
534
|
+
input = await generateFlowchartInput(prompt, format, {
|
|
535
|
+
model: options.model,
|
|
536
|
+
apiKey: options.apiKey,
|
|
537
|
+
temperature,
|
|
538
|
+
context: contextString,
|
|
539
|
+
verbose: options.verbose,
|
|
540
|
+
useCache: options.cache !== false,
|
|
541
|
+
cacheOnly: isRedraw,
|
|
542
|
+
timeoutMs: config.timeoutSecs !== undefined ? config.timeoutSecs * 1000 : undefined,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
547
|
+
if (msg.startsWith('CACHE_MISS:')) {
|
|
548
|
+
console.error('❌ --redraw failed: LLM cache is cold for this prompt + context.');
|
|
549
|
+
console.error(' Run without --redraw first to populate the cache.');
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
throw err;
|
|
553
|
+
}
|
|
484
554
|
const aiTime = Date.now() - aiStart;
|
|
485
|
-
console.log(`✓ AI generation complete (${input.length} chars in ${aiTime}ms)`);
|
|
555
|
+
console.log(`✓ ${isRedraw ? 'Loaded from LLM cache' : 'AI generation complete'} (${input.length} chars in ${aiTime}ms)`);
|
|
486
556
|
if (options.verbose) {
|
|
487
557
|
console.log(`\n${format.toUpperCase()} Output:`);
|
|
488
558
|
console.log('─────────────────────────────────────────');
|
|
@@ -563,11 +633,11 @@ program
|
|
|
563
633
|
.action((action) => {
|
|
564
634
|
try {
|
|
565
635
|
if (action === 'clear') {
|
|
566
|
-
const cleared =
|
|
636
|
+
const cleared = cache.clear();
|
|
567
637
|
console.log(`✓ Cleared ${cleared} cache entries`);
|
|
568
638
|
}
|
|
569
639
|
else if (action === 'stats') {
|
|
570
|
-
const stats =
|
|
640
|
+
const stats = cache.stats();
|
|
571
641
|
console.log('Cache Statistics:');
|
|
572
642
|
console.log(` Total Entries: ${stats.totalEntries}`);
|
|
573
643
|
console.log(` Total Size: ${(stats.totalSize / 1024).toFixed(2)} KB`);
|