Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/init-config-and-index-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@lytics/dev-agent-cli": patch
---

### Bug Fixes

- **Default config now includes all 9 MCP adapters**: `dev init` previously only enabled 4 adapters. Now all 9 tools (search, refs, map, history, plan, explore, github, status, health) are enabled by default.

### Features

- **Index size reporting**: `dev index` now calculates and displays actual storage size after indexing (e.g., "Storage size: 2.5 MB"). Previously showed 0.

### Internal

- Moved `getDirectorySize` and `formatBytes` utilities to shared `file.ts` module
- Added comprehensive tests for size calculation and formatting
- Added integration test to verify storage size appears in index output

2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ For advanced users or development, you can manually configure the MCP server:

1. **Server Configuration**: The MCP server runs with full feature set including:
- Subagent coordinator with explorer, planner, and PR agents
- All 6 adapters (search, status, plan, explore, github, health)
- All 9 adapters (search, refs, map, history, status, plan, explore, github, health)
- STDIO transport for direct AI tool communication
- Rate limiting (100 req/min default, configurable per-tool)
- Retry logic with exponential backoff
Expand Down
2 changes: 1 addition & 1 deletion TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ dev mcp start --verbose
# In another terminal, send test message
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | dev mcp start

# Should list all 6 tools: dev_search, dev_status, dev_plan, dev_explore, dev_gh, dev_health
# Should list all 9 tools: dev_search, dev_refs, dev_map, dev_history, dev_status, dev_plan, dev_explore, dev_gh, dev_health
```

### Inspect storage
Expand Down
55 changes: 55 additions & 0 deletions packages/cli/src/commands/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as path from 'node:path';
import { Command } from 'commander';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { cleanCommand } from './clean';
import { indexCommand } from './index';
import { initCommand } from './init';

describe('CLI Commands', () => {
Expand Down Expand Up @@ -72,4 +73,58 @@ describe('CLI Commands', () => {
expect(forceOption?.short).toBe('-f');
});
});

describe('index command', () => {
it('should have correct command name and description', () => {
expect(indexCommand.name()).toBe('index');
expect(indexCommand.description()).toBe(
'Index a repository (code, git history, GitHub issues/PRs)'
);
});

it('should display storage size after indexing', async () => {
const indexDir = path.join(testDir, 'index-test');
await fs.mkdir(indexDir, { recursive: true });

// Create a simple TypeScript file to index
await fs.writeFile(
path.join(indexDir, 'sample.ts'),
`export function greet(name: string): string {
return \`Hello, \${name}!\`;
}

export class Calculator {
add(a: number, b: number): number {
return a + b;
}
}`
);

// Capture logger output
const loggedMessages: string[] = [];
const loggerModule = await import('../utils/logger.js');
const originalLog = loggerModule.logger.log;
vi.spyOn(loggerModule.logger, 'log').mockImplementation((msg: string) => {
loggedMessages.push(msg);
});

// Mock process.exit to prevent test termination
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);

// Create a program and add the command
const program = new Command();
program.addCommand(indexCommand);

// Run index command (skip git and github for faster test)
await program.parseAsync(['node', 'cli', 'index', indexDir, '--no-git', '--no-github']);

exitSpy.mockRestore();
loggerModule.logger.log = originalLog;

// Verify storage size is in the output
const storageSizeLog = loggedMessages.find((msg) => msg.includes('Storage size:'));
expect(storageSizeLog).toBeDefined();
expect(storageSizeLog).toMatch(/Storage size:.*\d+(\.\d+)?\s*(B|KB|MB|GB)/);
}, 30000); // 30s timeout for indexing
});
});
7 changes: 5 additions & 2 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import chalk from 'chalk';
import { Command } from 'commander';
import ora from 'ora';
import { getDefaultConfig, loadConfig } from '../utils/config.js';
import { formatBytes, getDirectorySize } from '../utils/file.js';
import { logger } from '../utils/logger.js';

/**
Expand Down Expand Up @@ -144,11 +145,12 @@ export const indexCommand = new Command('index')
},
});

// Update metadata with indexing stats
// Update metadata with indexing stats (calculate actual storage size)
const storageSize = await getDirectorySize(storagePath);
await updateIndexedStats(storagePath, {
files: stats.filesScanned,
components: stats.documentsIndexed,
size: 0, // TODO: Calculate actual size
size: storageSize,
});

await indexer.close();
Expand All @@ -164,6 +166,7 @@ export const indexCommand = new Command('index')
logger.log(` ${chalk.cyan('Documents extracted:')} ${stats.documentsExtracted}`);
logger.log(` ${chalk.cyan('Documents indexed:')} ${stats.documentsIndexed}`);
logger.log(` ${chalk.cyan('Vectors stored:')} ${stats.vectorsStored}`);
logger.log(` ${chalk.cyan('Storage size:')} ${formatBytes(storageSize)}`);
logger.log(` ${chalk.cyan('Duration:')} ${codeDuration}s`);

// Index git history if available
Expand Down
41 changes: 1 addition & 40 deletions packages/cli/src/commands/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import chalk from 'chalk';
import { Command } from 'commander';
import ora from 'ora';
import { loadConfig } from '../utils/config.js';
import { formatBytes, getDirectorySize } from '../utils/file.js';
import { logger } from '../utils/logger.js';

/**
Expand Down Expand Up @@ -62,46 +63,6 @@ async function detectLocalIndexes(repositoryPath: string): Promise<{
return result;
}

/**
* Calculate directory size recursively
*/
async function getDirectorySize(dirPath: string): Promise<number> {
try {
const stats = await fs.stat(dirPath);
if (!stats.isDirectory()) {
return stats.size;
}

const entries = await fs.readdir(dirPath, { withFileTypes: true });
let size = 0;

for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
size += await getDirectorySize(fullPath);
} else {
const stat = await fs.stat(fullPath);
size += stat.size;
}
}

