ownkey 0.2.1 → 0.5.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 (49) hide show
  1. package/README.md +64 -32
  2. package/dist/ai/factory.d.ts +23 -0
  3. package/dist/ai/factory.d.ts.map +1 -0
  4. package/dist/ai/factory.js +96 -0
  5. package/dist/ai/factory.js.map +1 -0
  6. package/dist/ai/gemini.d.ts +21 -0
  7. package/dist/ai/gemini.d.ts.map +1 -0
  8. package/dist/ai/gemini.js +87 -0
  9. package/dist/ai/gemini.js.map +1 -0
  10. package/dist/ai/provider.d.ts +39 -0
  11. package/dist/ai/provider.d.ts.map +1 -0
  12. package/dist/ai/provider.js +99 -0
  13. package/dist/ai/provider.js.map +1 -0
  14. package/dist/commands/config.js +3 -1
  15. package/dist/commands/config.js.map +1 -1
  16. package/dist/commands/suggest.d.ts.map +1 -1
  17. package/dist/commands/suggest.js +102 -10
  18. package/dist/commands/suggest.js.map +1 -1
  19. package/dist/config/defaults.d.ts.map +1 -1
  20. package/dist/config/defaults.js +5 -2
  21. package/dist/config/defaults.js.map +1 -1
  22. package/dist/config/keychain.d.ts.map +1 -1
  23. package/dist/config/keychain.js +12 -0
  24. package/dist/config/keychain.js.map +1 -1
  25. package/dist/db/client.d.ts.map +1 -1
  26. package/dist/db/client.js +41 -15
  27. package/dist/db/client.js.map +1 -1
  28. package/dist/types/index.d.ts +4 -1
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/types/index.js.map +1 -1
  31. package/package.json +10 -2
  32. package/.prettierrc +0 -9
  33. package/src/commands/apply.ts +0 -27
  34. package/src/commands/config.ts +0 -154
  35. package/src/commands/scan.ts +0 -114
  36. package/src/commands/suggest.ts +0 -37
  37. package/src/config/defaults.ts +0 -80
  38. package/src/config/keychain.ts +0 -125
  39. package/src/config/manager.ts +0 -166
  40. package/src/core/scanner.ts +0 -174
  41. package/src/db/client.ts +0 -151
  42. package/src/db/operations.ts +0 -217
  43. package/src/db/schema.ts +0 -118
  44. package/src/index.ts +0 -74
  45. package/src/types/index.ts +0 -198
  46. package/src/utils/encryption.ts +0 -54
  47. package/src/utils/logger.ts +0 -111
  48. package/tsconfig.json +0 -33
  49. /package/{src → dist}/db/migrations/001_init.sql +0 -0
