@koi-language/koi 1.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.
Files changed (85) hide show
  1. package/QUICKSTART.md +89 -0
  2. package/README.md +545 -0
  3. package/examples/actions-demo.koi +177 -0
  4. package/examples/cache-test.koi +29 -0
  5. package/examples/calculator.koi +61 -0
  6. package/examples/clear-registry.js +33 -0
  7. package/examples/clear-registry.koi +30 -0
  8. package/examples/code-introspection-test.koi +149 -0
  9. package/examples/counter.koi +132 -0
  10. package/examples/delegation-test.koi +52 -0
  11. package/examples/directory-import-test.koi +84 -0
  12. package/examples/hello-world-claude.koi +52 -0
  13. package/examples/hello-world.koi +52 -0
  14. package/examples/hello.koi +24 -0
  15. package/examples/mcp-example.koi +70 -0
  16. package/examples/multi-event-handler-test.koi +144 -0
  17. package/examples/new-import-test.koi +89 -0
  18. package/examples/pipeline.koi +162 -0
  19. package/examples/registry-demo.koi +184 -0
  20. package/examples/registry-playbook-demo.koi +162 -0
  21. package/examples/registry-playbook-email-compositor-2.koi +140 -0
  22. package/examples/registry-playbook-email-compositor.koi +140 -0
  23. package/examples/sentiment.koi +90 -0
  24. package/examples/simple.koi +48 -0
  25. package/examples/skill-import-test.koi +76 -0
  26. package/examples/skills/advanced/index.koi +95 -0
  27. package/examples/skills/math-operations.koi +69 -0
  28. package/examples/skills/string-operations.koi +56 -0
  29. package/examples/task-chaining-demo.koi +244 -0
  30. package/examples/test-await.koi +22 -0
  31. package/examples/test-crypto-sha256.koi +196 -0
  32. package/examples/test-delegation.koi +41 -0
  33. package/examples/test-multi-team-routing.koi +258 -0
  34. package/examples/test-no-handler.koi +35 -0
  35. package/examples/test-npm-import.koi +67 -0
  36. package/examples/test-parse.koi +10 -0
  37. package/examples/test-peers-with-team.koi +59 -0
  38. package/examples/test-permissions-fail.koi +20 -0
  39. package/examples/test-permissions.koi +36 -0
  40. package/examples/test-simple-registry.koi +31 -0
  41. package/examples/test-typescript-import.koi +64 -0
  42. package/examples/test-uses-team-syntax.koi +25 -0
  43. package/examples/test-uses-team.koi +31 -0
  44. package/examples/utils/calculator.test.ts +144 -0
  45. package/examples/utils/calculator.ts +56 -0
  46. package/examples/utils/math-helpers.js +50 -0
  47. package/examples/utils/math-helpers.ts +55 -0
  48. package/examples/web-delegation-demo.koi +165 -0
  49. package/package.json +78 -0
  50. package/src/cli/koi.js +793 -0
  51. package/src/compiler/build-optimizer.js +447 -0
  52. package/src/compiler/cache-manager.js +274 -0
  53. package/src/compiler/import-resolver.js +369 -0
  54. package/src/compiler/parser.js +7542 -0
  55. package/src/compiler/transpiler.js +1105 -0
  56. package/src/compiler/typescript-transpiler.js +148 -0
  57. package/src/grammar/koi.pegjs +767 -0
  58. package/src/runtime/action-registry.js +172 -0
  59. package/src/runtime/actions/call-skill.js +45 -0
  60. package/src/runtime/actions/format.js +115 -0
  61. package/src/runtime/actions/print.js +42 -0
  62. package/src/runtime/actions/registry-delete.js +37 -0
  63. package/src/runtime/actions/registry-get.js +37 -0
  64. package/src/runtime/actions/registry-keys.js +33 -0
  65. package/src/runtime/actions/registry-search.js +34 -0
  66. package/src/runtime/actions/registry-set.js +50 -0
  67. package/src/runtime/actions/return.js +31 -0
  68. package/src/runtime/actions/send-message.js +58 -0
  69. package/src/runtime/actions/update-state.js +36 -0
  70. package/src/runtime/agent.js +1368 -0
  71. package/src/runtime/cli-logger.js +205 -0
  72. package/src/runtime/incremental-json-parser.js +201 -0
  73. package/src/runtime/index.js +33 -0
  74. package/src/runtime/llm-provider.js +1372 -0
  75. package/src/runtime/mcp-client.js +1171 -0
  76. package/src/runtime/planner.js +273 -0
  77. package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
  78. package/src/runtime/registry-backends/local.js +260 -0
  79. package/src/runtime/registry.js +162 -0
  80. package/src/runtime/role.js +14 -0
  81. package/src/runtime/router.js +395 -0
  82. package/src/runtime/runtime.js +113 -0
  83. package/src/runtime/skill-selector.js +173 -0
  84. package/src/runtime/skill.js +25 -0
  85. package/src/runtime/team.js +162 -0
