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.
@@ -1,274 +1,25 @@
1
1
  /**
2
- * Query Cache - Local cache for LLM query results
2
+ * Query Cache - compatibility wrapper around the unified ExaiCache.
3
3
  *
4
- * Prevents duplicate API calls by caching responses locally based on:
5
- * - Prompt content
6
- * - Context content
7
- * - Model used
8
- * - Temperature setting
9
- *
10
- * Cache entries expire after 7 days by default.
11
- */
12
- import { createHash } from 'crypto';
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'fs';
14
- import { join } from 'path';
15
- import { tmpdir } from 'os';
16
- const DEFAULT_OPTIONS = {
17
- cacheDir: join(tmpdir(), 'exai-cache'),
18
- ttlDays: 7,
19
- maxEntries: 100,
20
- verbose: false,
21
- };
22
- /**
23
- * Generate cache key from query parameters
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":"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"}
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 { clearCache, getCacheStats } from './ai/query-cache.js';
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('<input>', 'Input file path')
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
- const input = readFileSync(inputFile, 'utf-8');
239
+ let input;
238
240
  let format = options.format;
239
241
  const formatExplicitlySet = command.getOptionValueSource('format') === 'cli';
240
- // Auto-detect format from file extension (only if --format not explicitly set)
241
- if (!formatExplicitlySet) {
242
- if (inputFile.endsWith('.json')) {
243
- format = 'json';
244
- }
245
- else if (inputFile.endsWith('.dot') || inputFile.endsWith('.gv')) {
246
- format = 'dot';
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 caching')
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
- console.log(`📄 Config loaded from: ${resolve(options.configPath)}`);
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
- console.log('📂 [1/5] Gathering Context...');
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: options.cache !== false,
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
- cacheTtlDays: config.cacheTtlDays,
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 gathering: ${(contextString.length / 1024).toFixed(1)}KB `;
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
- console.log('🤖 [2/5] Calling AI to generate flowchart...');
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
- const input = await generateFlowchartInput(prompt, format, {
473
- model: options.model,
474
- apiKey: options.apiKey,
475
- temperature,
476
- context: contextString,
477
- verbose: options.verbose,
478
- useCache: options.cache !== false,
479
- cacheOptions: {
480
- ttlDays: config.cacheTtlDays,
481
- maxEntries: config.cacheMaxEntries,
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 = clearCache({ verbose: true });
636
+ const cleared = cache.clear();
567
637
  console.log(`✓ Cleared ${cleared} cache entries`);
568
638
  }
569
639
  else if (action === 'stats') {
570
- const stats = getCacheStats();
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`);