@@ -1,114 +0,0 @@
1
- import { Command } from 'commander';
2
- import { resolve } from 'path';
3
- import { existsSync } from 'fs';
4
- import { logger } from '../utils/logger.js';
5
- import { FileScanner } from '../core/scanner.js';
6
- import { dbOps } from '../db/operations.js';
7
- import { dbClient } from '../db/client.js';
8
- import chalk from 'chalk';
9
-
10
- export const scanCommand = new Command('scan')
11
- .description('Scan a project directory')
12
- .argument('[path]', 'Path to scan', '.')
13
- .option('--max-file-size <size>', 'Maximum file size (e.g., 500kb, 1mb)', '500kb')
14
- .option('--ignore <patterns...>', 'Additional ignore patterns')
15
- .action(async (path: string, options) => {
16
- try {
17
- const targetPath = resolve(path);
18
-
19
- // Validate path
20
- if (!existsSync(targetPath)) {
21
- logger.error(`Path does not exist: ${targetPath}`);
22
- process.exit(1);
23
- }
24
-
25
- logger.info(`Scanning: ${chalk.cyan(targetPath)}`);
26
-
27
- // Parse max file size
28
- const maxFileSize = parseFileSize(options.maxFileSize);
29
-
30
- // Create scanner
31
- const scanner = new FileScanner(targetPath, maxFileSize);
32
-
33
- // Scan
34
- const startTime = Date.now();
35
- const result = await scanner.scan();
36
- const duration = ((Date.now() - startTime) / 1000).toFixed(2);
37
-
38
- // Display results
39
- console.log(chalk.bold('\nšŸ“Š Scan Results:\n'));
40
- console.log(` Files found: ${chalk.green(result.files.length)}`);
41
- console.log(` Total size: ${formatBytes(result.totalSize)}`);
42
- console.log(` Skipped files: ${result.skippedFiles.length}`);
43
- console.log(` Duration: ${duration}s\n`);
44
-
45
- if (result.warnings.length > 0) {
46
- console.log(chalk.yellow(`āš ļø ${result.warnings.length} warnings:`));
47
- result.warnings.slice(0, 5).forEach((w) => console.log(chalk.gray(` - ${w}`)));
48
- if (result.warnings.length > 5) {
49
- console.log(chalk.gray(` ... and ${result.warnings.length - 5} more`));
50
- }
51
- console.log();
52
- }
53
-
54
- // Store in database if connected
55
- if (dbClient.isConnected()) {
56
- const projectName = targetPath.split(/[/\\]/).pop() || 'unknown';
57
- let project = await dbOps.getProjectByPath(targetPath);
58
-
59
- if (!project) {
60
- project = await dbOps.createProject(projectName, targetPath);
61
- logger.verbose('Created project record in database');
62
- }
63
-
64
- if (project) {
65
- const run = await dbOps.createRun(project.id, `scan ${path}`);
66
- if (run) {
67
- await dbOps.updateRunStatus(
68
- run.id,
69
- 'completed',
70
- `Scanned ${result.files.length} files`
71
- );
72
- }
73
- }
74
- }
75
-
76
- logger.success('Scan complete');
77
-
78
- // Show next steps
79
- console.log(chalk.cyan('Next step:'));
80
- console.log(chalk.gray(` ownkey suggest ${path}\n`));
81
- } catch (error) {
82
- logger.error('Scan failed', error as Error);
83
- process.exit(1);
84
- }
85
- });
86
-
87
- function parseFileSize(sizeStr: string): number {
88
- const match = sizeStr.match(/^(\d+(?:\.\d+)?)(kb|mb|gb)?$/i);
89
- if (!match) {
90
- throw new Error(`Invalid file size format: ${sizeStr}`);
91
- }
92
-
93
- const value = parseFloat(match[1]);
94
- const unit = (match[2] || 'b').toLowerCase();
95
-
96
- switch (unit) {
97
- case 'b':
98
- return value;
99
- case 'kb':
100
- return value * 1024;
101
- case 'mb':
102
- return value * 1024 * 1024;
103
- case 'gb':
104
- return value * 1024 * 1024 * 1024;
105
- default:
106
- return value;
107
- }
108
- }
109
-
110
- function formatBytes(bytes: number): string {
111
- if (bytes < 1024) return `${bytes} B`;
112
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
113
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
114
- }
@@ -1,37 +0,0 @@
1
- import { Command } from 'commander';
2
- import { resolve } from 'path';
3
- import { existsSync } from 'fs';
4
- import { logger } from '../utils/logger.js';
5
- import chalk from 'chalk';
6
-
7
- export const suggestCommand = new Command('suggest')
8
- .description('Generate AI-powered suggestions for your code')
9
- .argument('[path]', 'Path to analyze', '.')
10
- .option('--type <type>', 'Filter by suggestion type (bug, security, performance, etc.)')
11
- .option('--severity <severity>', 'Minimum severity (critical, high, medium, low, info)')
12
- .action(async (path: string) => {
13
- try {
14
- const targetPath = resolve(path);
15
-
16
- if (!existsSync(targetPath)) {
17
- logger.error(`Path does not exist: ${targetPath}`);
18
- process.exit(1);
19
- }
20
-
21
- logger.info(`Analyzing: ${chalk.cyan(targetPath)}`);
22
-
23
- // TODO: Implement AI analysis
24
- // For now, show a placeholder message
25
- console.log(chalk.yellow('\nāš ļø AI provider integration coming in v0.5.0\n'));
26
- console.log('This command will:');
27
- console.log(chalk.gray(' 1. Load project context'));
28
- console.log(chalk.gray(' 2. Send to AI provider for analysis'));
29
- console.log(chalk.gray(' 3. Display suggestions'));
30
- console.log(chalk.gray(' 4. Store in database\n'));
31
-
32
- logger.info('Suggest command placeholder - full implementation coming soon');
33
- } catch (error) {
34
- logger.error('Suggest failed', error as Error);
35
- process.exit(1);
36
- }
37
- });
@@ -1,80 +0,0 @@
1
- import { Config, LogLevel } from '../types/index.js';
2
-
3
- export const DEFAULT_CONFIG: Config = {
4
- providers: {
5
- openai: {
6
- model: 'gpt-4-turbo',
7
- },
8
- anthropic: {
9
- model: 'claude-3-sonnet-20240229',
10
- },
11
- ollama: {
12
- endpoint: 'http://localhost:11434',
13
- model: 'codellama',
14
- },
15
- },
16
- defaults: {
17
- maxFileSize: '500kb',
18
- logLevel: 'normal' as LogLevel,
19
- },
20
- };
21
-
22
- export const CONFIG_DIR = '.ownkey';
23
- export const CONFIG_FILE = 'config.json';
24
- export const KEYS_FILE = 'keys.enc';
25
- export const LOGS_DIR = 'logs';
26
-
27
- export const DEFAULT_EXCLUDES = [
28
- '.git',
29
- 'node_modules',
30
- 'dist',
31
- 'build',
32
- 'target',
33
- 'out',
34
- 'vendor',
35
- '.cache',
36
- '.next',
37
- '.nuxt',
38
- 'coverage',
39
- '.env',
40
- '*.lock',
41
- '*.min.js',
42
- '*.min.css',
43
- ];
44
-
45
- export const BINARY_EXTENSIONS = [
46
- '.jpg',
47
- '.jpeg',
48
- '.png',
49
- '.gif',
50
- '.bmp',
51
- '.ico',
52
- '.svg',
53
- '.webp',
54
- '.mp4',
55
- '.avi',
56
- '.mov',
57
- '.mp3',
58
- '.wav',
59
- '.zip',
60
- '.tar',
61
- '.gz',
62
- '.rar',
63
- '.7z',
64
- '.exe',
65
- '.dll',
66
- '.so',
67
- '.dylib',
68
- '.pdf',
69
- '.doc',
70
- '.docx',
71
- '.xls',
72
- '.xlsx',
73
- '.ppt',
74
- '.pptx',
75
- ];
76
-
77
- export const MAX_FILE_SIZE_BYTES = 500 * 1024; // 500KB
78
-
79
- export const BACKUP_EXTENSION = '.ownkey.bak';
80
- export const MAX_BACKUPS_PER_FILE = 3;
@@ -1,125 +0,0 @@
1
- import { AIProvider } from '../types/index.js';
2
- import { readFile, writeFile, mkdir } from 'fs/promises';
3
- import { existsSync } from 'fs';
4
- import { join } from 'path';
5
- import { homedir } from 'os';
6
- import { logger } from '../utils/logger.js';
7
- import { Encryption } from '../utils/encryption.js';
8
- import { KEYS_FILE, CONFIG_DIR } from './defaults.js';
9
-
10
- // Try to use keytar, but fallback gracefully if not available
11
- let keytar: any = null;
12
- try {
13
- keytar = await import('keytar');
14
- } catch (error) {
15
- logger.verbose('Keytar not available, using encrypted file storage for API keys');
16
- }
17
-
18
- const SERVICE_NAME = 'ownkey-cli';
19
- const ENCRYPTION_PASSWORD = 'ownkey-local-encryption-key'; // In production, derive from machine ID
20
-
21
- export class KeychainManager {
22
- private keysFilePath: string;
23
- private useKeychain: boolean;
24
-
25
- constructor() {
26
- this.keysFilePath = join(homedir(), CONFIG_DIR, KEYS_FILE);
27
- this.useKeychain = keytar !== null;
28
- }
29
-
30
- private async ensureKeysDir(): Promise<void> {
31
- const dir = join(homedir(), CONFIG_DIR);
32
- if (!existsSync(dir)) {
33
- await mkdir(dir, { recursive: true });
34
- }
35
- }
36
-
37
- async setApiKey(provider: AIProvider, apiKey: string): Promise<void> {
38
- const accountName = `${provider}-api-key`;
39
-
40
- if (this.useKeychain) {
41
- try {
42
- await keytar.setPassword(SERVICE_NAME, accountName, apiKey);
43
- logger.verbose(`Stored ${provider} API key in system keychain`);
44
- return;
45
- } catch (error) {
46
- logger.warn(`Failed to store in keychain, falling back to encrypted file`);
47
- this.useKeychain = false;
48
- }
49
- }
50
-
51
- // Fallback to encrypted file
52
- await this.ensureKeysDir();
53
- const keys = await this.loadKeysFromFile();
54
- keys[provider] = apiKey;
55
- await this.saveKeysToFile(keys);
56
- logger.verbose(`Stored ${provider} API key in encrypted file`);
57
- }
58
-
59
- async getApiKey(provider: AIProvider): Promise<string | null> {
60
- const accountName = `${provider}-api-key`;
61
-
62
- if (this.useKeychain) {
63
- try {
64
- const key = await keytar.getPassword(SERVICE_NAME, accountName);
65
- if (key) {
66
- logger.verbose(`Retrieved ${provider} API key from system keychain`);
67
- return key;
68
- }
69
- } catch (error) {
70
- logger.warn(`Failed to retrieve from keychain, trying encrypted file`);
71
- this.useKeychain = false;
72
- }
73
- }
74
-
75
- // Fallback to encrypted file
76
- const keys = await this.loadKeysFromFile();
77
- const key = keys[provider] || null;
78
- if (key) {
79
- logger.verbose(`Retrieved ${provider} API key from encrypted file`);
80
- }
81
- return key;
82
- }
83
-
84
- async deleteApiKey(provider: AIProvider): Promise<void> {
85
- const accountName = `${provider}-api-key`;
86
-
87
- if (this.useKeychain) {
88
- try {
89
- await keytar.deletePassword(SERVICE_NAME, accountName);
90
- logger.verbose(`Deleted ${provider} API key from system keychain`);
91
- } catch (error) {
92
- // Ignore errors
93
- }
94
- }
95
-
96
- // Also delete from file
97
- const keys = await this.loadKeysFromFile();
98
- delete keys[provider];
99
- await this.saveKeysToFile(keys);
100
- logger.verbose(`Deleted ${provider} API key from encrypted file`);
101
- }
102
-
103
- private async loadKeysFromFile(): Promise<Record<string, string>> {
104
- if (!existsSync(this.keysFilePath)) {
105
- return {};
106
- }
107
-
108
- try {
109
- const encryptedData = await readFile(this.keysFilePath, 'utf-8');
110
- const decrypted = await Encryption.decrypt(encryptedData, ENCRYPTION_PASSWORD);
111
- return JSON.parse(decrypted);
112
- } catch (error) {
113
- logger.warn('Failed to decrypt keys file, starting fresh');
114
- return {};
115
- }
116
- }
117
-
118
- private async saveKeysToFile(keys: Record<string, string>): Promise<void> {
119
- const json = JSON.stringify(keys);
120
- const encrypted = await Encryption.encrypt(json, ENCRYPTION_PASSWORD);
121
- await writeFile(this.keysFilePath, encrypted, 'utf-8');
122
- }
123
- }
124
-
125
- export const keychainManager = new KeychainManager();
@@ -1,166 +0,0 @@
1
- import { Config, ConfigError, AIProvider } from '../types/index.js';
2
- import { readFile, writeFile, mkdir } from 'fs/promises';
3
- import { existsSync } from 'fs';
4
- import { join } from 'path';
5
- import { homedir } from 'os';
6
- import { logger } from '../utils/logger.js';
7
- import { keychainManager } from './keychain.js';
8
- import { DEFAULT_CONFIG, CONFIG_DIR, CONFIG_FILE } from './defaults.js';
9
-
10
- export class ConfigManager {
11
- private configPath: string;
12
- private config: Config | null = null;
13
-
14
- constructor() {
15
- this.configPath = join(homedir(), CONFIG_DIR, CONFIG_FILE);
16
- }
17
-
18
- async ensureConfigDir(): Promise<void> {
19
- const dir = join(homedir(), CONFIG_DIR);
20
- if (!existsSync(dir)) {
21
- await mkdir(dir, { recursive: true });
22
- logger.verbose(`Created config directory: ${dir}`);
23
- }
24
- }
25
-
26
- async exists(): Promise<boolean> {
27
- return existsSync(this.configPath);
28
- }
29
-
30
- async load(): Promise<Config> {
31
- if (this.config) {
32
- return this.config;
33
- }
34
-
35
- if (!existsSync(this.configPath)) {
36
- logger.verbose('No config file found, using defaults');
37
- this.config = { ...DEFAULT_CONFIG };
38
- return this.config;
39
- }
40
-
41
- try {
42
- const content = await readFile(this.configPath, 'utf-8');
43
- const loaded = JSON.parse(content);
44
-
45
- // Merge with defaults to ensure all fields exist
46
- this.config = {
47
- ...DEFAULT_CONFIG,
48
- ...loaded,
49
- providers: {
50
- ...DEFAULT_CONFIG.providers,
51
- ...loaded.providers,
52
- },
53
- defaults: {
54
- ...DEFAULT_CONFIG.defaults,
55
- ...loaded.defaults,
56
- },
57
- };
58
-
59
- logger.verbose('Loaded configuration from file');
60
- return this.config!;
61
- } catch (error) {
62
- throw new ConfigError('Failed to load configuration file', error);
63
- }
64
- }
65
-
66
- async save(config: Config): Promise<void> {
67
- await this.ensureConfigDir();
68
-
69
- try {
70
- const content = JSON.stringify(config, null, 2);
71
- await writeFile(this.configPath, content, 'utf-8');
72
- this.config = config;
73
- logger.verbose('Saved configuration to file');
74
- } catch (error) {
75
- throw new ConfigError('Failed to save configuration file', error);
76
- }
77
- }
78
-
79
- async update(updates: Partial<Config>): Promise<void> {
80
- const current = await this.load();
81
- const updated = {
82
- ...current,
83
- ...updates,
84
- providers: {
85
- ...current.providers,
86
- ...updates.providers,
87
- },
88
- defaults: {
89
- ...current.defaults,
90
- ...updates.defaults,
91
- },
92
- };
93
- await this.save(updated);
94
- }
95
-
96
- async setSupabase(url: string, serviceRoleKey: string): Promise<void> {
97
- const config = await this.load();
98
- config.supabaseUrl = url;
99
- config.supabaseServiceRoleKey = serviceRoleKey;
100
- await this.save(config);
101
- logger.success('Supabase configuration saved');
102
- }
103
-
104
- async setProvider(provider: AIProvider, apiKey: string, model?: string): Promise<void> {
105
- const config = await this.load();
106
- config.activeProvider = provider;
107
-
108
- if (model) {
109
- if (!config.providers[provider]) {
110
- config.providers[provider] = {};
111
- }
112
- config.providers[provider]!.model = model;
113
- }
114
-
115
- await this.save(config);
116
- await keychainManager.setApiKey(provider, apiKey);
117
- logger.success(`${provider} provider configured`);
118
- }
119
-
120
- async getApiKey(provider?: AIProvider): Promise<string | null> {
121
- const config = await this.load();
122
- const targetProvider = provider || config.activeProvider;
123
-
124
- if (!targetProvider) {
125
- return null;
126
- }
127
-
128
- return await keychainManager.getApiKey(targetProvider);
129
- }
130
-
131
- async validate(): Promise<{ valid: boolean; errors: string[] }> {
132
- const errors: string[] = [];
133
-
134
- try {
135
- const config = await this.load();
136
-
137
- // Check if at least one provider is configured
138
- if (!config.activeProvider) {
139
- errors.push('No active AI provider configured. Run: ownkey config');
140
- }
141
-
142
- // Check if API key exists for active provider
143
- if (config.activeProvider) {
144
- const apiKey = await this.getApiKey(config.activeProvider);
145
- if (!apiKey) {
146
- errors.push(`No API key found for ${config.activeProvider}`);
147
- }
148
- }
149
-
150
- return { valid: errors.length === 0, errors };
151
- } catch (error) {
152
- errors.push('Failed to load configuration');
153
- return { valid: false, errors };
154
- }
155
- }
156
-
157
- async reset(): Promise<void> {
158
- this.config = null;
159
- if (existsSync(this.configPath)) {
160
- await writeFile(this.configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
161
- logger.success('Configuration reset to defaults');
162
- }
163
- }
164
- }
165
-
166
- export const configManager = new ConfigManager();
@@ -1,174 +0,0 @@
1
- import ignore, { Ignore } from 'ignore';
2
- import { readFile, stat, readdir } from 'fs/promises';
3
- import { join, relative, extname } from 'path';
4
- import { existsSync } from 'fs';
5
- import { FileMetadata, FileContent, ScanResult } from '../types/index.js';
6
- import { logger } from '../utils/logger.js';
7
- import { DEFAULT_EXCLUDES, BINARY_EXTENSIONS, MAX_FILE_SIZE_BYTES } from '../config/defaults.js';
8
-
9
- export class FileScanner {
10
- private ig: Ignore;
11
- private maxFileSize: number;
12
- private rootPath: string;
13
- private skippedFiles: string[] = [];
14
- private warnings: string[] = [];
15
-
16
- constructor(rootPath: string, maxFileSize: number = MAX_FILE_SIZE_BYTES) {
17
- this.rootPath = rootPath;
18
- this.maxFileSize = maxFileSize;
19
- this.ig = ignore();
20
- this.ig.add(DEFAULT_EXCLUDES);
21
- }
22
-
23
- async initialize(): Promise<void> {
24
- // Load .gitignore
25
- const gitignorePath = join(this.rootPath, '.gitignore');
26
- if (existsSync(gitignorePath)) {
27
- const content = await readFile(gitignorePath, 'utf-8');
28
- this.ig.add(content);
29
- logger.verbose('Loaded .gitignore patterns');
30
- }
31
-
32
- // Load .ownkeyignore
33
- const ownkeyignorePath = join(this.rootPath, '.ownkeyignore');
34
- if (existsSync(ownkeyignorePath)) {
35
- const content = await readFile(ownkeyignorePath, 'utf-8');
36
- this.ig.add(content);
37
- logger.verbose('Loaded .ownkeyignore patterns');
38
- }
39
- }
40
-
41
- async scan(): Promise<ScanResult> {
42
- await this.initialize();
43
-
44
- const files: FileMetadata[] = [];
45
- let totalSize = 0;
46
-
47
- await this.scanDirectory(this.rootPath, files);
48
-
49
- for (const file of files) {
50
- totalSize += file.size;
51
- }
52
-
53
- logger.verbose(`Scanned ${files.length} files (${this.formatBytes(totalSize)})`);
54
-
55
- if (this.skippedFiles.length > 0) {
56
- logger.verbose(`Skipped ${this.skippedFiles.length} files`);
57
- }
58
-
59
- return {
60
- files,
61
- totalSize,
62
- skippedFiles: this.skippedFiles,
63
- warnings: this.warnings,
64
- };
65
- }
66
-
67
- private async scanDirectory(dirPath: string, files: FileMetadata[]): Promise<void> {
68
- let entries;
69
- try {
70
- entries = await readdir(dirPath, { withFileTypes: true });
71
- } catch (error) {
72
- this.warnings.push(`Cannot read directory: ${dirPath}`);
73
- return;
74
- }
75
-
76
- for (const entry of entries) {
77
- const fullPath = join(dirPath, entry.name);
78
- const relativePath = relative(this.rootPath, fullPath);
79
-
80
- // Check if ignored
81
- if (this.ig.ignores(relativePath)) {
82
- logger.debug(`Ignored: ${relativePath}`);
83
- continue;
84
- }
85
-
86
- if (entry.isDirectory()) {
87
- await this.scanDirectory(fullPath, files);
88
- } else if (entry.isFile()) {
89
- await this.processFile(fullPath, relativePath, files);
90
- }
91
- }
92
- }
93
-
94
- private async processFile(
95
- fullPath: string,
96
- relativePath: string,
97
- files: FileMetadata[]
98
- ): Promise<void> {
99
- try {
100
- const stats = await stat(fullPath);
101
-
102
- // Check file size
103
- if (stats.size > this.maxFileSize) {
104
- this.skippedFiles.push(`${relativePath} (too large: ${this.formatBytes(stats.size)})`);
105
- return;
106
- }
107
-
108
- // Check if binary
109
- const ext = extname(fullPath).toLowerCase();
110
- if (BINARY_EXTENSIONS.includes(ext)) {
111
- this.skippedFiles.push(`${relativePath} (binary file)`);
112
- return;
113
- }
114
-
115
- // Check if file is actually text (simple heuristic)
116
- if (!(await this.isTextFile(fullPath))) {
117
- this.skippedFiles.push(`${relativePath} (binary content)`);
118
- return;
119
- }
120
-
121
- files.push({
122
- path: fullPath,
123
- relativePath,
124
- size: stats.size,
125
- lastModified: stats.mtime,
126
- extension: ext,
127
- });
128
- } catch (error) {
129
- this.warnings.push(`Cannot process file: ${relativePath}`);
130
- }
131
- }
132
-
133
- private async isTextFile(filePath: string): Promise<boolean> {
134
- try {
135
- const buffer = await readFile(filePath);
136
- const sample = buffer.subarray(0, Math.min(8000, buffer.length));
137
-
138
- // Check for null bytes (common in binary files)
139
- for (let i = 0; i < sample.length; i++) {
140
- if (sample[i] === 0) {
141
- return false;
142
- }
143
- }
144
-
145
- return true;
146
- } catch {
147
- return false;
148
- }
149
- }
150
-
151
- async loadFileContents(files: FileMetadata[]): Promise<FileContent[]> {
152
- const contents: FileContent[] = [];
153
-
154
- for (const file of files) {
155
- try {
156
- const content = await readFile(file.path, 'utf-8');
157
- contents.push({
158
- ...file,
159
- content,
160
- });
161
- } catch (error) {
162
- logger.warn(`Failed to read file: ${file.relativePath}`);
163
- }
164
- }
165
-
166
- return contents;
167
- }
168
-
169
- private formatBytes(bytes: number): string {
170
- if (bytes < 1024) return `${bytes} B`;
171
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
172
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
173
- }
174
- }