create-universal-ai-context 2.3.0 → 2.4.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.
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Windsurf IDE Adapter
3
+ *
4
+ * Generates .windsurf/rules.md file for Windsurf IDE Cascade AI
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { renderTemplateByName, buildContext } = require('../template-renderer');
10
+ const { isManagedFile } = require('../template-coordination');
11
+
12
+ /**
13
+ * Adapter metadata
14
+ */
15
+ const adapter = {
16
+ name: 'windsurf',
17
+ displayName: 'Windsurf IDE',
18
+ description: 'Project rules for Windsurf IDE Cascade AI',
19
+ outputType: 'single-file',
20
+ outputPath: '.windsurf/rules.md'
21
+ };
22
+
23
+ /**
24
+ * Get output path for Windsurf rules file
25
+ * @param {string} projectRoot - Project root directory
26
+ * @returns {string} Output file path
27
+ */
28
+ function getOutputPath(projectRoot) {
29
+ return path.join(projectRoot, '.windsurf', 'rules.md');
30
+ }
31
+
32
+ /**
33
+ * Check if Windsurf output already exists
34
+ * @param {string} projectRoot - Project root directory
35
+ * @returns {boolean}
36
+ */
37
+ function exists(projectRoot) {
38
+ const rulesPath = getOutputPath(projectRoot);
39
+ const windsurfDir = path.join(projectRoot, '.windsurf');
40
+ return fs.existsSync(rulesPath) || fs.existsSync(windsurfDir);
41
+ }
42
+
43
+ /**
44
+ * Generate Windsurf rules file
45
+ * @param {object} analysis - Analysis results from static analyzer
46
+ * @param {object} config - Configuration from CLI
47
+ * @param {string} projectRoot - Project root directory
48
+ * @returns {object} Generation result
49
+ */
50
+ async function generate(analysis, config, projectRoot) {
51
+ const result = {
52
+ success: false,
53
+ adapter: adapter.name,
54
+ files: [],
55
+ errors: []
56
+ };
57
+
58
+ try {
59
+ const rulesPath = getOutputPath(projectRoot);
60
+
61
+ // Check if file exists and is custom (not managed by us)
62
+ if (fs.existsSync(rulesPath) && !config.force) {
63
+ if (!isManagedFile(rulesPath)) {
64
+ result.errors.push({
65
+ message: '.windsurf/rules.md exists and appears to be custom. Use --force to overwrite.',
66
+ code: 'EXISTS_CUSTOM',
67
+ severity: 'error'
68
+ });
69
+ return result;
70
+ }
71
+ }
72
+
73
+ // Build context from analysis
74
+ const context = buildContext(analysis, config, 'windsurf');
75
+
76
+ // Render template
77
+ const content = renderTemplateByName('windsurf-rules', context);
78
+
79
+ // Create .windsurf directory if it doesn't exist
80
+ const windsurfDir = path.dirname(rulesPath);
81
+ if (!fs.existsSync(windsurfDir)) {
82
+ fs.mkdirSync(windsurfDir, { recursive: true });
83
+ }
84
+
85
+ // Write output file
86
+ fs.writeFileSync(rulesPath, content, 'utf-8');
87
+
88
+ result.success = true;
89
+ result.files.push({
90
+ path: rulesPath,
91
+ relativePath: '.windsurf/rules.md',
92
+ size: content.length
93
+ });
94
+ } catch (error) {
95
+ result.errors.push({
96
+ message: error.message,
97
+ stack: error.stack
98
+ });
99
+ }
100
+
101
+ return result;
102
+ }
103
+
104
+ /**
105
+ * Validate Windsurf output
106
+ * @param {string} projectRoot - Project root directory
107
+ * @returns {object} Validation result
108
+ */
109
+ function validate(projectRoot) {
110
+ const issues = [];
111
+ const rulesPath = getOutputPath(projectRoot);
112
+
113
+ if (!fs.existsSync(rulesPath)) {
114
+ issues.push({ file: '.windsurf/rules.md', error: 'not found' });
115
+ } else {
116
+ const content = fs.readFileSync(rulesPath, 'utf-8');
117
+ const placeholderMatch = content.match(/\{\{[A-Z_]+\}\}/g);
118
+ if (placeholderMatch && placeholderMatch.length > 0) {
119
+ issues.push({
120
+ file: '.windsurf/rules.md',
121
+ error: `Found ${placeholderMatch.length} unreplaced placeholders`
122
+ });
123
+ }
124
+ }
125
+
126
+ return {
127
+ valid: issues.filter(i => i.severity !== 'warning').length === 0,
128
+ issues
129
+ };
130
+ }
131
+
132
+ module.exports = {
133
+ ...adapter,
134
+ getOutputPath,
135
+ exists,
136
+ generate,
137
+ validate
138
+ };
@@ -303,7 +303,8 @@ function getPackageVersion() {
303
303
  const pkgPath = path.join(__dirname, '..', 'package.json');
304
304
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
305
305
  return pkg.version || '1.0.0';
306
- } catch {
306
+ } catch (error) {
307
+ // Silently fall back to default version
307
308
  return '1.0.0';
308
309
  }