@@ -0,0 +1,447 @@
1
+ /**
2
+ * Build-Time Optimizer
3
+ *
4
+ * Pre-computes expensive operations during compilation to avoid runtime overhead:
5
+ * - Embeddings for agent affordances (with persistent caching)
6
+ * - Static agent metadata
7
+ * - Any other cacheable data
8
+ *
9
+ * Uses SHA-256 content hashing to detect changes and avoid redundant API calls.
10
+ */
11
+
12
+ import { LLMProvider } from '../runtime/llm-provider.js';
13
+ import { CacheManager } from './cache-manager.js';
14
+
15
+ export class BuildTimeOptimizer {
16
+ constructor(config = {}) {
17
+ this.enableCache = config.cache !== false;
18
+ this.verbose = config.verbose || false;
19
+ this.llmProvider = null; // For embeddings
20
+ this.chatProvider = null; // For code introspection
21
+ this.cacheManager = new CacheManager({
22
+ verbose: config.verbose || false
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Extract and pre-compute affordances from AST (with cache)
28
+ * @param ast - Abstract syntax tree
29
+ * @param sourceContent - Original source code content (for cache hashing)
30
+ * @param sourcePath - Path to source file (for cache tracking)
31
+ */
32
+ async optimizeAST(ast, sourceContent = '', sourcePath = 'unknown') {
33
+ if (!this.enableCache) {
34
+ if (this.verbose) console.log('[BuildOptimizer] Cache disabled, skipping');
35
+ return null;
36
+ }
37
+
38
+ console.log('šŸ”„ [BuildOptimizer] Checking cache...');
39
+
40
+ // Check if we have cached data for this exact source content
41
+ const cached = this.cacheManager.get(sourceContent, sourcePath);
42
+ if (cached) {
43
+ const totalAffordances = (cached.metadata.totalAffordances || 0) + (cached.metadata.totalSkillAffordances || 0);
44
+ console.log(`āœ… [BuildOptimizer] Using cached embeddings (${totalAffordances} affordances)`);
45
+ console.log(` Last generated: ${new Date(cached.metadata.generatedAt).toLocaleString()}`);
46
+ console.log(` šŸ’° Saved API calls!`);
47
+ return cached;
48
+ }
49
+
50
+ // Cache miss - generate embeddings
51
+ console.log('šŸ”„ [BuildOptimizer] Cache miss, pre-computing embeddings...');
52
+
53
+ const affordances = {};
54
+ const skillAffordances = {};
55
+ let totalEmbeddings = 0;
56
+ let totalSkillEmbeddings = 0;
57
+
58
+ // Find all declarations in AST
59
+ for (const decl of ast.declarations) {
60
+ if (decl.type === 'AgentDecl') {
61
+ const agentAffordances = await this.extractAgentAffordances(decl);
62
+
63
+ if (agentAffordances && Object.keys(agentAffordances).length > 0) {
64
+ affordances[decl.name.name] = agentAffordances;
65
+ totalEmbeddings += Object.keys(agentAffordances).length;
66
+ }
67
+ } else if (decl.type === 'SkillDecl') {
68
+ const skillData = await this.extractSkillAffordance(decl);
69
+
70
+ if (skillData) {
71
+ skillAffordances[decl.name.name] = skillData;
72
+ totalSkillEmbeddings++;
73
+ }
74
+ }
75
+ }
76
+
77
+ console.log(`āœ… [BuildOptimizer] Pre-computed ${totalEmbeddings + totalSkillEmbeddings} embeddings (${totalEmbeddings} agents, ${totalSkillEmbeddings} skills)`);
78
+
79
+ const result = {
80
+ affordances,
81
+ skillAffordances,
82
+ metadata: {
83
+ generatedAt: Date.now(),
84
+ totalAgents: Object.keys(affordances).length,
85
+ totalAffordances: totalEmbeddings,
86
+ totalSkills: Object.keys(skillAffordances).length,
87
+ totalSkillAffordances: totalSkillEmbeddings
88
+ }
89
+ };
90
+
91
+ // Store in cache
92
+ this.cacheManager.set(sourceContent, sourcePath, result);
93
+
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * Extract and pre-compute affordances from AST (without cache)
99
+ * Generates embeddings but doesn't store them in persistent cache
100
+ * @param ast - Abstract syntax tree
101
+ */
102
+ async optimizeASTWithoutCache(ast) {
103
+ const affordances = {};
104
+ let totalEmbeddings = 0;
105
+
106
+ // Find all agent declarations in AST
107
+ for (const decl of ast.declarations) {
108
+ if (decl.type === 'AgentDecl') {
109
+ const agentAffordances = await this.extractAgentAffordances(decl);
110
+
111
+ if (agentAffordances && Object.keys(agentAffordances).length > 0) {
112
+ affordances[decl.name.name] = agentAffordances;
113
+ totalEmbeddings += Object.keys(agentAffordances).length;
114
+ }
115
+ }
116
+ }
117
+
118
+ console.log(`āœ… [BuildOptimizer] Pre-computed ${totalEmbeddings} embeddings (no cache)`);
119
+
120
+ return {
121
+ affordances,
122
+ metadata: {
123
+ generatedAt: Date.now(),
124
+ totalAgents: Object.keys(affordances).length,
125
+ totalAffordances: totalEmbeddings
126
+ }
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Extract affordances for a single agent
132
+ */
133
+ async extractAgentAffordances(agentNode) {
134
+ const affordances = {};
135
+
136
+ // Find event handlers
137
+ const eventHandlers = agentNode.body.filter(b => b.type === 'EventHandler');
138
+
139
+ if (eventHandlers.length === 0) {
140
+ return affordances;
141
+ }
142
+
143
+ if (this.verbose) {
144
+ console.log(` [Agent:${agentNode.name.name}] Extracting ${eventHandlers.length} affordances...`);
145
+ }
146
+
147
+ for (const handler of eventHandlers) {
148
+ const eventName = handler.event.name;
149
+
150
+ // Try to find playbook
151
+ const playbook = this.findPlaybookForHandler(handler);
152
+
153
+ let description;
154
+ let confidence;
155
+ let hasPlaybook = false;
156
+
157
+ if (playbook) {
158
+ // Extract description from playbook
159
+ description = this.inferIntentFromPlaybook(playbook, eventName);
160
+ confidence = 0.9;
161
+ hasPlaybook = true;
162
+ } else {
163
+ // No playbook - use code introspection
164
+ description = await this.introspectHandlerCode(handler, eventName);
165
+ confidence = 0.8; // High confidence since we analyzed the actual code
166
+ hasPlaybook = false;
167
+ }
168
+
169
+ // Validate description
170
+ if (!description || description.trim() === '') {
171
+ console.warn(`āš ļø [BuildOptimizer] Empty description for ${agentNode.name.name}.${eventName}, skipping embedding`);
172
+ description = `Handler: ${eventName}`;
173
+ }
174
+
175
+ // Generate embedding
176
+ const embedding = await this.generateEmbedding(description);
177
+
178
+ affordances[eventName] = {
179
+ description: description,
180
+ embedding: embedding,
181
+ confidence: confidence,
182
+ hasPlaybook: hasPlaybook
183
+ };
184
+
185
+ if (this.verbose) {
186
+ console.log(` āœ“ ${eventName}: "${description.substring(0, 50)}..."`);
187
+ }
188
+ }
189
+
190
+ return affordances;
191
+ }
192
+
193
+ /**
194
+ * Find playbook for an event handler
195
+ */
196
+ findPlaybookForHandler(handler) {
197
+ // Check if handler has a playbook statement
198
+ for (const stmt of handler.body) {
199
+ if (stmt.type === 'PlaybookStatement') {
200
+ return stmt.content.value;
201
+ }
202
+ }
203
+
204
+ return null;
205
+ }
206
+
207
+ /**
208
+ * Infer intent from playbook text
209
+ */
210
+ inferIntentFromPlaybook(playbook, eventName) {
211
+ // Handle non-string playbooks
212
+ if (!playbook || typeof playbook !== 'string') {
213
+ return this.humanizeEventName(eventName);
214
+ }
215
+
216
+ // Remove template literals
217
+ const cleanText = playbook
218
+ .replace(/\$\{[^}]+\}/g, '')
219
+ .split('\n')
220
+ .map(line => line.trim())
221
+ .filter(line => line.length > 0 && !line.startsWith('//') && !line.startsWith('Return'))
222
+ .slice(0, 3) // Take first 3 lines
223
+ .join(' ');
224
+
225
+ if (cleanText.length > 10 && cleanText.length < 200) {
226
+ return cleanText;
227
+ }
228
+
229
+ // Fallback
230
+ return this.humanizeEventName(eventName);
231
+ }
232
+
233
+ /**
234
+ * Introspect handler code using LLM to understand what it does
235
+ * @param handler - Event handler AST node
236
+ * @param eventName - Name of the event
237
+ * @returns Description of what the handler does
238
+ */
239
+ async introspectHandlerCode(handler, eventName) {
240
+ // Serialize the handler body to source code
241
+ const codeLines = [];
242
+
243
+ for (const stmt of handler.body) {
244
+ const code = this.serializeStatement(stmt);
245
+ if (code) {
246
+ codeLines.push(code);
247
+ }
248
+ }
249
+
250
+ const sourceCode = codeLines.join('\n');
251
+
252
+ // If no code, fallback to event name
253
+ if (!sourceCode || sourceCode.trim().length === 0) {
254
+ return this.humanizeEventName(eventName);
255
+ }
256
+
257
+ // Use LLM to analyze the code
258
+ if (!this.chatProvider) {
259
+ this.initChatProvider();
260
+ }
261
+
262
+ const introspectionPrompt = `Analyze this event handler code and provide a concise description (1-2 sentences) of what it does.
263
+
264
+ Event name: ${eventName}
265
+
266
+ Code:
267
+ \`\`\`javascript
268
+ ${sourceCode}
269
+ \`\`\`
270
+
271
+ Focus on:
272
+ - What operations it performs
273
+ - What data it processes or returns
274
+ - Its main purpose or capability
275
+
276
+ Return ONLY the description text, no markdown, no explanations, no prefix like "This handler...". Just a direct description.`;
277
+
278
+ try {
279
+ const response = await this.chatProvider.executeOpenAI(introspectionPrompt, false);
280
+
281
+ // Clean up the response
282
+ const description = response
283
+ .replace(/^(This handler|This event handler|This function|The handler|The function)\s*/i, '')
284
+ .replace(/^(handles?|processes?|performs?)\s*/i, '')
285
+ .trim();
286
+
287
+ if (description && description.length > 10) {
288
+ return description;
289
+ }
290
+ } catch (error) {
291
+ console.warn(`[BuildOptimizer] Code introspection failed for ${eventName}:`, error.message);
292
+ }
293
+
294
+ // Fallback to humanized event name
295
+ return this.humanizeEventName(eventName);
296
+ }
297
+
298
+ /**
299
+ * Serialize an AST statement to source code (simplified)
300
+ */
301
+ serializeStatement(stmt) {
302
+ if (!stmt) return '';
303
+
304
+ switch (stmt.type) {
305
+ case 'ConstDeclaration':
306
+ return `const ${stmt.name.name} = ...`;
307
+
308
+ case 'ReturnStatement':
309
+ if (stmt.value && stmt.value.type === 'ObjectLiteral') {
310
+ // Extract object keys
311
+ const keys = stmt.value.properties?.map(p => p.key?.name || p.key).join(', ') || '';
312
+ return `return { ${keys} }`;
313
+ }
314
+ return 'return ...';
315
+
316
+ case 'SendStatement':
317
+ const role = stmt.role?.name || 'Role';
318
+ const event = stmt.event?.name || 'event';
319
+ return `send to ${role}.${event}()`;
320
+
321
+ case 'ExpressionStatement':
322
+ if (stmt.expression?.type === 'CallExpression') {
323
+ const callee = stmt.expression.callee?.name || stmt.expression.callee?.property?.name || 'function';
324
+ return `${callee}(...)`;
325
+ }
326
+ return '...';
327
+
328
+ default:
329
+ return `// ${stmt.type}`;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Extract affordance for a skill
335
+ */
336
+ async extractSkillAffordance(skillNode) {
337
+ // Skills have an explicit affordance field
338
+ const affordanceText = skillNode.affordance?.value || skillNode.affordance || '';
339
+
340
+ if (!affordanceText || affordanceText.trim().length === 0) {
341
+ // No affordance defined - use skill name as fallback
342
+ const description = this.humanizeEventName(skillNode.name.name);
343
+ return {
344
+ description,
345
+ embedding: await this.generateEmbedding(description),
346
+ confidence: 0.5
347
+ };
348
+ }
349
+
350
+ // Clean up affordance text (remove extra whitespace/newlines)
351
+ let cleanDescription = affordanceText
352
+ .split('\n')
353
+ .map(line => line.trim())
354
+ .filter(line => line.length > 0)
355
+ .join(' ')
356
+ .trim();
357
+
358
+ // Validate description is not empty
359
+ if (!cleanDescription || cleanDescription.length === 0) {
360
+ cleanDescription = this.humanizeEventName(skillNode.name.name);
361
+ }
362
+
363
+ // Generate embedding for the affordance
364
+ const embedding = await this.generateEmbedding(cleanDescription);
365
+
366
+ if (this.verbose) {
367
+ console.log(` āœ“ Skill ${skillNode.name.name}: "${cleanDescription.substring(0, 50)}..."`);
368
+ }
369
+
370
+ return {
371
+ description: cleanDescription,
372
+ embedding: embedding,
373
+ confidence: 0.9
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Humanize event name
379
+ */
380
+ humanizeEventName(eventName) {
381
+ return eventName
382
+ .replace(/([A-Z])/g, ' $1')
383
+ .replace(/_/g, ' ')
384
+ .toLowerCase()
385
+ .trim();
386
+ }
387
+
388
+ /**
389
+ * Initialize chat LLM provider for code introspection
390
+ */
391
+ initChatProvider() {
392
+ this.chatProvider = new LLMProvider({
393
+ provider: 'openai',
394
+ model: 'gpt-4o-mini', // Fast and cheap model for code analysis
395
+ temperature: 0.1,
396
+ maxTokens: 150
397
+ });
398
+ }
399
+
400
+ /**
401
+ * Generate embedding for text
402
+ */
403
+ async generateEmbedding(text) {
404
+ if (!this.llmProvider) {
405
+ this.llmProvider = new LLMProvider({
406
+ provider: 'openai',
407
+ model: 'text-embedding-3-small'
408
+ });
409
+ }
410
+
411
+ try {
412
+ return await this.llmProvider.getEmbedding(text);
413
+ } catch (error) {
414
+ console.warn(`[BuildOptimizer] Failed to generate embedding: ${error.message}`);
415
+ return null;
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Generate JavaScript code for cached data
421
+ */
422
+ generateCacheCode(cacheData) {
423
+ if (!cacheData) {
424
+ return '';
425
+ }
426
+
427
+ const totalAffordances = (cacheData.metadata.totalAffordances || 0) + (cacheData.metadata.totalSkillAffordances || 0);
428
+
429
+ const code = `
430
+ // ============================================================
431
+ // Pre-computed Affordances (Build-time Cache)
432
+ // Generated at: ${new Date(cacheData.metadata.generatedAt).toISOString()}
433
+ // Total agents: ${cacheData.metadata.totalAgents || 0}
434
+ // Total agent affordances: ${cacheData.metadata.totalAffordances || 0}
435
+ // Total skills: ${cacheData.metadata.totalSkills || 0}
436
+ // Total skill affordances: ${cacheData.metadata.totalSkillAffordances || 0}
437
+ // ============================================================
438
+
439
+ const CACHED_AFFORDANCES = ${JSON.stringify(cacheData.affordances || {}, null, 2)};
440
+
441
+ const CACHED_SKILL_AFFORDANCES = ${JSON.stringify(cacheData.skillAffordances || {}, null, 2)};
442
+
443
+ `;
444
+
445
+ return code;
446
+ }
447
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Persistent Cache Manager
3
+ *
4
+ * Manages a persistent cache directory for build-time optimizations.
5
+ * Uses SHA-256 hashing to detect source file changes and avoid
6
+ * regenerating expensive computations (embeddings, etc.)
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import crypto from 'crypto';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ export class CacheManager {
18
+ constructor(config = {}) {
19
+ // Cache directory relative to project root
20
+ this.cacheDir = config.cacheDir || path.join(process.cwd(), '.koi-cache');
21
+ this.verbose = config.verbose || false;
22
+
23
+ // Ensure cache directory exists
24
+ this.ensureCacheDir();
25
+ }
26
+
27
+ /**
28
+ * Ensure cache directory exists
29
+ */
30
+ ensureCacheDir() {
31
+ if (!fs.existsSync(this.cacheDir)) {
32
+ fs.mkdirSync(this.cacheDir, { recursive: true });
33
+ if (this.verbose) {
34
+ console.log(`[Cache] Created cache directory: ${this.cacheDir}`);
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Compute SHA-256 hash of file content
41
+ */
42
+ hashContent(content) {
43
+ return crypto.createHash('sha256').update(content).digest('hex');
44
+ }
45
+
46
+ /**
47
+ * Get cache file path for a source file
48
+ */
49
+ getCachePath(sourceHash) {
50
+ return path.join(this.cacheDir, `affordances-${sourceHash}.json`);
51
+ }
52
+
53
+ /**
54
+ * Get metadata file path
55
+ */
56
+ getMetadataPath() {
57
+ return path.join(this.cacheDir, 'cache-metadata.json');
58
+ }
59
+
60
+ /**
61
+ * Load cache metadata (tracks all cached files)
62
+ */
63
+ loadMetadata() {
64
+ const metaPath = this.getMetadataPath();
65
+
66
+ if (!fs.existsSync(metaPath)) {
67
+ return {
68
+ version: '1.0.0',
69
+ files: {}
70
+ };
71
+ }
72
+
73
+ try {
74
+ const content = fs.readFileSync(metaPath, 'utf-8');
75
+ return JSON.parse(content);
76
+ } catch (error) {
77
+ console.warn(`[Cache] Failed to load metadata: ${error.message}`);
78
+ return {
79
+ version: '1.0.0',
80
+ files: {}
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Save cache metadata
87
+ */
88
+ saveMetadata(metadata) {
89
+ const metaPath = this.getMetadataPath();
90
+
91
+ try {
92
+ fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
93
+ } catch (error) {
94
+ console.warn(`[Cache] Failed to save metadata: ${error.message}`);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check if cache exists for source content
100
+ */
101
+ has(sourceContent) {
102
+ const hash = this.hashContent(sourceContent);
103
+ const cachePath = this.getCachePath(hash);
104
+ return fs.existsSync(cachePath);
105
+ }
106
+
107
+ /**
108
+ * Get cached data for source content
109
+ */
110
+ get(sourceContent, sourcePath = null) {
111
+ const hash = this.hashContent(sourceContent);
112
+ const cachePath = this.getCachePath(hash);
113
+
114
+ if (!fs.existsSync(cachePath)) {
115
+ return null;
116
+ }
117
+
118
+ try {
119
+ const content = fs.readFileSync(cachePath, 'utf-8');
120
+ const cached = JSON.parse(content);
121
+
122
+ if (this.verbose) {
123
+ console.log(`[Cache] āœ“ Cache hit for ${sourcePath || 'source'} (hash: ${hash.substring(0, 8)}...)`);
124
+ console.log(`[Cache] Cached: ${cached.metadata.totalAffordances} affordances from ${new Date(cached.metadata.generatedAt).toLocaleString()}`);
125
+ }
126
+
127
+ return cached;
128
+ } catch (error) {
129
+ console.warn(`[Cache] Failed to read cache: ${error.message}`);
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Store data in cache
136
+ */
137
+ set(sourceContent, sourcePath, data) {
138
+ const hash = this.hashContent(sourceContent);
139
+ const cachePath = this.getCachePath(hash);
140
+
141
+ // Add cache metadata
142
+ const cacheEntry = {
143
+ ...data,
144
+ cacheMetadata: {
145
+ sourceHash: hash,
146
+ sourcePath: sourcePath,
147
+ cachedAt: Date.now()
148
+ }
149
+ };
150
+
151
+ try {
152
+ fs.writeFileSync(cachePath, JSON.stringify(cacheEntry, null, 2));
153
+
154
+ if (this.verbose) {
155
+ console.log(`[Cache] āœ“ Cached ${data.metadata.totalAffordances} affordances to ${path.basename(cachePath)}`);
156
+ }
157
+
158
+ // Update metadata index
159
+ const metadata = this.loadMetadata();
160
+ metadata.files[sourcePath] = {
161
+ hash: hash,
162
+ lastCached: Date.now(),
163
+ affordanceCount: data.metadata.totalAffordances
164
+ };
165
+ this.saveMetadata(metadata);
166
+
167
+ return true;
168
+ } catch (error) {
169
+ console.warn(`[Cache] Failed to write cache: ${error.message}`);
170
+ return false;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Clear cache for a specific file or all cache
176
+ */
177
+ clear(sourcePath = null) {
178
+ if (!sourcePath) {
179
+ // Clear all cache
180
+ if (fs.existsSync(this.cacheDir)) {
181
+ const files = fs.readdirSync(this.cacheDir);
182
+ files.forEach(file => {
183
+ fs.unlinkSync(path.join(this.cacheDir, file));
184
+ });
185
+ console.log(`[Cache] Cleared all cache (${files.length} files)`);
186
+ }
187
+ return;
188
+ }
189
+
190
+ // Clear cache for specific file
191
+ const metadata = this.loadMetadata();
192
+ const fileInfo = metadata.files[sourcePath];
193
+
194
+ if (fileInfo) {
195
+ const cachePath = this.getCachePath(fileInfo.hash);
196
+ if (fs.existsSync(cachePath)) {
197
+ fs.unlinkSync(cachePath);
198
+ delete metadata.files[sourcePath];
199
+ this.saveMetadata(metadata);
200
+ console.log(`[Cache] Cleared cache for ${sourcePath}`);
201
+ }
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Get cache statistics
207
+ */
208
+ getStats() {
209
+ const metadata = this.loadMetadata();
210
+ const files = Object.keys(metadata.files).length;
211
+
212
+ let totalSize = 0;
213
+ let totalAffordances = 0;
214
+
215
+ if (fs.existsSync(this.cacheDir)) {
216
+ const cacheFiles = fs.readdirSync(this.cacheDir);
217
+ cacheFiles.forEach(file => {
218
+ const filePath = path.join(this.cacheDir, file);
219
+ const stats = fs.statSync(filePath);
220
+ totalSize += stats.size;
221
+ });
222
+ }
223
+
224
+ Object.values(metadata.files).forEach(info => {
225
+ totalAffordances += info.affordanceCount || 0;
226
+ });
227
+
228
+ return {
229
+ cacheDir: this.cacheDir,
230
+ files: files,
231
+ totalAffordances: totalAffordances,
232
+ totalSize: totalSize,
233
+ totalSizeFormatted: this.formatBytes(totalSize)
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Format bytes to human readable
239
+ */
240
+ formatBytes(bytes) {
241
+ if (bytes === 0) return '0 Bytes';
242
+ const k = 1024;
243
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
244
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
245
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
246
+ }
247
+
248
+ /**
249
+ * Print cache summary
250
+ */
251
+ printStats() {
252
+ const stats = this.getStats();
253
+
254
+ console.log(`\nšŸ“Š Cache Statistics:`);
255
+ console.log(` Location: ${stats.cacheDir}`);
256
+ console.log(` Cached files: ${stats.files}`);
257
+ console.log(` Total affordances: ${stats.totalAffordances}`);
258
+ console.log(` Cache size: ${stats.totalSizeFormatted}`);
259
+
260
+ if (stats.files > 0) {
261
+ const metadata = this.loadMetadata();
262
+ console.log(`\n Recent files:`);
263
+ const sorted = Object.entries(metadata.files)
264
+ .sort((a, b) => b[1].lastCached - a[1].lastCached)
265
+ .slice(0, 5);
266
+
267
+ sorted.forEach(([filepath, info]) => {
268
+ const time = new Date(info.lastCached).toLocaleString();
269
+ console.log(` • ${path.basename(filepath)} (${info.affordanceCount} affordances, ${time})`);
270
+ });
271
+ }
272
+ console.log('');
273
+ }
274
+ }