-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathAIService.cfc
More file actions
654 lines (583 loc) · 20.5 KB
/
AIService.cfc
File metadata and controls
654 lines (583 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
/**
* Central AI operations service for ColdBox CLI AI integration
* Coordinates guidelines, skills, MCP servers, and agent configurations
*/
component singleton {
// DI
property name="print" inject="PrintBuffer";
property name="fileSystemUtil" inject="fileSystem";
property name="packageService" inject="PackageService";
property name="guidelineManager" inject="GuidelineManager@coldbox-cli";
property name="skillManager" inject="SkillManager@coldbox-cli";
property name="agentRegistry" inject="AgentRegistry@coldbox-cli";
property name="mcpRegistry" inject="MCPRegistry@coldbox-cli";
property name="utility" inject="Utility@coldbox-cli";
/**
* Install AI integration for a project
*
* @directory The project directory (defaults to current directory)
* @agents Comma-separated list of agents to configure (claude,copilot,codex,gemini,opencode)
* @language Project language mode: boxlang, cfml, hybrid (default: boxlang)
* @force Overwrite existing AI configuration
*/
function install(
required string directory,
string agents = "claude",
string language = "boxlang",
boolean force = false
){
var result = {
"success" : true,
"message" : "",
"guidelines" : [],
"skills" : [],
"agents" : [],
"manifest" : {}
};
// Validate directory
if ( !directoryExists( arguments.directory ) ) {
result.success = false;
result.message = "Directory does not exist: #arguments.directory#";
return result;
}
// Check if already installed
var aiDir = arguments.directory & "/.ai";
if ( directoryExists( aiDir ) && !arguments.force ) {
result.success = false;
result.message = "AI integration already installed. Use --force to overwrite.";
return result;
}
// Create .ai directory structure
createAIDirectoryStructure( arguments.directory );
// Initialize manifest
var templateType = variables.utility.detectTemplateType( arguments.directory )
var manifest = {
"coldboxCliVersion" : variables.utility.getColdboxCliVersion(),
"lastSync" : dateTimeFormat( now(), "iso" ),
"language" : arguments.language,
"templateType" : templateType,
"guidelines" : [],
"skills" : [],
"agents" : listToArray( arguments.agents ),
"mcpServers" : {
"core" : [],
"module" : [],
"custom" : []
}
};
// Install core guidelines
result.guidelines = variables.guidelineManager.installCoreGuidelines(
arguments.directory,
arguments.language,
manifest
);
// Install core skills
result.skills = variables.skillManager.installCoreSkills(
arguments.directory,
arguments.language,
manifest
);
// Initialize MCP servers
var mcpServers = variables.mcpRegistry.getServersForProject( arguments.directory );
manifest.mcpServers.core = mcpServers.core;
manifest.mcpServers.module = mcpServers.module;
result.mcpServers.core = mcpServers.core;
result.mcpServers.module = mcpServers.module;
// If only 1 agent is configured, automatically set it as active
var agentsList = listToArray( arguments.agents )
if ( agentsList.len() == 1 ) {
manifest.activeAgent = agentsList.first()
}
// Save manifest BEFORE configuring agents so they can read MCP servers
saveManifest( arguments.directory, manifest );
// Configure agents (reads manifest for MCP servers)
result.agents = variables.agentRegistry.configureAgents(
arguments.directory,
arguments.agents,
arguments.language
);
result.manifest = manifest;
// Update box.json with AI configuration
updateBoxJsonAIConfig(
arguments.directory,
arguments.language,
arguments.agents
);
result.message = "AI integration installed successfully!";
return result;
}
/**
* Refresh AI integration (sync with installed modules)
*
* The refresh process will:
* 1. Load the existing manifest to understand current state
* 2. Check installed modules and determine if any guidelines or skills need to be added
* updated, or removed based on the current box.json dependencies and the guidelines/skills they provide
* 3. Update the manifest with any changes and save it
* 4. Return a result struct with details on what was added, updated, or removed, along with a success status and message
*
* @directory The project directory
*
* @return Struct with success status, message, and lists of added/updated/removed guidelines and skills
*/
struct function refresh( required string directory ){
var result = {
"success" : true,
"message" : "",
"guidelines" : {
"added" : [],
"updated" : [],
"removed" : []
},
"skills" : {
"added" : [],
"updated" : [],
"removed" : []
},
"mcpServers" : { "added" : [], "removed" : [] }
};
// Load existing manifest
var manifest = loadManifest( arguments.directory );
if ( !structKeyExists( manifest, "coldboxCliVersion" ) ) {
result.success = false;
result.message = "No AI integration found. Run 'coldbox ai install' first.";
return result;
}
// Update coldbox-cli version
manifest.coldboxCliVersion = variables.utility.getColdboxCliVersion()
manifest.lastSync = dateTimeFormat( now(), "iso" )
// Update/add template type (detects current structure, handles old manifests or structure changes)
manifest.templateType = variables.utility.detectTemplateType( arguments.directory )
// Refresh guidelines based on installed modules
var guidelineChanges = variables.guidelineManager.refresh( arguments.directory, manifest );
result.guidelines.added.append( guidelineChanges.added, true );
result.guidelines.updated.append( guidelineChanges.updated, true );
result.guidelines.removed.append( guidelineChanges.removed, true );
// Refresh skills based on installed modules
var skillChanges = variables.skillManager.refresh( arguments.directory, manifest );
result.skills.added.append( skillChanges.added, true );
result.skills.updated.append( skillChanges.updated, true );
result.skills.removed.append( skillChanges.removed, true );
// Refresh MCP servers based on installed modules
var newMcpServers = variables.mcpRegistry.getServersForProject( arguments.directory );
var oldMcpServers = manifest.mcpServers ?: {
"core" : [],
"module" : [],
"custom" : []
};
// Track changes in module servers (core servers never change, custom servers are preserved)
var oldModuleServers = oldMcpServers.module ?: [];
var newModuleServers = newMcpServers.module;
// Find added module servers
newModuleServers.each( ( serverName ) => {
if ( !oldModuleServers.findNoCase( serverName ) ) {
result.mcpServers.added.append( serverName );
}
} );
// Find removed module servers
oldModuleServers.each( ( serverName ) => {
if ( !newModuleServers.findNoCase( serverName ) ) {
result.mcpServers.removed.append( serverName );
}
} );
// Preserve custom servers
var customServers = oldMcpServers.custom ?: [];
manifest.mcpServers = {
"core" : newMcpServers.core,
"module" : newMcpServers.module,
"custom" : customServers
};
// Save updated manifest
saveManifest( arguments.directory, manifest );
// Regenerate agent configuration files with updated content
// This ensures MCP servers and other changes are reflected in agent instruction files
if ( structKeyExists( manifest, "agents" ) && manifest.agents.len() ) {
var language = manifest.language ?: "boxlang";
manifest.agents.each( ( agent ) => {
variables.agentRegistry.configureAgent( directory, agent, language );
} );
}
result.message = "AI integration refreshed successfully!";
return result;
}
/**
* Get AI integration info for a project
*
* @directory The project directory
*/
function getInfo( required string directory ){
var manifest = loadManifest( arguments.directory );
return {
"installed" : structKeyExists( manifest, "coldboxCliVersion" ),
"coldboxCliVersion" : manifest.coldboxCliVersion ?: "unknown",
"language" : manifest.language ?: "unknown",
"templateType" : manifest.templateType ?: "unknown",
"lastSync" : manifest.lastSync ?: "never",
"guidelines" : manifest.guidelines ?: [],
"skills" : manifest.skills ?: [],
"agents" : manifest.agents ?: [],
"mcpServers" : manifest.mcpServers ?: {
"core" : [],
"module" : [],
"custom" : []
}
};
}
/**
* Diagnose AI integration health
*
* @directory The project directory
*/
function diagnose( required string directory ){
var issues = {
"errors" : [],
"warnings" : [],
"recommendations" : [],
"summary" : {}
};
// Check if AI integration is installed
var aiDir = arguments.directory & "/.ai"
if ( !directoryExists( aiDir ) ) {
issues.errors.append( "AI integration not installed. Run 'coldbox ai install' first." )
// Build summary for early return
issues.summary = {
"status" : "error",
"errorCount" : issues.errors.len(),
"warningCount" : issues.warnings.len(),
"recommendationCount" : issues.recommendations.len()
}
return issues
}
// Load manifest
var manifest = loadManifest( arguments.directory )
if ( !structKeyExists( manifest, "coldboxCliVersion" ) ) {
issues.errors.append( "Invalid or missing .ai/.manifest.json file" )
// Build summary for early return
issues.summary = {
"status" : "error",
"errorCount" : issues.errors.len(),
"warningCount" : issues.warnings.len(),
"recommendationCount" : issues.recommendations.len()
}
return issues
}
// Check coldbox-cli version
var currentVersion = variables.utility.getColdboxCliVersion();
if ( manifest.coldboxCliVersion != currentVersion ) {
issues.warnings.append(
"coldbox-cli v#currentVersion# installed, but guidelines are from v#manifest.coldboxCliVersion#"
);
issues.recommendations.append( "Run 'coldbox ai refresh' to update guidelines/skills" );
}
// Validate guidelines
var guidelineIssues = variables.guidelineManager.diagnose( arguments.directory, manifest );
issues.warnings.append( guidelineIssues.warnings, true );
issues.recommendations.append(
guidelineIssues.recommendations,
true
);
// Validate skills
var skillIssues = variables.skillManager.diagnose( arguments.directory, manifest );
issues.warnings.append( skillIssues.warnings, true );
issues.recommendations.append( skillIssues.recommendations, true );
// Validate agents
var agentIssues = variables.agentRegistry.diagnose( arguments.directory, manifest );
issues.warnings.append( agentIssues.warnings, true );
issues.recommendations.append( agentIssues.recommendations, true );
// Validate MCP servers
if ( !structKeyExists( manifest, "mcpServers" ) ) {
issues.warnings.append( "MCP servers not configured in manifest" );
issues.recommendations.append( "Run 'coldbox ai refresh' to initialize MCP servers" );
} else {
var coreServers = manifest.mcpServers.core ?: [];
if ( !coreServers.len() ) {
issues.warnings.append( "No core MCP servers configured" );
issues.recommendations.append( "Run 'coldbox ai refresh' to add core servers" );
}
}
// Build summary
issues.summary = {
"status" : issues.errors.len() ? "error" : ( issues.warnings.len() ? "warning" : "good" ),
"errorCount" : issues.errors.len(),
"warningCount" : issues.warnings.len(),
"recommendationCount" : issues.recommendations.len()
};
return issues;
}
/**
* Load the manifest file
*
* @directory The project directory
*/
struct function loadManifest( required string directory ){
var manifestPath = arguments.directory & "/.ai/.manifest.json";
if ( !fileExists( manifestPath ) ) {
return {};
}
return deserializeJSON( fileRead( manifestPath ) );
}
/**
* Get the manifest file path for a directory
*
* @directory The target directory
*
* @return The full path to the manifest file
*/
string function getManifestPath( required string directory ){
return arguments.directory & "/.ai/.manifest.json";
}
/**
* Save a manifest file and update last sync time
*
* @directory The project directory
* @manifest The manifest struct to save
*/
AIService function saveManifest(
required string directory,
required struct manifest
){
var manifestPath = getManifestPath( arguments.directory )
arguments.manifest.lastSync = dateTimeFormat( now(), "iso" )
fileWrite(
manifestPath,
serializeJSON( arguments.manifest, true )
)
return this
}
/**
* This function updates the last sync time in the manifest without modifying any other content, then saves it.
* This is useful for operations that want to update the sync time after making changes to guidelines/skills/agents without needing to re-save the entire manifest content.
*
* @directory The project directory
*/
AIService function updateLastSync( required string directory ){
var manifest = loadManifest( arguments.directory );
return saveManifest( arguments.directory, manifest );
}
// ========================================
// Private Helpers
// ========================================
/**
* Create .ai directory structure
*
* @directory The project directory where .ai structure will be created
*/
private function createAIDirectoryStructure( required string directory ){
var dirs = [
"#arguments.directory#/.ai",
"#arguments.directory#/.ai/guidelines",
"#arguments.directory#/.ai/guidelines/core",
"#arguments.directory#/.ai/guidelines/modules",
"#arguments.directory#/.ai/guidelines/custom",
"#arguments.directory#/.ai/skills",
"#arguments.directory#/.ai/skills/core",
"#arguments.directory#/.ai/skills/modules",
"#arguments.directory#/.ai/skills/custom"
];
dirs.each( ( dir ) => {
if ( !directoryExists( dir ) ) {
directoryCreate( dir )
}
} )
}
/**
* Update box.json with AI configuration
*
* @directory The project directory
* @language Project language mode (boxlang, cfml, hybrid)
* @agents Comma-separated list of agents
*/
private function updateBoxJsonAIConfig(
required string directory,
required string language,
required string agents
){
var packageDir = arguments.directory;
var boxJson = variables.packageService.readPackageDescriptorRaw( packageDir );
// Add language at top level
boxJson.language = arguments.language;
// Add ai configuration section
boxJson.ai = {
"enabled" : true,
"agents" : listToArray( arguments.agents )
};
variables.packageService.writePackageDescriptor( boxJson, packageDir );
}
/**
* Get AI integration statistics
*
* @directory The project directory
*
* @return Struct with detailed statistics about guidelines, skills, agents, MCP servers, and context usage
*/
function getStats( required string directory ){
// Load manifest and info
var manifest = loadManifest( arguments.directory );
var info = getInfo( arguments.directory );
var stats = {
"guidelines" : {
"total" : info.guidelines.len(),
"core" : 0,
"module" : 0,
"custom" : 0,
"override" : 0,
"totalSize" : 0,
"avgSize" : 0,
"inlinedSize" : 0,
"onDemandSize" : 0
},
"skills" : {
"total" : info.skills.len(),
"core" : 0,
"module" : 0,
"custom" : 0,
"override" : 0,
"totalSize" : 0,
"avgSize" : 0
},
"agents" : {
"total" : manifest.agents.len(),
"configured" : manifest.agents,
"filesSize" : 0
},
"mcpServers" : {
"total" : 0,
"core" : 0,
"module" : 0,
"custom" : 0
},
"language" : manifest.language ?: "unknown",
"templateType" : manifest.templateType ?: "unknown",
"lastSync" : manifest.lastSync ?: "never",
"contextEstimate" : {
"baseContextKB" : 0,
"inlinedKB" : 0,
"onDemandKB" : 0,
"totalAvailableKB" : 0
}
};
// Determine which guidelines are inlined based on language
var inlinedGuidelines = [ "coldbox" ];
if ( stats.language == "boxlang" || stats.language == "hybrid" ) {
inlinedGuidelines.append( "boxlang" );
}
if ( stats.language == "cfml" || stats.language == "hybrid" ) {
inlinedGuidelines.append( "cfml" );
}
// Count guidelines by type and calculate sizes
var aiDir = arguments.directory & "/.ai";
var guidelinesDir = aiDir & "/guidelines";
info.guidelines.each( ( guideline ) => {
var type = guideline.type ?: "module";
if ( structKeyExists( stats.guidelines, type ) ) {
stats.guidelines[ type ]++;
}
// Calculate if this guideline is inlined or on-demand
if ( type == "core" && inlinedGuidelines.find( guideline.name ) ) {
// Core inlined guideline - calculate actual file size
var guidelineFile = guidelinesDir & "/core/" & guideline.name & ".md";
if ( fileExists( guidelineFile ) ) {
stats.guidelines.inlinedSize += getFileInfo( guidelineFile ).size;
}
} else {
// On-demand guideline - only description counts in base context
// Full file counts toward on-demand total
var guidelinePath = "";
if ( type == "core" ) {
guidelinePath = guidelinesDir & "/core/" & guideline.name & ".md";
} else if ( type == "module" ) {
guidelinePath = guidelinesDir & "/modules/" & guideline.name & ".md";
} else if ( type == "custom" ) {
guidelinePath = guidelinesDir & "/custom/" & guideline.name & ".md";
} else if ( type == "override" ) {
guidelinePath = guidelinesDir & "/overrides/" & guideline.name & ".md";
}
if ( len( guidelinePath ) && fileExists( guidelinePath ) ) {
stats.guidelines.onDemandSize += getFileInfo( guidelinePath ).size;
}
}
} );
// Calculate total guidelines size
if ( directoryExists( guidelinesDir ) ) {
stats.guidelines.totalSize = calculateDirectorySize( guidelinesDir );
stats.guidelines.avgSize = stats.guidelines.total > 0 ? int(
stats.guidelines.totalSize / stats.guidelines.total
) : 0;
}
// Count skills by type (all skills are on-demand)
info.skills.each( ( skill ) => {
var type = skill.type ?: "module";
var source = skill.source ?: "";
if ( type == "override" ) {
stats.skills.override++;
} else if ( source == "core" ) {
stats.skills.core++;
} else if ( source == "custom" || type == "custom" ) {
stats.skills.custom++;
} else {
stats.skills.module++;
}
} );
// Skills size (all on-demand)
var skillsDir = aiDir & "/skills";
if ( directoryExists( skillsDir ) ) {
var skillsSize = calculateDirectorySize( skillsDir );
stats.skills.totalSize = skillsSize;
stats.skills.avgSize = stats.skills.total > 0 ? int( skillsSize / stats.skills.total ) : 0;
}
// Count MCP servers
var mcpServers = manifest.mcpServers ?: {
"core" : [],
"module" : [],
"custom" : []
};
stats.mcpServers.core = mcpServers.core.len();
stats.mcpServers.module = mcpServers.module.len();
stats.mcpServers.custom = mcpServers.custom.len();
stats.mcpServers.total = stats.mcpServers.core + stats.mcpServers.module + stats.mcpServers.custom;
// Calculate agent files size (the actual base context)
if ( manifest.agents.len() ) {
manifest.agents.each( ( agent ) => {
var agentPath = variables.agentRegistry.getAgentConfigPath( directory, agent );
if ( fileExists( agentPath ) ) {
stats.agents.filesSize += getFileInfo( agentPath ).size;
}
} );
}
// Calculate context estimates
// Base context = agent files (includes inlined guidelines + inventories)
stats.contextEstimate.baseContextKB = int( stats.agents.filesSize / 1024 );
// Inlined guidelines (part of base context, shown separately for clarity)
stats.contextEstimate.inlinedKB = int( stats.guidelines.inlinedSize / 1024 );
// On-demand resources (not in base context, but available)
stats.contextEstimate.onDemandKB = int( ( stats.guidelines.onDemandSize + stats.skills.totalSize ) / 1024 );
// Total available if all resources were loaded
stats.contextEstimate.totalAvailableKB = int(
( stats.agents.filesSize + stats.guidelines.onDemandSize + stats.skills.totalSize ) / 1024
);
return stats;
}
/**
* Calculate directory size recursively (only .md and .txt files)
*
* @path The directory path
*
* @return Total size in bytes
*/
private function calculateDirectorySize( required string path ){
var totalSize = 0;
if ( !directoryExists( arguments.path ) ) {
return 0;
}
var files = directoryList(
arguments.path,
true,
"path",
"*.md|*.txt"
);
files.each( ( target ) => {
totalSize += getFileInfo( target ).size;
} );
return totalSize;
}
}