309
310
  }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Content Preservation Module
3
+ * Handles migration and preservation of custom content during context generation
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * Find custom content in .claude/ directory
11
+ * Custom content = files without "MANAGED BY" header
12
+ * @param {string} claudeDir - Path to .claude/ directory
13
+ * @returns {Array} List of custom content items
14
+ */
15
+ function findCustomContentInClaude(claudeDir) {
16
+ const custom = [];
17
+
18
+ const walkDir = (dir, depth = 0) => {
19
+ // Limit depth to avoid infinite recursion
20
+ if (depth > 10) return;
21
+
22
+ if (!fs.existsSync(dir)) {
23
+ return;
24
+ }
25
+
26
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (entry.isDirectory()) {
29
+ // Skip node_modules and other common dirs
30
+ if (entry.name === 'node_modules' || entry.name === '.git') {
31
+ continue;
32
+ }
33
+ walkDir(path.join(dir, entry.name), depth + 1);
34
+ } else if (entry.name.endsWith('.md')) {
35
+ const filePath = path.join(dir, entry.name);
36
+ try {
37
+ const content = fs.readFileSync(filePath, 'utf-8');
38
+
39
+ // Check if file has managed header
40
+ if (!content.includes('MANAGED BY CREATE-AI-CONTEXT') &&
41
+ !content.includes('Auto-generated by AI Context Engineering')) {
42
+ const relPath = path.relative(claudeDir, filePath);
43
+ custom.push({
44
+ path: relPath,
45
+ type: determineContentType(relPath),
46
+ content: content
47
+ });
48
+ }
49
+ } catch (err) {
50
+ // Skip unreadable files
51
+ }
52
+ }
53
+ }
54
+ };
55
+
56
+ walkDir(claudeDir);
57
+ return custom;
58
+ }
59
+
60
+ /**
61
+ * Determine content type from path
62
+ * @param {string} relPath - Relative path from .claude/
63
+ * @returns {string} Content type
64
+ */
65
+ function determineContentType(relPath) {
66
+ // Normalize path separators to forward slashes for cross-platform compatibility
67
+ const normalizedPath = relPath.replace(/\\/g, '/');
68
+
69
+ if (normalizedPath.startsWith('agents/')) return 'agent';
70
+ if (normalizedPath.startsWith('commands/')) return 'command';
71
+ if (normalizedPath.startsWith('context/')) return 'context';
72
+ if (normalizedPath.startsWith('workflows/')) return 'workflow';
73
+ if (normalizedPath.startsWith('schemas/')) return 'schema';
74
+ if (normalizedPath.startsWith('standards/')) return 'standard';
75
+ return 'other';
76
+ }
77
+
78
+ /**
79
+ * Migrate custom content to .ai-context/custom/
80
+ * @param {string} claudeDir - Source .claude/ directory
81
+ * @param {string} aiContextDir - Destination .ai-context/ directory
82
+ * @param {Array} customItems - Items to migrate
83
+ * @returns {Array} List of migrated items with paths
84
+ */
85
+ function migrateCustomContent(claudeDir, aiContextDir, customItems) {
86
+ const customDir = path.join(aiContextDir, 'custom');
87
+ fs.mkdirSync(customDir, { recursive: true });
88
+
89
+ const migrated = [];
90
+
91
+ for (const item of customItems) {
92
+ const destPath = path.join(customDir, item.path);
93
+ const destDir = path.dirname(destPath);
94
+
95
+ try {
96
+ fs.mkdirSync(destDir, { recursive: true });
97
+
98
+ // Add preservation header
99
+ const preservedHeader = `<!--
100
+ PRESERVED FROM .claude/${item.path}
101
+ This file was migrated from .claude/ to .ai-context/custom/ to preserve custom content.
102
+ Original migration date: ${new Date().toISOString()}
103
+ -->
104
+ `;
105
+
106
+ fs.writeFileSync(destPath, preservedHeader + item.content);
107
+
108
+ migrated.push({
109
+ original: `.claude/${item.path}`,
110
+ destination: `.ai-context/custom/${item.path}`,
111
+ type: item.type
112
+ });
113
+ } catch (err) {
114
+ // Log error but continue with other files
115
+ console.warn(`Failed to migrate ${item.path}: ${err.message}`);
116
+ }
117
+ }
118
+
119
+ return migrated;
120
+ }
121
+
122
+ /**
123
+ * Detect custom content markers in a file
124
+ * @param {string} content - File content
125
+ * @returns {boolean} True if content has custom markers
126
+ */
127
+ function detectCustomContent(content) {
128
+ // Check for absence of managed marker
129
+ if (!content || content.length === 0) {
130
+ return false;
131
+ }
132
+
133
+ const hasManagedMarker = content.includes('MANAGED BY CREATE-AI-CONTEXT') ||
134
+ content.includes('Auto-generated by AI Context Engineering') ||
135
+ content.includes('Managed by create-ai-context') ||
136
+ content.includes('CREATE-AI-CONTEXT');
137
+
138
+ if (!hasManagedMarker) {
139
+ return true;
140
+ }
141
+
142
+ // Check for custom edit markers
143
+ if (content.includes('<!-- CUSTOM EDIT -->') ||
144
+ content.includes('# CUSTOM EDIT') ||
145
+ content.includes('<!-- USER CUSTOM -->')) {
146
+ return true;
147
+ }
148
+
149
+ return false;
150
+ }
151
+
152
+ /**
153
+ * Find custom files in .agent/ directory
154
+ * @param {string} agentDir - Path to .agent/ directory
155
+ * @returns {Array} List of custom file paths (relative)
156
+ */
157
+ function findCustomFilesInAgent(agentDir) {
158
+ const custom = [];
159
+
160
+ if (!fs.existsSync(agentDir)) {
161
+ return custom;
162
+ }
163
+
164
+ const walkDir = (dir, depth = 0) => {
165
+ if (depth > 10) return;
166
+
167
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
168
+ for (const entry of entries) {
169
+ if (entry.isDirectory()) {
170
+ if (entry.name !== 'node_modules' && entry.name !== '.git') {
171
+ walkDir(path.join(dir, entry.name), depth + 1);
172
+ }
173
+ } else if (entry.name.endsWith('.md')) {
174
+ const filePath = path.join(dir, entry.name);
175
+ try {
176
+ const content = fs.readFileSync(filePath, 'utf-8');
177
+
178
+ if (!content.includes('Auto-generated by AI Context Engineering') &&
179
+ !content.includes('Auto-generated by AI Context Engineering v2')) {
180
+ const relPath = path.relative(agentDir, filePath);
181
+ custom.push(relPath);
182
+ }
183
+ } catch (err) {
184
+ // Skip unreadable files
185
+ }
186
+ }
187
+ }
188
+ };
189
+
190
+ walkDir(agentDir);
191
+ return custom;
192
+ }
193
+
194
+ /**
195
+ * Backup existing files before migration
196
+ * @param {string} filePath - File to backup
197
+ * @returns {string} Backup file path
198
+ */
199
+ function backupFile(filePath) {
200
+ if (!fs.existsSync(filePath)) {
201
+ return null;
202
+ }
203
+
204
+ const backupPath = filePath + '.backup.' + Date.now();
205
+ try {
206
+ fs.copyFileSync(filePath, backupPath);
207
+ return backupPath;
208
+ } catch (err) {
209
+ console.warn(`Failed to create backup of ${filePath}: ${err.message}`);
210
+ return null;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Restore from backup
216
+ * @param {string} backupPath - Backup file path
217
+ * @returns {boolean} True if restored successfully
218
+ */
219
+ function restoreFromBackup(backupPath) {
220
+ if (!fs.existsSync(backupPath)) {
221
+ return false;
222
+ }
223
+
224
+ const originalPath = backupPath.replace(/\.backup\.\d+$/, '');
225
+ try {
226
+ fs.copyFileSync(backupPath, originalPath);
227
+ fs.unlinkSync(backupPath);
228
+ return true;
229
+ } catch (err) {
230
+ console.warn(`Failed to restore from backup ${backupPath}: ${err.message}`);
231
+ return false;
232
+ }
233
+ }
234
+
235
+ module.exports = {
236
+ findCustomContentInClaude,
237
+ migrateCustomContent,
238
+ detectCustomContent,
239
+ findCustomFilesInAgent,
240
+ determineContentType,
241
+ backupFile,
242
+ restoreFromBackup
243
+ };
package/lib/index.js CHANGED
@@ -77,7 +77,11 @@ async function run(options = {}) {
77
77
  mode = 'merge', // 'merge' | 'overwrite' | 'interactive'
78
78
  preserveCustom = true,
79
79
  updateRefs = false,
80
- backup = false
80
+ backup = false,
81
+ // Force flag
82
+ force = false,
83
+ // Placeholder validation
84
+ failOnUnreplaced = false
81
85
  } = options;
82
86
 
83
87
  // Determine target directory
@@ -267,9 +271,11 @@ async function run(options = {}) {
267
271
  const placeholdersReplaced = await replacePlaceholders(targetDir, {
268
272
  ...config,
269
273
  techStack,
270
- analysis
274
+ analysis,
275
+ failOnUnreplaced: config.failOnUnreplaced,
276
+ verbose: config.verbose
271
277
  });
272
- spinner.succeed(`Replaced ${placeholdersReplaced} placeholders`);
278
+ spinner.succeed(`Replaced ${placeholdersReplaced.totalReplaced} placeholders`);
273
279
 
274
280
  // Phase 10: AI Orchestration (if in Claude Code environment)
275
281
  if (env.mode === 'full-ai' || env.mode === 'hybrid') {
@@ -289,7 +295,8 @@ async function run(options = {}) {
289
295
  try {
290
296
  generationResults = await generateAllContexts(analysis, config, targetDir, {
291
297
  aiTools: config.aiTools,
292
- verbose: config.verbose
298
+ verbose: config.verbose,
299
+ force: config.force || false
293
300
  });
294
301
  const toolsGenerated = generationResults.generated.map(g => g.adapter).join(', ');
295
302
  if (generationResults.success) {
@@ -7,6 +7,7 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { glob } = require('glob');
10
+ const chalk = require('chalk');
10
11
 
11
12
  /**
12
13
  * Context directory and file names
@@ -463,7 +464,16 @@ function capitalize(str) {
463
464
  /**
464
465
  * Replace placeholders in all files in a directory
465
466
  */
467
+ /**
468
+ * Replace all placeholders in all files
469
+ * @param {string} targetDir - Target directory
470
+ * @param {object} config - Configuration options
471
+ * @param {boolean} config.failOnUnreplaced - Throw if placeholders remain
472
+ * @param {boolean} config.verbose - Log warnings for unreplaced placeholders
473
+ * @returns {object} Results { totalReplaced: number, unreplaced: Array, unreplacedCount: number }
474
+ */
466
475
  async function replacePlaceholders(targetDir, config = {}) {
476
+ const { failOnUnreplaced = false, verbose = false } = config;
467
477
  const contextDir = path.join(targetDir, AI_CONTEXT_DIR);
468
478
  const values = getDefaultValues(config, config.techStack || {}, config.analysis || {});
469
479
 
@@ -476,6 +486,7 @@ async function replacePlaceholders(targetDir, config = {}) {
476
486
  });
477
487
 
478
488
  let totalReplaced = 0;
489
+ const unreplacedDetails = [];
479
490
 
480
491
  for (const filePath of files) {
481
492
  try {
@@ -492,6 +503,10 @@ async function replacePlaceholders(targetDir, config = {}) {
492
503
  fs.writeFileSync(filePath, content, 'utf8');
493
504
  totalReplaced++;
494
505
  }
506
+
507
+ // Check for remaining placeholders
508
+ const remaining = findPlaceholdersInContent(content, filePath);
509
+ unreplacedDetails.push(...remaining);
495
510
  } catch (error) {
496
511
  // Skip files that can't be read
497
512
  }
@@ -513,12 +528,78 @@ async function replacePlaceholders(targetDir, config = {}) {
513
528
  fs.writeFileSync(aiContextPath, content, 'utf8');
514
529
  totalReplaced++;
515
530
  }
531
+
532
+ // Check for remaining placeholders
533
+ const remaining = findPlaceholdersInContent(content, aiContextPath);
534
+ unreplacedDetails.push(...remaining);
516
535
  } catch (error) {
517
536
  // Skip if can't read
518
537
  }
519
538
  }
520
539
 
521
- return totalReplaced;
540
+ // Deduplicate unreplaced items
541
+ const uniqueUnreplaced = deduplicateUnreplaced(unreplacedDetails);
542
+ const unreplacedCount = uniqueUnreplaced.length;
543
+
544
+ // Handle unreplaced placeholders
545
+ if (unreplacedCount > 0) {
546
+ const placeholders = uniqueUnreplaced.map(u => u.placeholder).join(', ');
547
+ const message = `${unreplacedCount} placeholder${unreplacedCount > 1 ? 's' : ''} not replaced: ${placeholders}`;
548
+
549
+ if (verbose) {
550
+ console.warn(chalk.yellow(`⚠ ${message}`));
551
+ uniqueUnreplaced.forEach(u => {
552
+ console.warn(chalk.gray(` - ${u.placeholder} in ${u.file}`));
553
+ });
554
+ }
555
+
556
+ if (failOnUnreplaced) {
557
+ throw new Error(message);
558
+ }
559
+ }
560
+
561
+ return { totalReplaced, unreplaced: uniqueUnreplaced, unreplacedCount };
562
+ }
563
+
564
+ /**
565
+ * Find placeholders in content (helper for replacePlaceholders)
566
+ * @param {string} content - File content
567
+ * @param {string} filePath - File path
568
+ * @returns {Array} List of unreplaced placeholder info
569
+ */
570
+ function findPlaceholdersInContent(content, filePath) {
571
+ const placeholderPattern = /\{\{([A-Z_]+)\}\}/g;
572
+ const found = [];
573
+ let match;
574
+
575
+ while ((match = placeholderPattern.exec(content)) !== null) {
576
+ found.push({
577
+ placeholder: match[0],
578
+ name: match[1],
579
+ file: path.relative(process.cwd(), filePath),
580
+ index: match.index,
581
+ known: KNOWN_PLACEHOLDERS.hasOwnProperty(match[1])
582
+ });
583
+ }
584
+
585
+ return found;
586
+ }
587
+
588
+ /**
589
+ * Deduplicate unreplaced placeholder list
590
+ * @param {Array} unreplaced - List of unreplaced items
591
+ * @returns {Array} Deduplicated list
592
+ */
593
+ function deduplicateUnreplaced(unreplaced) {
594
+ const seen = new Set();
595
+ return unreplaced.filter(item => {
596
+ const key = `${item.placeholder}:${item.file}`;
597
+ if (seen.has(key)) {
598
+ return false;
599
+ }
600
+ seen.add(key);
601
+ return true;
602
+ });
522
603
  }
523
604
 
524
605
  /**
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Tool Coordination Module
3
+ * Helpers for adding tool awareness to generated content
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * Get tool coordination header
11
+ * @param {string} toolName - Name of the tool (claude, copilot, cline, antigravity)
12
+ * @param {string} version - Version of create-ai-context
13
+ * @returns {string} Header comment for the tool
14
+ */
15
+ function getToolCoordinationHeader(toolName, version) {
16
+ version = version || '2.3.0';
17
+
18
+ const newline = '\n';
19
+ const htmlHeaderBase = '<!-- ========================================= -->';
20
+ const htmlFooterBase = '<!-- ========================================= -->';
21
+ const hashHeaderBase = '# ==========================================';
22
+ const hashFooterBase = '# ==========================================';
23
+
24
+ const headers = {
25
+ claude: [
26
+ htmlHeaderBase,
27
+ '<!-- WARNING: CLAUDE CODE CONTEXT -->',
28
+ '<!-- Managed by create-ai-context v' + version + ' -->',
29
+ '<!-- Source: .ai-context/ directory -->',
30
+ htmlFooterBase
31
+ ].join(newline),
32
+
33
+ copilot: [
34
+ htmlHeaderBase,
35
+ '<!-- WARNING: GITHUB COPILOT INSTRUCTIONS -->',
36
+ '<!-- Managed by create-ai-context v' + version + ' -->',
37
+ '<!-- Source: .ai-context/ directory -->',
38
+ htmlFooterBase
39
+ ].join(newline),
40
+
41
+ cline: [
42
+ hashHeaderBase,
43
+ '# WARNING: CLINE RULES',
44
+ '# Managed by create-ai-context v' + version,
45
+ '# Source: .ai-context/ directory',
46
+ hashFooterBase
47
+ ].join(newline),
48
+
49
+ antigravity: [
50
+ htmlHeaderBase,
51
+ '<!-- WARNING: ANTIGRAVITY CONTEXT -->',
52
+ '<!-- Managed by create-ai-context v' + version + ' -->',
53
+ '<!-- Source: .ai-context/ directory -->',
54
+ htmlFooterBase
55
+ ].join(newline)
56
+ };
57
+
58
+ return headers[toolName] || '';
59
+ }
60
+
61
+ /**
62
+ * Get tool coordination footer
63
+ * @param {string} toolName - Name of the tool
64
+ * @returns {string} Footer content with cross-tool references
65
+ */
66
+ function getToolCoordinationFooter(toolName) {
67
+ const footer = '---\n' +
68
+ '## Universal Context Directory\n\n' +
69
+ 'This project uses **AI Context Engineering** for coordinated documentation across all AI tools.\n\n' +
70
+ '**Universal Source:** .ai-context/\n\n' +
71
+ '**Related Tool Contexts:**\n' +
72
+ '- Claude Code: `AI_CONTEXT.md`\n' +
73
+ '- GitHub Copilot: `.github/copilot-instructions.md`\n' +
74
+ '- Cline: `.clinerules`\n' +
75
+ '- Antigravity: `.agent/`\n\n' +
76
+ '**Regeneration Command:**\n' +
77
+ '```bash\n' +
78
+ 'npx create-ai-context generate --ai ' + toolName + '\n```\n\n' +
79
+ '**To Modify Documentation:**\n' +
80
+ 'Edit source files in .ai-context/ then regenerate.\n';
81
+
82
+ return footer;
83
+ }
84
+
85
+ /**
86
+ * Check if file is managed by create-ai-context
87
+ * @param {string} filePath - Path to file to check
88
+ * @returns {boolean} True if file is managed by create-ai-context
89
+ */
90
+ function isManagedFile(filePath) {
91
+ try {
92
+ if (!fs.existsSync(filePath)) {
93
+ return false;
94
+ }
95
+ const content = fs.readFileSync(filePath, 'utf-8');
96
+ return content.includes('Managed by create-ai-context') ||
97
+ content.includes('CREATE-AI-CONTEXT') ||
98
+ content.includes('Auto-generated by AI Context Engineering');
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get universal context reference for templates
106
+ * @returns {object} Object with context directory info
107
+ */
108
+ function getUniversalContextReference() {
109
+ return {
110
+ directory: '.ai-context',
111
+ description: 'Universal AI Context Engineering directory',
112
+ subdirectories: {
113
+ agents: '.ai-context/agents/',
114
+ commands: '.ai-context/commands/',
115
+ context: '.ai-context/context/',
116
+ indexes: '.ai-context/indexes/',
117
+ custom: '.ai-context/custom/',
118
+ tools: '.ai-context/tools/'
119
+ },
120
+ mainFile: 'AI_CONTEXT.md',
121
+ workflowIndex: '.ai-context/context/WORKFLOW_INDEX.md',
122
+ rpiPlan: '.ai-context/RPI_WORKFLOW_PLAN.md'
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Format tool coordination message for CLI output
128
+ * @param {string} toolName - Name of the tool
129
+ * @returns {string} Formatted message
130
+ */
131
+ function formatCoordinationMessage(toolName) {
132
+ const messages = {
133
+ claude: 'Claude Code context generated. Symlinks created from .ai-context/',
134
+ copilot: 'Copilot instructions generated from .ai-context/ universal context',
135
+ cline: 'Cline rules generated from .ai-context/ universal context',
136
+ antigravity: 'Antigravity context generated from .ai-context/ universal context'
137
+ };
138
+
139
+ return messages[toolName] || toolName + ' context generated';
140
+ }
141
+
142
+ module.exports = {
143
+ getToolCoordinationHeader,
144
+ getToolCoordinationFooter,
145
+ isManagedFile,
146
+ getUniversalContextReference,
147
+ formatCoordinationMessage
148
+ };