@amirdaraee/namewise 0.3.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 (105) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +82 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +61 -0
  3. package/.github/workflows/auto-release.yml +78 -0
  4. package/.github/workflows/ci.yml +78 -0
  5. package/.github/workflows/publish.yml +43 -0
  6. package/.github/workflows/test.yml +37 -0
  7. package/CHANGELOG.md +128 -0
  8. package/LICENSE +21 -0
  9. package/README.md +251 -0
  10. package/dist/cli/commands.d.ts +3 -0
  11. package/dist/cli/commands.d.ts.map +1 -0
  12. package/dist/cli/commands.js +19 -0
  13. package/dist/cli/commands.js.map +1 -0
  14. package/dist/cli/rename.d.ts +2 -0
  15. package/dist/cli/rename.d.ts.map +1 -0
  16. package/dist/cli/rename.js +136 -0
  17. package/dist/cli/rename.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +13 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/parsers/excel-parser.d.ts +6 -0
  23. package/dist/parsers/excel-parser.d.ts.map +1 -0
  24. package/dist/parsers/excel-parser.js +42 -0
  25. package/dist/parsers/excel-parser.js.map +1 -0
  26. package/dist/parsers/factory.d.ts +7 -0
  27. package/dist/parsers/factory.d.ts.map +1 -0
  28. package/dist/parsers/factory.js +29 -0
  29. package/dist/parsers/factory.js.map +1 -0
  30. package/dist/parsers/pdf-parser.d.ts +7 -0
  31. package/dist/parsers/pdf-parser.d.ts.map +1 -0
  32. package/dist/parsers/pdf-parser.js +67 -0
  33. package/dist/parsers/pdf-parser.js.map +1 -0
  34. package/dist/parsers/text-parser.d.ts +6 -0
  35. package/dist/parsers/text-parser.d.ts.map +1 -0
  36. package/dist/parsers/text-parser.js +39 -0
  37. package/dist/parsers/text-parser.js.map +1 -0
  38. package/dist/parsers/word-parser.d.ts +6 -0
  39. package/dist/parsers/word-parser.d.ts.map +1 -0
  40. package/dist/parsers/word-parser.js +44 -0
  41. package/dist/parsers/word-parser.js.map +1 -0
  42. package/dist/services/ai-factory.d.ts +5 -0
  43. package/dist/services/ai-factory.d.ts.map +1 -0
  44. package/dist/services/ai-factory.js +15 -0
  45. package/dist/services/ai-factory.js.map +1 -0
  46. package/dist/services/claude-service.d.ts +9 -0
  47. package/dist/services/claude-service.d.ts.map +1 -0
  48. package/dist/services/claude-service.js +113 -0
  49. package/dist/services/claude-service.js.map +1 -0
  50. package/dist/services/file-renamer.d.ts +12 -0
  51. package/dist/services/file-renamer.d.ts.map +1 -0
  52. package/dist/services/file-renamer.js +99 -0
  53. package/dist/services/file-renamer.js.map +1 -0
  54. package/dist/services/openai-service.d.ts +9 -0
  55. package/dist/services/openai-service.d.ts.map +1 -0
  56. package/dist/services/openai-service.js +112 -0
  57. package/dist/services/openai-service.js.map +1 -0
  58. package/dist/types/index.d.ts +61 -0
  59. package/dist/types/index.d.ts.map +1 -0
  60. package/dist/types/index.js +2 -0
  61. package/dist/types/index.js.map +1 -0
  62. package/dist/utils/file-templates.d.ts +18 -0
  63. package/dist/utils/file-templates.d.ts.map +1 -0
  64. package/dist/utils/file-templates.js +232 -0
  65. package/dist/utils/file-templates.js.map +1 -0
  66. package/dist/utils/naming-conventions.d.ts +4 -0
  67. package/dist/utils/naming-conventions.d.ts.map +1 -0
  68. package/dist/utils/naming-conventions.js +55 -0
  69. package/dist/utils/naming-conventions.js.map +1 -0
  70. package/package.json +75 -0
  71. package/src/cli/commands.ts +20 -0
  72. package/src/cli/rename.ts +157 -0
  73. package/src/index.ts +17 -0
  74. package/src/parsers/excel-parser.ts +49 -0
  75. package/src/parsers/factory.ts +34 -0
  76. package/src/parsers/pdf-parser.ts +78 -0
  77. package/src/parsers/text-parser.ts +43 -0
  78. package/src/parsers/word-parser.ts +50 -0
  79. package/src/services/ai-factory.ts +16 -0
  80. package/src/services/claude-service.ts +114 -0
  81. package/src/services/file-renamer.ts +123 -0
  82. package/src/services/openai-service.ts +113 -0
  83. package/src/types/index.ts +71 -0
  84. package/src/types/pdf-extraction.d.ts +7 -0
  85. package/src/utils/file-templates.ts +275 -0
  86. package/src/utils/naming-conventions.ts +67 -0
  87. package/tests/data/empty-file.txt +0 -0
  88. package/tests/data/sample-markdown.md +9 -0
  89. package/tests/data/sample-pdf.pdf +0 -0
  90. package/tests/data/sample-text.txt +25 -0
  91. package/tests/integration/end-to-end.test.ts +209 -0
  92. package/tests/integration/workflow.test.ts +336 -0
  93. package/tests/mocks/mock-ai-service.ts +58 -0
  94. package/tests/unit/cli/commands.test.ts +163 -0
  95. package/tests/unit/parsers/factory.test.ts +100 -0
  96. package/tests/unit/parsers/pdf-parser.test.ts +63 -0
  97. package/tests/unit/parsers/text-parser.test.ts +85 -0
  98. package/tests/unit/services/ai-factory.test.ts +37 -0
  99. package/tests/unit/services/claude-service.test.ts +188 -0
  100. package/tests/unit/services/file-renamer.test.ts +299 -0
  101. package/tests/unit/services/openai-service.test.ts +196 -0
  102. package/tests/unit/utils/file-templates.test.ts +199 -0
  103. package/tests/unit/utils/naming-conventions.test.ts +88 -0
  104. package/tsconfig.json +20 -0
  105. package/vitest.config.ts +30 -0