return size;
} catch {
return 0;
}
}

/**
* Format bytes to human-readable string
*/
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;
}

/**
* Prompt user for confirmation
*/
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/src/utils/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ describe('Config Utilities', () => {
const config = getDefaultConfig();
expect(config.repositoryPath).toBe(process.cwd());
});

it('should include all 9 MCP adapters', () => {
const config = getDefaultConfig('/test/path');
const adapters = config.mcp?.adapters;

expect(adapters).toBeDefined();
expect(Object.keys(adapters!)).toHaveLength(9);

// Verify all adapters are present and enabled by default
expect(adapters?.search?.enabled).toBe(true);
expect(adapters?.refs?.enabled).toBe(true);
expect(adapters?.map?.enabled).toBe(true);
expect(adapters?.history?.enabled).toBe(true);
expect(adapters?.plan?.enabled).toBe(true);
expect(adapters?.explore?.enabled).toBe(true);
expect(adapters?.github?.enabled).toBe(true);
expect(adapters?.status?.enabled).toBe(true);
expect(adapters?.health?.enabled).toBe(true);
});
});

describe('saveConfig', () => {
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,14 @@ export function getDefaultConfig(repositoryPath: string = process.cwd()): DevAge
mcp: {
adapters: {
search: { enabled: true },
github: { enabled: true },
refs: { enabled: true },
map: { enabled: true },
history: { enabled: true },
plan: { enabled: true },
explore: { enabled: true },
status: { enabled: false },
github: { enabled: true },
status: { enabled: true },
health: { enabled: true },
},
},
// Legacy fields for backward compatibility
Expand Down
102 changes: 101 additions & 1 deletion packages/cli/src/utils/file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { normalizeFilePath, prepareFileForSearch, readFileContent, resolveFilePath } from './file';
import {
formatBytes,
getDirectorySize,
normalizeFilePath,
prepareFileForSearch,
readFileContent,
resolveFilePath,
} from './file';

describe('File Utilities', () => {
describe('resolveFilePath', () => {
Expand Down Expand Up @@ -234,4 +241,97 @@ describe('File Utilities', () => {
expect(result.relativePath).toBe('test.txt');
});
});

describe('formatBytes', () => {
it('should format 0 bytes', () => {
expect(formatBytes(0)).toBe('0 B');
});

it('should format bytes', () => {
expect(formatBytes(500)).toBe('500 B');
});

it('should format kilobytes', () => {
expect(formatBytes(1024)).toBe('1 KB');
expect(formatBytes(1536)).toBe('1.5 KB');
});

it('should format megabytes', () => {
expect(formatBytes(1024 * 1024)).toBe('1 MB');
expect(formatBytes(1024 * 1024 * 2.5)).toBe('2.5 MB');
});

it('should format gigabytes', () => {
expect(formatBytes(1024 * 1024 * 1024)).toBe('1 GB');
});

it('should round to one decimal place', () => {
expect(formatBytes(1234567)).toBe('1.2 MB');
});
});

describe('getDirectorySize', () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dirsize-test-'));
});

afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});

it('should return 0 for empty directory', async () => {
const size = await getDirectorySize(tempDir);
expect(size).toBe(0);
});

it('should calculate size of single file', async () => {
const content = 'Hello, World!'; // 13 bytes
await fs.writeFile(path.join(tempDir, 'test.txt'), content);

const size = await getDirectorySize(tempDir);
expect(size).toBe(13);
});

it('should calculate size of multiple files', async () => {
await fs.writeFile(path.join(tempDir, 'file1.txt'), 'aaaa'); // 4 bytes
await fs.writeFile(path.join(tempDir, 'file2.txt'), 'bbbbbb'); // 6 bytes

const size = await getDirectorySize(tempDir);
expect(size).toBe(10);
});

it('should calculate size recursively', async () => {
const subDir = path.join(tempDir, 'subdir');
await fs.mkdir(subDir);
await fs.writeFile(path.join(tempDir, 'root.txt'), '12345'); // 5 bytes
await fs.writeFile(path.join(subDir, 'nested.txt'), '67890'); // 5 bytes

const size = await getDirectorySize(tempDir);
expect(size).toBe(10);
});

it('should handle deeply nested directories', async () => {
const deepDir = path.join(tempDir, 'a', 'b', 'c');
await fs.mkdir(deepDir, { recursive: true });
await fs.writeFile(path.join(deepDir, 'deep.txt'), 'content'); // 7 bytes

const size = await getDirectorySize(tempDir);
expect(size).toBe(7);
});

it('should return 0 for non-existent directory', async () => {
const size = await getDirectorySize('/nonexistent/path');
expect(size).toBe(0);
});

it('should handle single file path', async () => {
const filePath = path.join(tempDir, 'single.txt');
await fs.writeFile(filePath, '12345678'); // 8 bytes

const size = await getDirectorySize(filePath);
expect(size).toBe(8);
});
});
});
41 changes: 41 additions & 0 deletions packages/cli/src/utils/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,44 @@ export async function prepareFileForSearch(
relativePath,
};
}

/**
* Calculate directory size recursively
* Returns total size in bytes
*/
export async function getDirectorySize(dirPath: string): Promise<number> {
try {
const stats = await fs.stat(dirPath);
if (!stats.isDirectory()) {
return stats.size;
}

const entries = await fs.readdir(dirPath, { withFileTypes: true });
let size = 0;

for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
size += await getDirectorySize(fullPath);
} else {
const stat = await fs.stat(fullPath);
size += stat.size;
}
}

return size;
} catch {
return 0;
}
}

/**
* Format bytes to human-readable string
*/
export function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
}