claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
564 lines (472 loc) • 17.2 kB
text/typescript
/**
* CFN Dependency Ingestion - TypeScript Implementation
* Dynamically discovers and ingests CFN Loop CLI dependency files
*
* Features:
* - Dynamic file discovery from dependency diagram
* - Content injection mode (--inject-content) for atomic context loading
* - Token estimation with 25k safety limit
* - Priority filtering (P0, P1, P2)
* - Type filtering (TS, SH)
* - File existence validation
*
* Usage:
* node ingest-dependencies.js [--inject-content] [--priority P0,P1] [--type TS,SH]
*
* Version: 2.0.0 (Enhancement #5: Content Injection Mode)
*/
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
// Types
type DiagramType = 'cli' | 'docker';
interface Options {
injectContent: boolean;
priorityFilter: string;
typeFilter: string;
includeDeprecated: boolean;
skipValidation: boolean;
diagram: DiagramType;
}
interface FileInfo {
path: string;
priority?: 'P0' | 'P1' | 'P2';
type?: 'TS' | 'SH' | 'MD';
}
interface IngestionResult {
files: string[];
totalTokens: number;
content?: string;
exceeded25kLimit: boolean;
}
// Constants
const PROJECT_ROOT = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
const DIAGRAMS: Record<DiagramType, string> = {
cli: path.join(PROJECT_ROOT, 'readme/CFN_LOOP_DEPENDENCY_DIAGRAM.txt'),
docker: path.join(PROJECT_ROOT, 'readme/CFN_LOOP_DOCKER_DEPENDENCY_DIAGRAM.txt'),
};
const TOKEN_LIMIT = 25000; // 25k token safety limit (for single-file mode)
const CHUNK_SIZE = 20000; // 20k tokens per chunk for multi-file mode
const CHARS_PER_TOKEN = 4; // Rough estimation
const TEMP_DIR = '/tmp/cfn-dependency-chunks'; // Temp directory for chunk files
// Global state for current diagram (set during parseArgs)
let DIAGRAM_PATH = DIAGRAMS.cli;
// Parse command-line arguments
function parseArgs(): Options {
const args = process.argv.slice(2);
const options: Options = {
injectContent: false,
priorityFilter: '',
typeFilter: '',
includeDeprecated: false,
skipValidation: false,
diagram: 'cli',
};
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--inject-content':
options.injectContent = true;
break;
case '--priority':
options.priorityFilter = args[++i] || '';
break;
case '--type':
options.typeFilter = args[++i] || '';
break;
case '--include-deprecated':
options.includeDeprecated = true;
break;
case '--skip-validation':
options.skipValidation = true;
break;
case '--diagram':
const diagramArg = args[++i] || 'cli';
if (diagramArg !== 'cli' && diagramArg !== 'docker') {
console.error(`Invalid diagram type: ${diagramArg}. Use 'cli' or 'docker'`);
process.exit(1);
}
options.diagram = diagramArg as DiagramType;
break;
case '--help':
case '-h':
printHelp();
process.exit(0);
default:
console.error(`Unknown option: ${args[i]}`);
printHelp();
process.exit(1);
}
}
// Set global diagram path based on option
DIAGRAM_PATH = DIAGRAMS[options.diagram];
return options;
}
function printHelp(): void {
console.log(`
CFN Dependency Ingestion - TypeScript Implementation
Usage:
node ingest-dependencies.js [options]
Options:
--diagram cli|docker Select dependency diagram (default: cli)
--inject-content Inject file contents directly (default: output Read commands)
--priority P0,P1,P2 Filter by priority levels (comma-separated)
--type TS,SH Filter by file type (TS=TypeScript, SH=Shell)
--include-deprecated Include deprecated files
--skip-validation Skip file existence validation
--help, -h Show this help message
Diagrams:
cli - CFN_LOOP_DEPENDENCY_DIAGRAM.txt (CLI mode execution flow)
docker - CFN_LOOP_DOCKER_DEPENDENCY_DIAGRAM.txt (Docker mode execution flow)
Examples:
# CLI mode dependencies (default)
node ingest-dependencies.js --inject-content
# Docker mode dependencies
node ingest-dependencies.js --diagram docker --inject-content
# P0 files only with content injection
node ingest-dependencies.js --inject-content --priority P0
# TypeScript files only
node ingest-dependencies.js --type TS
`);
}
// Extract all file paths from the dependency diagram
function extractAllFiles(): string[] {
const diagramContent = fs.readFileSync(DIAGRAM_PATH, 'utf-8');
const filePattern = /\.(claude|src|tests)\/[^ ,)]+\.(md|ts|sh|js|cjs)/g;
const matches = diagramContent.match(filePattern) || [];
// Deduplicate and filter wildcards
const uniqueFiles = [...new Set(matches)]
.filter(file => !file.includes('*') && !file.includes('<'));
return uniqueFiles;
}
// Validate file existence
function validateFileExistence(files: string[], skipValidation: boolean): string[] {
if (skipValidation) {
return files;
}
const validFiles: string[] = [];
let missingCount = 0;
for (const file of files) {
// Handle both absolute and relative paths
const fullPath = path.isAbsolute(file)
? file
: path.join(PROJECT_ROOT, file);
if (fs.existsSync(fullPath)) {
validFiles.push(file);
} else {
console.error(`WARNING: File not found: ${file}`);
missingCount++;
}
}
if (missingCount > 0) {
console.error(`WARNING: ${missingCount} file(s) not found (use --skip-validation to disable checks)`);
}
return validFiles;
}
// Filter by priority markers
function filterByPriority(files: string[], priorityFilter: string): string[] {
if (!priorityFilter) {
return files;
}
const diagramContent = fs.readFileSync(DIAGRAM_PATH, 'utf-8');
const priorities = priorityFilter.split(',');
return files.filter(file => {
const filename = path.basename(file);
return priorities.some(priority => {
const pattern = new RegExp(`\\[${priority}\\].*${filename}`);
return pattern.test(diagramContent);
});
});
}
// Filter by file type
function filterByType(files: string[], typeFilter: string): string[] {
if (!typeFilter) {
return files;
}
return files.filter(file => {
const types = typeFilter.split(',');
return types.some(type => {
if (type === 'TS') {
return /\.(ts|js|cjs)$/.test(file);
} else if (type === 'SH') {
return /\.sh$/.test(file);
}
return false;
});
});
}
// Filter deprecated files
function filterDeprecated(files: string[], includeDeprecated: boolean): string[] {
if (includeDeprecated) {
return files;
}
const diagramContent = fs.readFileSync(DIAGRAM_PATH, 'utf-8');
return files.filter(file => {
const filename = path.basename(file);
const deprecatedPattern = new RegExp(`\\[DEPRECATED\\].*${filename}`);
// Exclude if marked DEPRECATED
if (deprecatedPattern.test(diagramContent)) {
return false;
}
// Exclude helpers unless marked with priority
if (file.includes('/helpers/')) {
const priorityPattern = new RegExp(`\\[(P0|P1|P2)\\].*${filename}`);
return priorityPattern.test(diagramContent);
}
return true;
});
}
// Categorize files by priority
function categorizeByPriority(files: string[], diagramType: DiagramType): Record<string, string[]> {
const diagramContent = fs.readFileSync(DIAGRAM_PATH, 'utf-8');
const diagramFile = diagramType === 'docker'
? 'readme/CFN_LOOP_DOCKER_DEPENDENCY_DIAGRAM.txt'
: 'readme/CFN_LOOP_DEPENDENCY_DIAGRAM.txt';
const categories: Record<string, string[]> = {
diagram: [diagramFile],
P0: [],
P1: [],
P2: [],
coordination: [],
agents: [],
commands: [],
};
for (const file of files) {
const filename = path.basename(file);
if (/\[P0\]/.test(diagramContent) && new RegExp(`\\[P0\\].*${filename}`).test(diagramContent)) {
categories.P0.push(file);
} else if (/\[P1\]/.test(diagramContent) && new RegExp(`\\[P1\\].*${filename}`).test(diagramContent)) {
categories.P1.push(file);
} else if (/\[P2\]/.test(diagramContent) && new RegExp(`\\[P2\\].*${filename}`).test(diagramContent)) {
categories.P2.push(file);
} else if (/coordination-wait|report-completion|orchestrate|cfn-redis/.test(file)) {
categories.coordination.push(file);
} else if (file.includes('.claude/agents/cfn-dev-team')) {
categories.agents.push(file);
} else if (file.includes('.claude/commands')) {
categories.commands.push(file);
}
}
return categories;
}
// Estimate token count
function estimateTokens(text: string): number {
return Math.ceil(text.length / CHARS_PER_TOKEN);
}
// Read file content with error handling
function readFileContent(filePath: string): string {
try {
// Handle both absolute and relative paths
const fullPath = path.isAbsolute(filePath)
? filePath
: path.join(PROJECT_ROOT, filePath);
return fs.readFileSync(fullPath, 'utf-8');
} catch (error) {
return `[ERROR: Could not read file: ${error}]`;
}
}
// Split content into chunks for parallel reading
function splitIntoChunks(files: string[]): { chunkFiles: string[], totalTokens: number } {
// Ensure temp directory exists
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true });
}
// Clean old chunks
const oldChunks = fs.readdirSync(TEMP_DIR).filter(f => f.startsWith('chunk-'));
oldChunks.forEach(f => fs.unlinkSync(path.join(TEMP_DIR, f)));
let currentChunk = '';
let currentChunkTokens = 0;
let chunkIndex = 1;
const chunkFiles: string[] = [];
let totalTokens = 0;
for (const file of files) {
const content = readFileContent(file);
const fileBlock = `
${'='.repeat(80)}
FILE: ${file}
${'='.repeat(80)}
${content}
`;
const blockTokens = estimateTokens(fileBlock);
// If adding this file exceeds chunk size, write current chunk
if (currentChunkTokens + blockTokens > CHUNK_SIZE && currentChunk.length > 0) {
const chunkPath = path.join(TEMP_DIR, `chunk-${chunkIndex}.txt`);
fs.writeFileSync(chunkPath, currentChunk, 'utf-8');
chunkFiles.push(chunkPath);
currentChunk = '';
currentChunkTokens = 0;
chunkIndex++;
}
currentChunk += fileBlock;
currentChunkTokens += blockTokens;
totalTokens += blockTokens;
}
// Write final chunk if any content remains
if (currentChunk.length > 0) {
const chunkPath = path.join(TEMP_DIR, `chunk-${chunkIndex}.txt`);
fs.writeFileSync(chunkPath, currentChunk, 'utf-8');
chunkFiles.push(chunkPath);
}
return { chunkFiles, totalTokens };
}
// Inject content mode - read all files and concatenate
function injectContent(files: string[]): IngestionResult {
let totalContent = '';
let totalChars = 0;
const processedFiles: string[] = [];
// Build content with file markers
for (const file of files) {
const content = readFileContent(file);
const fileBlock = `
${'='.repeat(80)}
FILE: ${file}
${'='.repeat(80)}
${content}
`;
totalContent += fileBlock;
totalChars += fileBlock.length;
processedFiles.push(file);
}
const totalTokens = estimateTokens(totalContent);
const exceeded25kLimit = totalTokens > TOKEN_LIMIT;
if (exceeded25kLimit) {
console.error(`WARNING: Content exceeds 25k token limit (${totalTokens} tokens)`);
console.error('Splitting into 20k token chunks for parallel reading...');
const { chunkFiles, totalTokens: chunkedTokens } = splitIntoChunks(files);
console.log('');
console.log('# CFN Loop CLI Dependency Context (Chunked)');
console.log(`# Total tokens: ${chunkedTokens.toLocaleString()}`);
console.log(`# Chunks: ${chunkFiles.length} files (20k tokens each)`);
console.log(`# Location: ${TEMP_DIR}/`);
console.log('');
console.log('# For Task tool agents: Read these files in parallel');
chunkFiles.forEach((chunkFile, idx) => {
console.log(`Read: ${chunkFile}`);
});
console.log('');
console.log('# Cleanup after reading: rm -rf /tmp/cfn-dependency-chunks/');
return {
files: processedFiles,
totalTokens: chunkedTokens,
exceeded25kLimit: true,
};
}
return {
files: processedFiles,
totalTokens,
content: totalContent,
exceeded25kLimit: false,
};
}
// Output Read commands (traditional mode)
function outputReadCommands(categories: Record<string, string[]>, diagramType: DiagramType): void {
const seenFiles = new Set<string>();
const modeName = diagramType === 'docker' ? 'Docker' : 'CLI';
const diagramFile = categories.diagram[0];
const outputFile = (file: string) => {
if (!seenFiles.has(file)) {
seenFiles.add(file);
console.log(`Read: ${file}`);
}
};
console.log(`# CFN Loop ${modeName} Dependency Ingestion`);
console.log(`# Generated from: ${diagramFile}`);
console.log('');
console.log('# Step 1: Read the dependency diagram');
categories.diagram.forEach(outputFile);
console.log('');
console.log('# Step 2: Read P0 critical path files (required for 5-iteration e2e)');
if (categories.P0.length > 0) {
categories.P0.forEach(outputFile);
} else {
console.log('# No P0 files found matching filters');
}
console.log('');
console.log('# Step 3: Read P1 files (post-validation features)');
if (categories.P1.length > 0) {
categories.P1.forEach(outputFile);
} else {
console.log('# No P1 files found matching filters');
}
console.log('');
console.log('# Step 4: Read P2 files (deferred features)');
if (categories.P2.length > 0) {
categories.P2.forEach(outputFile);
} else {
console.log('# No P2 files found matching filters');
}
console.log('');
console.log('# Step 5: Read coordination layer (Redis/Shell scripts)');
if (categories.coordination.length > 0) {
categories.coordination.forEach(outputFile);
} else {
console.log('# No coordination files found matching filters');
}
console.log('');
console.log('# Step 6: Read agent profiles (coordinators and workers)');
if (categories.agents.length > 0) {
categories.agents.forEach(outputFile);
} else {
console.log('# No agent profile files found');
}
console.log('');
console.log('# Step 7: Read slash commands');
if (categories.commands.length > 0) {
categories.commands.forEach(outputFile);
} else {
console.log('# No command files found');
}
console.log('');
console.log('# Ingestion complete');
console.log(`# Total files: ${seenFiles.size}`);
}
// Main execution
function main(): void {
const options = parseArgs();
// Validate diagram exists
if (!fs.existsSync(DIAGRAM_PATH)) {
console.error(`ERROR: Dependency diagram not found: ${DIAGRAM_PATH}`);
process.exit(1);
}
const modeName = options.diagram === 'docker' ? 'Docker' : 'CLI';
const diagramFile = options.diagram === 'docker'
? 'readme/CFN_LOOP_DOCKER_DEPENDENCY_DIAGRAM.txt'
: 'readme/CFN_LOOP_DEPENDENCY_DIAGRAM.txt';
// Extract and filter files
let files = extractAllFiles();
files = validateFileExistence(files, options.skipValidation);
files = filterByPriority(files, options.priorityFilter);
files = filterByType(files, options.typeFilter);
files = filterDeprecated(files, options.includeDeprecated);
// Add diagram to file list
const allFiles = [diagramFile, ...files];
if (options.injectContent) {
// Content injection mode
const result = injectContent(allFiles);
if (result.exceeded25kLimit) {
// Fallback to Read commands
const categories = categorizeByPriority(files, options.diagram);
outputReadCommands(categories, options.diagram);
} else {
// Output injected content
console.log(`# CFN Loop ${modeName} Dependency Context`);
console.log(`# Files: ${result.files.length}`);
console.log(`# Estimated tokens: ${result.totalTokens.toLocaleString()}`);
console.log(`# Token limit: ${TOKEN_LIMIT.toLocaleString()}`);
console.log('');
console.log(result.content);
}
} else {
// Traditional Read command mode
const categories = categorizeByPriority(files, options.diagram);
outputReadCommands(categories, options.diagram);
}
}
// Execute if run directly (ESM compatible)
const __filename = fileURLToPath(import.meta.url);
const isMainModule = process.argv[1] === __filename;
if (isMainModule) {
main();
}
export { extractAllFiles, filterByPriority, filterByType, injectContent, estimateTokens };