@@ -0,0 +1,34 @@
1
+ import { DocumentParser } from '../types/index.js';
2
+ import { PDFParser } from './pdf-parser.js';
3
+ import { WordParser } from './word-parser.js';
4
+ import { ExcelParser } from './excel-parser.js';
5
+ import { TextParser } from './text-parser.js';
6
+
7
+ export class DocumentParserFactory {
8
+ private parsers: DocumentParser[] = [
9
+ new PDFParser(),
10
+ new WordParser(),
11
+ new ExcelParser(),
12
+ new TextParser()
13
+ ];
14
+
15
+ getParser(filePath: string): DocumentParser | null {
16
+ return this.parsers.find(parser => parser.supports(filePath)) || null;
17
+ }
18
+
19
+ getSupportedExtensions(): string[] {
20
+ const extensions = new Set<string>();
21
+
22
+ // Add known extensions based on parser implementations
23
+ extensions.add('.pdf');
24
+ extensions.add('.docx');
25
+ extensions.add('.doc');
26
+ extensions.add('.xlsx');
27
+ extensions.add('.xls');
28
+ extensions.add('.txt');
29
+ extensions.add('.md');
30
+ extensions.add('.rtf');
31
+
32
+ return Array.from(extensions);
33
+ }
34
+ }
@@ -0,0 +1,78 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { DocumentParser, ParseResult, DocumentMetadata } from '../types/index.js';
4
+
5
+ export class PDFParser implements DocumentParser {
6
+ supports(filePath: string): boolean {
7
+ return path.extname(filePath).toLowerCase() === '.pdf';
8
+ }
9
+
10
+ async parse(filePath: string): Promise<ParseResult> {
11
+ try {
12
+ // Dynamic import for pdf-extraction (default export)
13
+ const pdfExtraction = await import('pdf-extraction');
14
+ const extract = pdfExtraction.default;
15
+
16
+ const dataBuffer = fs.readFileSync(filePath);
17
+ const data = await extract(dataBuffer, {});
18
+
19
+ const content = data.text?.trim() || '';
20
+
21
+ // Extract PDF metadata if available
22
+ const metadata: DocumentMetadata = {};
23
+
24
+ // Cast data to any to access potentially existing metadata properties
25
+ const pdfData = data as any;
26
+
27
+ if (pdfData.meta) {
28
+ if (pdfData.meta.info) {
29
+ metadata.title = pdfData.meta.info.Title;
30
+ metadata.author = pdfData.meta.info.Author;
31
+ metadata.creator = pdfData.meta.info.Creator;
32
+ metadata.subject = pdfData.meta.info.Subject;
33
+
34
+ // Parse dates if available
35
+ if (pdfData.meta.info.CreationDate) {
36
+ metadata.creationDate = this.parseDate(pdfData.meta.info.CreationDate);
37
+ }
38
+ if (pdfData.meta.info.ModDate) {
39
+ metadata.modificationDate = this.parseDate(pdfData.meta.info.ModDate);
40
+ }
41
+ }
42
+
43
+ if (pdfData.numpages) {
44
+ metadata.pages = pdfData.numpages;
45
+ }
46
+ }
47
+
48
+ // Estimate word count from text content
49
+ if (content) {
50
+ metadata.wordCount = content.split(/\s+/).filter(word => word.length > 0).length;
51
+ }
52
+
53
+ return { content, metadata };
54
+ } catch (error) {
55
+ throw new Error(`Failed to parse PDF file: ${error instanceof Error ? error.message : 'Unknown error'}`);
56
+ }
57
+ }
58
+
59
+ private parseDate(dateStr: string): Date | undefined {
60
+ try {
61
+ // PDF dates are in format: D:YYYYMMDDHHmmSSOHH'mm or D:YYYYMMDDHHMMSS
62
+ if (dateStr.startsWith('D:')) {
63
+ const datepart = dateStr.slice(2, 16); // YYYYMMDDHHMMSS
64
+ const year = parseInt(datepart.slice(0, 4));
65
+ const month = parseInt(datepart.slice(4, 6)) - 1; // Month is 0-based
66
+ const day = parseInt(datepart.slice(6, 8));
67
+ const hour = parseInt(datepart.slice(8, 10) || '0');
68
+ const minute = parseInt(datepart.slice(10, 12) || '0');
69
+ const second = parseInt(datepart.slice(12, 14) || '0');
70
+
71
+ return new Date(year, month, day, hour, minute, second);
72
+ }
73
+ return new Date(dateStr);
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { DocumentParser, ParseResult, DocumentMetadata } from '../types/index.js';
4
+
5
+ export class TextParser implements DocumentParser {
6
+ supports(filePath: string): boolean {
7
+ const ext = path.extname(filePath).toLowerCase();
8
+ return ext === '.txt' || ext === '.md' || ext === '.rtf';
9
+ }
10
+
11
+ async parse(filePath: string): Promise<ParseResult> {
12
+ try {
13
+ const content = fs.readFileSync(filePath, 'utf-8').trim();
14
+ const metadata: DocumentMetadata = {};
15
+
16
+ // Extract basic metadata from content
17
+ const lines = content.split('\n').filter(line => line.trim());
18
+
19
+ if (lines.length > 0) {
20
+ // For markdown files, look for title in first heading
21
+ if (path.extname(filePath).toLowerCase() === '.md') {
22
+ const firstLine = lines[0];
23
+ if (firstLine.startsWith('# ')) {
24
+ metadata.title = firstLine.substring(2).trim();
25
+ }
26
+ } else {
27
+ // For other text files, use first non-empty line as potential title
28
+ const firstNonEmptyLine = lines[0];
29
+ if (firstNonEmptyLine.length < 100 && !firstNonEmptyLine.endsWith('.')) {
30
+ metadata.title = firstNonEmptyLine.trim();
31
+ }
32
+ }
33
+
34
+ // Word count
35
+ metadata.wordCount = content.split(/\s+/).filter(word => word.length > 0).length;
36
+ }
37
+
38
+ return { content, metadata };
39
+ } catch (error) {
40
+ throw new Error(`Failed to parse text file: ${error instanceof Error ? error.message : 'Unknown error'}`);
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,50 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import mammoth from 'mammoth';
4
+ import { DocumentParser, ParseResult, DocumentMetadata } from '../types/index.js';
5
+
6
+ export class WordParser implements DocumentParser {
7
+ supports(filePath: string): boolean {
8
+ const ext = path.extname(filePath).toLowerCase();
9
+ return ext === '.docx' || ext === '.doc';
10
+ }
11
+
12
+ async parse(filePath: string): Promise<ParseResult> {
13
+ try {
14
+ const buffer = fs.readFileSync(filePath);
15
+
16
+ // Extract text content
17
+ const textResult = await mammoth.extractRawText({ buffer });
18
+ const content = textResult.value.trim();
19
+
20
+ // Extract metadata
21
+ const metadata: DocumentMetadata = {};
22
+
23
+ // Estimate word count
24
+ if (content) {
25
+ metadata.wordCount = content.split(/\s+/).filter(word => word.length > 0).length;
26
+ }
27
+
28
+ // Try to extract document properties for .docx files
29
+ if (path.extname(filePath).toLowerCase() === '.docx') {
30
+ try {
31
+ // For DOCX files, we could parse document.xml for metadata
32
+ // For now, we'll use basic analysis of the content
33
+ const lines = content.split('\n');
34
+ const firstNonEmptyLine = lines.find(line => line.trim().length > 0);
35
+
36
+ // If the first line looks like a title (short and not a sentence)
37
+ if (firstNonEmptyLine && firstNonEmptyLine.length < 100 && !firstNonEmptyLine.endsWith('.')) {
38
+ metadata.title = firstNonEmptyLine.trim();
39
+ }
40
+ } catch {
41
+ // Ignore metadata extraction errors
42
+ }
43
+ }
44
+
45
+ return { content, metadata };
46
+ } catch (error) {
47
+ throw new Error(`Failed to parse Word document: ${error instanceof Error ? error.message : 'Unknown error'}`);
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,16 @@
1
+ import { AIProvider } from '../types/index.js';
2
+ import { ClaudeService } from './claude-service.js';
3
+ import { OpenAIService } from './openai-service.js';
4
+
5
+ export class AIServiceFactory {
6
+ static create(provider: 'claude' | 'openai', apiKey: string): AIProvider {
7
+ switch (provider) {
8
+ case 'claude':
9
+ return new ClaudeService(apiKey);
10
+ case 'openai':
11
+ return new OpenAIService(apiKey);
12
+ default:
13
+ throw new Error(`Unsupported AI provider: ${provider}`);
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,114 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { AIProvider, FileInfo } from '../types/index.js';
3
+ import { applyNamingConvention, getNamingInstructions, NamingConvention } from '../utils/naming-conventions.js';
4
+ import { getTemplateInstructions, FileCategory } from '../utils/file-templates.js';
5
+
6
+ export class ClaudeService implements AIProvider {
7
+ name = 'Claude';
8
+ private client: Anthropic;
9
+
10
+ constructor(apiKey: string) {
11
+ this.client = new Anthropic({
12
+ apiKey: apiKey
13
+ });
14
+ }
15
+
16
+ async generateFileName(content: string, originalName: string, namingConvention: string = 'kebab-case', category: string = 'general', fileInfo?: FileInfo): Promise<string> {
17
+ const convention = namingConvention as NamingConvention;
18
+ const fileCategory = category as FileCategory;
19
+ const namingInstructions = getNamingInstructions(convention);
20
+ const templateInstructions = getTemplateInstructions(fileCategory);
21
+
22
+ // Build comprehensive context from all metadata
23
+ let metadataContext = '';
24
+ if (fileInfo) {
25
+ metadataContext += `File Information:
26
+ - Original filename: ${originalName}
27
+ - File size: ${Math.round(fileInfo.size / 1024)}KB
28
+ - Created: ${fileInfo.createdAt.toLocaleDateString()}
29
+ - Modified: ${fileInfo.modifiedAt.toLocaleDateString()}
30
+ - Parent folder: ${fileInfo.parentFolder}
31
+ - Folder path: ${fileInfo.folderPath.join(' > ')}`;
32
+
33
+ if (fileInfo.documentMetadata) {
34
+ const meta = fileInfo.documentMetadata;
35
+ metadataContext += `
36
+ Document Properties:`;
37
+ if (meta.title) metadataContext += `\n- Title: ${meta.title}`;
38
+ if (meta.author) metadataContext += `\n- Author: ${meta.author}`;
39
+ if (meta.creator) metadataContext += `\n- Creator: ${meta.creator}`;
40
+ if (meta.subject) metadataContext += `\n- Subject: ${meta.subject}`;
41
+ if (meta.keywords?.length) metadataContext += `\n- Keywords: ${meta.keywords.join(', ')}`;
42
+ if (meta.creationDate) metadataContext += `\n- Created: ${meta.creationDate.toLocaleDateString()}`;
43
+ if (meta.modificationDate) metadataContext += `\n- Modified: ${meta.modificationDate.toLocaleDateString()}`;
44
+ if (meta.pages) metadataContext += `\n- Pages: ${meta.pages}`;
45
+ if (meta.wordCount) metadataContext += `\n- Word count: ${meta.wordCount}`;
46
+ }
47
+ }
48
+
49
+ const prompt = `Based on the following document information, generate a descriptive filename that captures the main topic/purpose of the document. The filename should be:
50
+ - Descriptive and meaningful
51
+ - Professional and clean
52
+ - Between 3-8 words
53
+ - ${namingInstructions}
54
+ - ${templateInstructions}
55
+ - Do not include file extension
56
+ - Do not include personal names, dates, or template variables - just the core content description
57
+ - Only use letters, numbers, and appropriate separators for the naming convention
58
+ - Use all available context (metadata, folder context, document properties) to create the most accurate filename
59
+
60
+ ${metadataContext}
61
+
62
+ Document content (first 2000 characters):
63
+ ${content.substring(0, 2000)}
64
+
65
+ Respond with only the core filename (without personal info or dates) using the specified naming convention, no explanation.`;
66
+
67
+ try {
68
+ const response = await this.client.messages.create({
69
+ model: 'claude-3-haiku-20240307',
70
+ max_tokens: 100,
71
+ messages: [
72
+ {
73
+ role: 'user',
74
+ content: prompt
75
+ }
76
+ ]
77
+ });
78
+
79
+ const suggestedName = response.content[0].type === 'text'
80
+ ? response.content[0].text.trim()
81
+ : 'untitled-document';
82
+
83
+ // Apply naming convention and clean the suggested name
84
+ return this.sanitizeFileName(suggestedName, convention);
85
+ } catch (error) {
86
+ console.error('Claude API error:', error);
87
+ throw new Error(`Failed to generate filename with Claude: ${error instanceof Error ? error.message : 'Unknown error'}`);
88
+ }
89
+ }
90
+
91
+ private sanitizeFileName(name: string, convention: NamingConvention): string {
92
+ // Remove any potential file extensions from the suggestion
93
+ const nameWithoutExt = name.replace(/\.[^/.]+$/, '');
94
+
95
+ // Apply the naming convention
96
+ let cleaned = applyNamingConvention(nameWithoutExt, convention);
97
+
98
+ // Ensure it's not empty and not too long
99
+ if (!cleaned) {
100
+ cleaned = applyNamingConvention('untitled document', convention);
101
+ } else if (cleaned.length > 100) {
102
+ // Truncate while preserving naming convention structure
103
+ cleaned = cleaned.substring(0, 100);
104
+ // Clean up any broken separators at the end
105
+ if (convention === 'kebab-case') {
106
+ cleaned = cleaned.replace(/-[^-]*$/, '');
107
+ } else if (convention === 'snake_case') {
108
+ cleaned = cleaned.replace(/_[^_]*$/, '');
109
+ }
110
+ }
111
+
112
+ return cleaned;
113
+ }
114
+ }
@@ -0,0 +1,123 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { FileInfo, Config, RenameResult, AIProvider } from '../types/index.js';
4
+ import { DocumentParserFactory } from '../parsers/factory.js';
5
+ import { categorizeFile, applyTemplate } from '../utils/file-templates.js';
6
+
7
+ export class FileRenamer {
8
+ constructor(
9
+ private parserFactory: DocumentParserFactory,
10
+ private aiService: AIProvider,
11
+ private config: Config
12
+ ) {}
13
+
14
+ async renameFiles(files: FileInfo[]): Promise<RenameResult[]> {
15
+ const results: RenameResult[] = [];
16
+
17
+ for (let i = 0; i < files.length; i++) {
18
+ const file = files[i];
19
+ // Use \r to overwrite the same line, show progress counter
20
+ process.stdout.write(`\rProcessing: ${file.name}... (${i + 1}/${files.length})`);
21
+
22
+ try {
23
+ const result = await this.renameFile(file);
24
+ results.push(result);
25
+ } catch (error) {
26
+ results.push({
27
+ originalPath: file.path,
28
+ newPath: file.path,
29
+ suggestedName: file.name,
30
+ success: false,
31
+ error: error instanceof Error ? error.message : 'Unknown error'
32
+ });
33
+ }
34
+ }
35
+
36
+ // Clear the processing line and move to next line
37
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
38
+
39
+ return results;
40
+ }
41
+
42
+ private async renameFile(file: FileInfo): Promise<RenameResult> {
43
+ // Check file size
44
+ if (file.size > this.config.maxFileSize) {
45
+ throw new Error(`File size (${Math.round(file.size / 1024 / 1024)}MB) exceeds maximum allowed size (${Math.round(this.config.maxFileSize / 1024 / 1024)}MB)`);
46
+ }
47
+
48
+ // Get appropriate parser
49
+ const parser = this.parserFactory.getParser(file.path);
50
+ if (!parser) {
51
+ throw new Error(`No parser available for file type: ${file.extension}`);
52
+ }
53
+
54
+ // Extract content and metadata
55
+ const parseResult = await parser.parse(file.path);
56
+ const content = parseResult.content;
57
+ if (!content || content.trim().length === 0) {
58
+ throw new Error('No content could be extracted from the file');
59
+ }
60
+
61
+ // Update file info with extracted document metadata
62
+ file.documentMetadata = parseResult.metadata;
63
+
64
+ // Determine file category (use configured category or auto-categorize)
65
+ const fileCategory = this.config.templateOptions.category === 'auto'
66
+ ? categorizeFile(file.path, content, file)
67
+ : this.config.templateOptions.category;
68
+
69
+ // Generate core filename using AI with all available metadata
70
+ const coreFileName = await this.aiService.generateFileName(
71
+ content,
72
+ file.name,
73
+ this.config.namingConvention,
74
+ fileCategory,
75
+ file // Pass the entire file info with all metadata
76
+ );
77
+ if (!coreFileName || coreFileName.trim().length === 0) {
78
+ throw new Error('AI service failed to generate a filename');
79
+ }
80
+
81
+ // Apply template to include personal info, dates, etc.
82
+ const templatedName = applyTemplate(
83
+ coreFileName,
84
+ fileCategory,
85
+ this.config.templateOptions,
86
+ this.config.namingConvention
87
+ );
88
+
89
+ // Create new filename with original extension
90
+ const newFileName = `${templatedName}${file.extension}`;
91
+ const newPath = path.join(path.dirname(file.path), newFileName);
92
+
93
+ // Check if new filename would conflict with existing file
94
+ if (newPath !== file.path) {
95
+ await this.checkForConflicts(newPath);
96
+ }
97
+
98
+ // Perform the rename (or simulate if dry run)
99
+ if (!this.config.dryRun && newPath !== file.path) {
100
+ await fs.rename(file.path, newPath);
101
+ }
102
+
103
+ return {
104
+ originalPath: file.path,
105
+ newPath,
106
+ suggestedName: newFileName,
107
+ success: true
108
+ };
109
+ }
110
+
111
+ private async checkForConflicts(newPath: string): Promise<void> {
112
+ try {
113
+ await fs.access(newPath);
114
+ // If we reach here, the file exists
115
+ throw new Error(`Target filename already exists: ${path.basename(newPath)}`);
116
+ } catch (error) {
117
+ // If the error is ENOENT (file doesn't exist), that's what we want
118
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
119
+ throw error;
120
+ }
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,113 @@
1
+ import OpenAI from 'openai';
2
+ import { AIProvider, FileInfo } from '../types/index.js';
3
+ import { applyNamingConvention, getNamingInstructions, NamingConvention } from '../utils/naming-conventions.js';
4
+ import { getTemplateInstructions, FileCategory } from '../utils/file-templates.js';
5
+
6
+ export class OpenAIService implements AIProvider {
7
+ name = 'OpenAI';
8
+ private client: OpenAI;
9
+
10
+ constructor(apiKey: string) {
11
+ this.client = new OpenAI({
12
+ apiKey: apiKey
13
+ });
14
+ }
15
+
16
+ async generateFileName(content: string, originalName: string, namingConvention: string = 'kebab-case', category: string = 'general', fileInfo?: FileInfo): Promise<string> {
17
+ const convention = namingConvention as NamingConvention;
18
+ const fileCategory = category as FileCategory;
19
+ const namingInstructions = getNamingInstructions(convention);
20
+ const templateInstructions = getTemplateInstructions(fileCategory);
21
+
22
+ // Build comprehensive context from all metadata
23
+ let metadataContext = '';
24
+ if (fileInfo) {
25
+ metadataContext += `File Information:
26
+ - Original filename: ${originalName}
27
+ - File size: ${Math.round(fileInfo.size / 1024)}KB
28
+ - Created: ${fileInfo.createdAt.toLocaleDateString()}
29
+ - Modified: ${fileInfo.modifiedAt.toLocaleDateString()}
30
+ - Parent folder: ${fileInfo.parentFolder}
31
+ - Folder path: ${fileInfo.folderPath.join(' > ')}`;
32
+
33
+ if (fileInfo.documentMetadata) {
34
+ const meta = fileInfo.documentMetadata;
35
+ metadataContext += `
36
+ Document Properties:`;
37
+ if (meta.title) metadataContext += `\n- Title: ${meta.title}`;
38
+ if (meta.author) metadataContext += `\n- Author: ${meta.author}`;
39
+ if (meta.creator) metadataContext += `\n- Creator: ${meta.creator}`;
40
+ if (meta.subject) metadataContext += `\n- Subject: ${meta.subject}`;
41
+ if (meta.keywords?.length) metadataContext += `\n- Keywords: ${meta.keywords.join(', ')}`;
42
+ if (meta.creationDate) metadataContext += `\n- Created: ${meta.creationDate.toLocaleDateString()}`;
43
+ if (meta.modificationDate) metadataContext += `\n- Modified: ${meta.modificationDate.toLocaleDateString()}`;
44
+ if (meta.pages) metadataContext += `\n- Pages: ${meta.pages}`;
45
+ if (meta.wordCount) metadataContext += `\n- Word count: ${meta.wordCount}`;
46
+ }
47
+ }
48
+
49
+ const prompt = `Based on the following document information, generate a descriptive filename that captures the main topic/purpose of the document. The filename should be:
50
+ - Descriptive and meaningful
51
+ - Professional and clean
52
+ - Between 3-8 words
53
+ - ${namingInstructions}
54
+ - ${templateInstructions}
55
+ - Do not include file extension
56
+ - Do not include personal names, dates, or template variables - just the core content description
57
+ - Only use letters, numbers, and appropriate separators for the naming convention
58
+ - Use all available context (metadata, folder context, document properties) to create the most accurate filename
59
+
60
+ ${metadataContext}
61
+
62
+ Document content (first 2000 characters):
63
+ ${content.substring(0, 2000)}
64
+
65
+ Respond with only the core filename (without personal info or dates) using the specified naming convention, no explanation.`;
66
+
67
+ try {
68
+ const response = await this.client.chat.completions.create({
69
+ model: 'gpt-3.5-turbo',
70
+ messages: [
71
+ {
72
+ role: 'user',
73
+ content: prompt
74
+ }
75
+ ],
76
+ max_tokens: 100,
77
+ temperature: 0.3
78
+ });
79
+
80
+ const suggestedName = response.choices[0]?.message?.content?.trim() || 'untitled-document';
81
+
82
+ // Clean and validate the suggested name
83
+ return this.sanitizeFileName(suggestedName, convention);
84
+ } catch (error) {
85
+ console.error('OpenAI API error:', error);
86
+ throw new Error(`Failed to generate filename with OpenAI: ${error instanceof Error ? error.message : 'Unknown error'}`);
87
+ }
88
+ }
89
+
90
+ private sanitizeFileName(name: string, convention: NamingConvention): string {
91
+ // Remove any potential file extensions from the suggestion
92
+ const nameWithoutExt = name.replace(/\.[^/.]+$/, '');
93
+
94
+ // Apply the naming convention
95
+ let cleaned = applyNamingConvention(nameWithoutExt, convention);
96
+
97
+ // Ensure it's not empty and not too long
98
+ if (!cleaned) {
99
+ cleaned = applyNamingConvention('untitled document', convention);
100
+ } else if (cleaned.length > 100) {
101
+ // Truncate while preserving naming convention structure
102
+ cleaned = cleaned.substring(0, 100);
103
+ // Clean up any broken separators at the end
104
+ if (convention === 'kebab-case') {
105
+ cleaned = cleaned.replace(/-[^-]*$/, '');
106
+ } else if (convention === 'snake_case') {
107
+ cleaned = cleaned.replace(/_[^_]*$/, '');
108
+ }
109
+ }
110
+
111
+ return cleaned;
112
+ }
113
+ }
@@ -0,0 +1,71 @@
1
+ export interface DocumentMetadata {
2
+ title?: string;
3
+ author?: string;
4
+ creator?: string;
5
+ subject?: string;
6
+ keywords?: string[];
7
+ creationDate?: Date;
8
+ modificationDate?: Date;
9
+ pages?: number;
10
+ wordCount?: number;
11
+ }
12
+
13
+ export interface FileInfo {
14
+ path: string;
15
+ name: string;
16
+ extension: string;
17
+ size: number;
18
+ content?: string;
19
+ // File system metadata
20
+ createdAt: Date;
21
+ modifiedAt: Date;
22
+ accessedAt: Date;
23
+ // Context metadata
24
+ parentFolder: string;
25
+ folderPath: string[];
26
+ // Document metadata (extracted from file contents)
27
+ documentMetadata?: DocumentMetadata;
28
+ }
29
+
30
+ export interface RenameResult {
31
+ originalPath: string;
32
+ newPath: string;
33
+ suggestedName: string;
34
+ success: boolean;
35
+ error?: string;
36
+ }
37
+
38
+ export interface AIProvider {
39
+ name: string;
40
+ generateFileName: (content: string, originalName: string, namingConvention?: string, category?: string, fileInfo?: FileInfo) => Promise<string>;
41
+ }
42
+
43
+ export type NamingConvention = 'kebab-case' | 'snake_case' | 'camelCase' | 'PascalCase' | 'lowercase' | 'UPPERCASE';
44
+ export type FileCategory = 'document' | 'movie' | 'music' | 'series' | 'photo' | 'book' | 'general' | 'auto';
45
+ export type DateFormat = 'YYYY-MM-DD' | 'YYYY' | 'YYYYMMDD' | 'none';
46
+
47
+ export interface TemplateOptions {
48
+ category: FileCategory;
49
+ personalName?: string;
50
+ dateFormat: DateFormat;
51
+ }
52
+
53
+ export interface Config {
54
+ aiProvider: 'claude' | 'openai';
55
+ apiKey: string;
56
+ maxFileSize: number;
57
+ supportedExtensions: string[];
58
+ dryRun: boolean;
59
+ namingConvention: NamingConvention;
60
+ templateOptions: TemplateOptions;
61
+ }
62
+
63
+ export interface ParseResult {
64
+ content: string;
65
+ metadata?: DocumentMetadata;
66
+ }
67
+
68
+ export interface DocumentParser {
69
+ supports: (filePath: string) => boolean;
70
+ parse: (filePath: string) => Promise<ParseResult>;
71
+ }
@@ -0,0 +1,7 @@
1
+ declare module 'pdf-extraction' {
2
+ function extract(buffer: Buffer, options?: any): Promise<{
3
+ text?: string;
4
+ pages?: any[];
5
+ }>;
6
+ export = extract;
7
+ }