@customize-agent/knowledge 1.0.1
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.
- package/dist/chunking/text-chunker.d.ts +24 -0
- package/dist/chunking/text-chunker.js +90 -0
- package/dist/classification/classifier.d.ts +12 -0
- package/dist/classification/classifier.js +95 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +40 -0
- package/dist/core/change-tracker.d.ts +10 -0
- package/dist/core/change-tracker.js +66 -0
- package/dist/core/file-scanner.d.ts +8 -0
- package/dist/core/file-scanner.js +32 -0
- package/dist/core/index-state-store.d.ts +103 -0
- package/dist/core/index-state-store.js +439 -0
- package/dist/core/knowledge-base-manager.d.ts +76 -0
- package/dist/core/knowledge-base-manager.js +300 -0
- package/dist/core/multi-project-manager.d.ts +30 -0
- package/dist/core/multi-project-manager.js +163 -0
- package/dist/core/project-config.d.ts +9 -0
- package/dist/core/project-config.js +73 -0
- package/dist/core/project-id.d.ts +1 -0
- package/dist/core/project-id.js +8 -0
- package/dist/core/project-registry.d.ts +10 -0
- package/dist/core/project-registry.js +70 -0
- package/dist/dedup/dedup-engine.d.ts +20 -0
- package/dist/dedup/dedup-engine.js +78 -0
- package/dist/dedup/relationship-detector.d.ts +10 -0
- package/dist/dedup/relationship-detector.js +84 -0
- package/dist/embedding/embedding-provider.d.ts +15 -0
- package/dist/embedding/embedding-provider.js +31 -0
- package/dist/extraction/content-extractor.d.ts +36 -0
- package/dist/extraction/content-extractor.js +655 -0
- package/dist/extraction/external-extractor.d.ts +63 -0
- package/dist/extraction/external-extractor.js +139 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +22 -0
- package/dist/search/federation-search.d.ts +41 -0
- package/dist/search/federation-search.js +102 -0
- package/dist/server/dashboard-client.d.ts +2 -0
- package/dist/server/dashboard-client.js +396 -0
- package/dist/server/dashboard-i18n.d.ts +112 -0
- package/dist/server/dashboard-i18n.js +220 -0
- package/dist/server/dashboard-page.d.ts +6 -0
- package/dist/server/dashboard-page.js +138 -0
- package/dist/server/dashboard-server.d.ts +13 -0
- package/dist/server/dashboard-server.js +225 -0
- package/dist/server/dashboard-styles.d.ts +1 -0
- package/dist/server/dashboard-styles.js +152 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +1 -0
- package/dist/vector/chroma-store.d.ts +39 -0
- package/dist/vector/chroma-store.js +131 -0
- package/dist/vector/collection-manager.d.ts +16 -0
- package/dist/vector/collection-manager.js +77 -0
- package/dist/vector/types.d.ts +33 -0
- package/dist/vector/types.js +1 -0
- package/dist/vector/vector-indexer.d.ts +18 -0
- package/dist/vector/vector-indexer.js +61 -0
- package/package.json +45 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ClassifiedFile } from '../types.js';
|
|
2
|
+
export interface TextChunk {
|
|
3
|
+
index: number;
|
|
4
|
+
text: string;
|
|
5
|
+
startChar: number;
|
|
6
|
+
endChar: number;
|
|
7
|
+
tokenCount: number;
|
|
8
|
+
sectionTitle?: string;
|
|
9
|
+
metadata: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface ChunkConfig {
|
|
12
|
+
maxChunkSize: number;
|
|
13
|
+
overlap: number;
|
|
14
|
+
headerInjection: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class TextChunker {
|
|
17
|
+
chunk(text: string, file: ClassifiedFile, metadata?: Record<string, unknown>): TextChunk[];
|
|
18
|
+
private withHeader;
|
|
19
|
+
private splitBySemanticBoundary;
|
|
20
|
+
private createChunk;
|
|
21
|
+
private extractSectionTitle;
|
|
22
|
+
private takeOverlap;
|
|
23
|
+
private estimateTokens;
|
|
24
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const DEFAULT_CONFIGS = {
|
|
2
|
+
document: { maxChunkSize: 800, overlap: 100, headerInjection: true },
|
|
3
|
+
spreadsheet: { maxChunkSize: 1000, overlap: 200, headerInjection: true },
|
|
4
|
+
image: { maxChunkSize: 512, overlap: 0, headerInjection: true },
|
|
5
|
+
cad: { maxChunkSize: 600, overlap: 100, headerInjection: true },
|
|
6
|
+
code: { maxChunkSize: 1000, overlap: 200, headerInjection: true },
|
|
7
|
+
data: { maxChunkSize: 600, overlap: 100, headerInjection: true },
|
|
8
|
+
web: { maxChunkSize: 800, overlap: 100, headerInjection: true },
|
|
9
|
+
diagram: { maxChunkSize: 512, overlap: 0, headerInjection: true },
|
|
10
|
+
archive: { maxChunkSize: 500, overlap: 50, headerInjection: false },
|
|
11
|
+
other: { maxChunkSize: 500, overlap: 50, headerInjection: false },
|
|
12
|
+
};
|
|
13
|
+
export class TextChunker {
|
|
14
|
+
chunk(text, file, metadata = {}) {
|
|
15
|
+
if (text.trim().length === 0)
|
|
16
|
+
return [];
|
|
17
|
+
const config = DEFAULT_CONFIGS[file.category];
|
|
18
|
+
const normalized = this.withHeader(text, file, config);
|
|
19
|
+
const paragraphs = this.splitBySemanticBoundary(normalized, file.category);
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let buffer = '';
|
|
22
|
+
let chunkStart = 0;
|
|
23
|
+
let cursor = 0;
|
|
24
|
+
for (const paragraph of paragraphs) {
|
|
25
|
+
const candidate = buffer.length === 0 ? paragraph : `${buffer}\n\n${paragraph}`;
|
|
26
|
+
if (this.estimateTokens(candidate) > config.maxChunkSize && buffer.length > 0) {
|
|
27
|
+
chunks.push(this.createChunk(chunks.length, buffer, chunkStart, cursor, metadata));
|
|
28
|
+
const overlapText = this.takeOverlap(buffer, config.overlap);
|
|
29
|
+
buffer = overlapText.length > 0 ? `${overlapText}\n\n${paragraph}` : paragraph;
|
|
30
|
+
chunkStart = Math.max(0, cursor - overlapText.length);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
buffer = candidate;
|
|
34
|
+
}
|
|
35
|
+
cursor += paragraph.length + 2;
|
|
36
|
+
}
|
|
37
|
+
if (buffer.trim().length > 0) {
|
|
38
|
+
chunks.push(this.createChunk(chunks.length, buffer, chunkStart, normalized.length, metadata));
|
|
39
|
+
}
|
|
40
|
+
return chunks;
|
|
41
|
+
}
|
|
42
|
+
withHeader(text, file, config) {
|
|
43
|
+
if (!config.headerInjection)
|
|
44
|
+
return text;
|
|
45
|
+
return `文件: ${file.relativePath}\n类型: ${file.category}/${file.format}\n\n${text}`;
|
|
46
|
+
}
|
|
47
|
+
splitBySemanticBoundary(text, category) {
|
|
48
|
+
if (category === 'document') {
|
|
49
|
+
return text.split(/\n(?=#{1,6}\s)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
if (category === 'code') {
|
|
52
|
+
return text.split(/\n(?=(export\s+)?(async\s+)?(function|class|interface|type|const|let|var)\s)/u).map(part => part.trim()).filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
if (category === 'data') {
|
|
55
|
+
return text.split(/\n(?=[\w.[\]-]+:\s)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
if (category === 'cad' || category === 'diagram') {
|
|
58
|
+
return text.split(/\n(?=(?:CAD|STEP|IGES|Mesh|Draw\.io|Excalidraw|SVG)\b)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
return text.split(/\n{2,}/u).map(part => part.trim()).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
createChunk(index, text, startChar, endChar, metadata) {
|
|
63
|
+
return {
|
|
64
|
+
index,
|
|
65
|
+
text: text.trim(),
|
|
66
|
+
startChar,
|
|
67
|
+
endChar,
|
|
68
|
+
tokenCount: this.estimateTokens(text),
|
|
69
|
+
sectionTitle: this.extractSectionTitle(text),
|
|
70
|
+
metadata,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
extractSectionTitle(text) {
|
|
74
|
+
const firstLine = text.trim().split(/\r?\n/u)[0]?.trim();
|
|
75
|
+
if (!firstLine)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (firstLine.startsWith('#'))
|
|
78
|
+
return firstLine.replace(/^#+\s*/u, '');
|
|
79
|
+
return firstLine.length <= 80 ? firstLine : undefined;
|
|
80
|
+
}
|
|
81
|
+
takeOverlap(text, overlapTokens) {
|
|
82
|
+
if (overlapTokens <= 0)
|
|
83
|
+
return '';
|
|
84
|
+
const chars = overlapTokens * 4;
|
|
85
|
+
return text.slice(Math.max(0, text.length - chars));
|
|
86
|
+
}
|
|
87
|
+
estimateTokens(text) {
|
|
88
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Stats } from 'node:fs';
|
|
2
|
+
import type { ClassifiedFile, FileCategory } from '../types.js';
|
|
3
|
+
export declare class FileClassifier {
|
|
4
|
+
private readonly extensionMap;
|
|
5
|
+
constructor();
|
|
6
|
+
classify(absolutePath: string, relativePath: string, stat: Stats): ClassifiedFile;
|
|
7
|
+
classifyVirtual(fileName: string): Pick<ClassifiedFile, 'category' | 'format' | 'mimeType'>;
|
|
8
|
+
groupByCategory(files: ClassifiedFile[]): Map<FileCategory, ClassifiedFile[]>;
|
|
9
|
+
shouldSkip(file: ClassifiedFile): string | null;
|
|
10
|
+
private buildExtensionMap;
|
|
11
|
+
private inferMimeType;
|
|
12
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
export class FileClassifier {
|
|
3
|
+
extensionMap;
|
|
4
|
+
constructor() {
|
|
5
|
+
this.extensionMap = this.buildExtensionMap();
|
|
6
|
+
}
|
|
7
|
+
classify(absolutePath, relativePath, stat) {
|
|
8
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
9
|
+
const [category, format] = this.extensionMap.get(ext) ?? ['other', 'unknown'];
|
|
10
|
+
return {
|
|
11
|
+
absolutePath,
|
|
12
|
+
relativePath,
|
|
13
|
+
category,
|
|
14
|
+
format,
|
|
15
|
+
fileSize: stat.size,
|
|
16
|
+
mtime: stat.mtimeMs,
|
|
17
|
+
mimeType: this.inferMimeType(ext, category),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
classifyVirtual(fileName) {
|
|
21
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
22
|
+
const [category, format] = this.extensionMap.get(ext) ?? ['other', 'unknown'];
|
|
23
|
+
return { category, format, mimeType: this.inferMimeType(ext, category) };
|
|
24
|
+
}
|
|
25
|
+
groupByCategory(files) {
|
|
26
|
+
const groups = new Map();
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const list = groups.get(file.category) ?? [];
|
|
29
|
+
list.push(file);
|
|
30
|
+
groups.set(file.category, list);
|
|
31
|
+
}
|
|
32
|
+
return groups;
|
|
33
|
+
}
|
|
34
|
+
shouldSkip(file) {
|
|
35
|
+
if (file.fileSize > 50 * 1024 * 1024)
|
|
36
|
+
return '文件超过 50MB 限制';
|
|
37
|
+
if (file.fileSize === 0)
|
|
38
|
+
return '空文件';
|
|
39
|
+
const ext = path.extname(file.absolutePath).toLowerCase();
|
|
40
|
+
const skipExts = ['.exe', '.dll', '.so', '.dylib', '.bin', '.dat', '.class', '.pyc', '.o'];
|
|
41
|
+
if (skipExts.includes(ext))
|
|
42
|
+
return '二进制可执行文件,跳过';
|
|
43
|
+
const basename = path.basename(file.absolutePath);
|
|
44
|
+
if (basename.startsWith('.') || basename.startsWith('._'))
|
|
45
|
+
return '隐藏/系统文件,跳过';
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
buildExtensionMap() {
|
|
49
|
+
const entries = [
|
|
50
|
+
['.pdf', 'document', 'pdf'], ['.docx', 'document', 'office'], ['.doc', 'document', 'office'], ['.rtf', 'document', 'office'], ['.odt', 'document', 'office'],
|
|
51
|
+
['.pptx', 'document', 'presentation'], ['.ppt', 'document', 'presentation'], ['.odp', 'document', 'presentation'],
|
|
52
|
+
['.md', 'document', 'markdown'], ['.markdown', 'document', 'markdown'], ['.mdx', 'document', 'markdown'],
|
|
53
|
+
['.txt', 'document', 'plaintext'], ['.rst', 'document', 'plaintext'], ['.asciidoc', 'document', 'plaintext'], ['.tex', 'document', 'plaintext'],
|
|
54
|
+
['.epub', 'document', 'ebook'], ['.mobi', 'document', 'ebook'],
|
|
55
|
+
['.xlsx', 'spreadsheet', 'excel'], ['.xls', 'spreadsheet', 'excel'], ['.xlsm', 'spreadsheet', 'excel'],
|
|
56
|
+
['.csv', 'spreadsheet', 'csv'], ['.tsv', 'spreadsheet', 'csv'], ['.tab', 'spreadsheet', 'csv'], ['.ods', 'spreadsheet', 'opendoc'],
|
|
57
|
+
['.png', 'image', 'raster'], ['.jpg', 'image', 'raster'], ['.jpeg', 'image', 'raster'], ['.gif', 'image', 'raster'], ['.bmp', 'image', 'raster'], ['.webp', 'image', 'raster'], ['.tiff', 'image', 'raster'], ['.tif', 'image', 'raster'],
|
|
58
|
+
['.svg', 'image', 'vector'], ['.eps', 'image', 'vector'], ['.raw', 'image', 'raw'], ['.cr2', 'image', 'raw'], ['.nef', 'image', 'raw'], ['.dng', 'image', 'raw'],
|
|
59
|
+
['.dwg', 'cad', 'autocad'], ['.dxf', 'cad', 'autocad'], ['.dwt', 'cad', 'autocad'],
|
|
60
|
+
['.step', 'cad', 'step'], ['.stp', 'cad', 'step'], ['.p21', 'cad', 'step'], ['.iges', 'cad', 'iges'], ['.igs', 'cad', 'iges'],
|
|
61
|
+
['.stl', 'cad', 'mesh'], ['.obj', 'cad', 'mesh'], ['.3mf', 'cad', 'mesh'], ['.fbx', 'cad', 'mesh'], ['.glb', 'cad', 'mesh'], ['.gltf', 'cad', 'mesh'],
|
|
62
|
+
['.sldprt', 'cad', 'solidworks'], ['.sldasm', 'cad', 'solidworks'], ['.slddrw', 'cad', 'solidworks'],
|
|
63
|
+
['.ts', 'code', 'typescript'], ['.tsx', 'code', 'typescript'], ['.mts', 'code', 'typescript'], ['.cts', 'code', 'typescript'],
|
|
64
|
+
['.js', 'code', 'javascript'], ['.jsx', 'code', 'javascript'], ['.mjs', 'code', 'javascript'], ['.cjs', 'code', 'javascript'],
|
|
65
|
+
['.py', 'code', 'python'], ['.pyi', 'code', 'python'], ['.pyx', 'code', 'python'], ['.ipynb', 'code', 'python'],
|
|
66
|
+
['.java', 'code', 'java_kotlin'], ['.kt', 'code', 'java_kotlin'], ['.scala', 'code', 'java_kotlin'],
|
|
67
|
+
['.c', 'code', 'c_family'], ['.cpp', 'code', 'c_family'], ['.cc', 'code', 'c_family'], ['.cxx', 'code', 'c_family'], ['.h', 'code', 'c_family'], ['.hpp', 'code', 'c_family'],
|
|
68
|
+
['.go', 'code', 'go'], ['.rs', 'code', 'rust'], ['.rb', 'code', 'ruby'], ['.php', 'code', 'php'],
|
|
69
|
+
['.sh', 'code', 'shell'], ['.bash', 'code', 'shell'], ['.zsh', 'code', 'shell'], ['.fish', 'code', 'shell'], ['.sql', 'code', 'sql'],
|
|
70
|
+
['.toml', 'code', 'config'], ['.ini', 'code', 'config'], ['.cfg', 'code', 'config'], ['.conf', 'code', 'config'], ['.env', 'code', 'config'],
|
|
71
|
+
['.json', 'data', 'json'], ['.jsonl', 'data', 'json'], ['.json5', 'data', 'json'], ['.geojson', 'data', 'json'],
|
|
72
|
+
['.yaml', 'data', 'yaml'], ['.yml', 'data', 'yaml'], ['.xml', 'data', 'xml'], ['.xsd', 'data', 'xml'], ['.wsdl', 'data', 'xml'], ['.proto', 'data', 'protobuf'], ['.graphql', 'data', 'graphql'], ['.gql', 'data', 'graphql'],
|
|
73
|
+
['.html', 'web', 'html'], ['.htm', 'web', 'html'], ['.xhtml', 'web', 'html'], ['.css', 'web', 'stylesheet'], ['.scss', 'web', 'stylesheet'], ['.sass', 'web', 'stylesheet'], ['.less', 'web', 'stylesheet'],
|
|
74
|
+
['.hbs', 'web', 'template'], ['.ejs', 'web', 'template'], ['.pug', 'web', 'template'], ['.j2', 'web', 'template'], ['.jinja2', 'web', 'template'],
|
|
75
|
+
['.drawio', 'diagram', 'drawio'], ['.dio', 'diagram', 'drawio'], ['.vsdx', 'diagram', 'visio'], ['.vdx', 'diagram', 'visio'], ['.puml', 'diagram', 'plantuml'], ['.plantuml', 'diagram', 'plantuml'], ['.mmd', 'diagram', 'mermaid'], ['.mermaid', 'diagram', 'mermaid'], ['.excalidraw', 'diagram', 'excalidraw'],
|
|
76
|
+
['.zip', 'archive', 'zip'], ['.jar', 'archive', 'zip'], ['.war', 'archive', 'zip'], ['.apk', 'archive', 'zip'], ['.tar', 'archive', 'tar'], ['.gz', 'archive', 'other'], ['.tgz', 'archive', 'tar'], ['.bz2', 'archive', 'other'], ['.rar', 'archive', 'other'], ['.7z', 'archive', 'other'],
|
|
77
|
+
];
|
|
78
|
+
return new Map(entries.map(([ext, category, format]) => [ext, [category, format]]));
|
|
79
|
+
}
|
|
80
|
+
inferMimeType(ext, category) {
|
|
81
|
+
const known = {
|
|
82
|
+
'.pdf': 'application/pdf',
|
|
83
|
+
'.json': 'application/json',
|
|
84
|
+
'.md': 'text/markdown',
|
|
85
|
+
'.txt': 'text/plain',
|
|
86
|
+
'.csv': 'text/csv',
|
|
87
|
+
'.html': 'text/html',
|
|
88
|
+
'.png': 'image/png',
|
|
89
|
+
'.jpg': 'image/jpeg',
|
|
90
|
+
'.jpeg': 'image/jpeg',
|
|
91
|
+
'.svg': 'image/svg+xml',
|
|
92
|
+
};
|
|
93
|
+
return known[ext] ?? `application/x-customize-agent-${category}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FileCategory } from './types.js';
|
|
2
|
+
export declare const KNOWLEDGE_BASE_DIR = "knowledgeBase";
|
|
3
|
+
export declare const PROJECT_CONFIG_PATH: readonly [".customize-agent", "kb", "project.json"];
|
|
4
|
+
export declare const USER_DATA_DIR = ".customize-agent";
|
|
5
|
+
export declare const GLOBAL_KNOWLEDGE_DIR = "global-knowledge";
|
|
6
|
+
export declare const ALL_CATEGORIES: readonly FileCategory[];
|
|
7
|
+
export declare const DEFAULT_CATEGORY_DIRS: Record<FileCategory, string>;
|
|
8
|
+
export declare const COLLECTION_CATEGORY_NAMES: Record<FileCategory, string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const KNOWLEDGE_BASE_DIR = 'knowledgeBase';
|
|
2
|
+
export const PROJECT_CONFIG_PATH = ['.customize-agent', 'kb', 'project.json'];
|
|
3
|
+
export const USER_DATA_DIR = '.customize-agent';
|
|
4
|
+
export const GLOBAL_KNOWLEDGE_DIR = 'global-knowledge';
|
|
5
|
+
export const ALL_CATEGORIES = [
|
|
6
|
+
'document',
|
|
7
|
+
'spreadsheet',
|
|
8
|
+
'image',
|
|
9
|
+
'cad',
|
|
10
|
+
'code',
|
|
11
|
+
'data',
|
|
12
|
+
'web',
|
|
13
|
+
'diagram',
|
|
14
|
+
'archive',
|
|
15
|
+
'other',
|
|
16
|
+
];
|
|
17
|
+
export const DEFAULT_CATEGORY_DIRS = {
|
|
18
|
+
document: '文档资料',
|
|
19
|
+
spreadsheet: '表格数据',
|
|
20
|
+
image: '图片素材',
|
|
21
|
+
cad: '图纸文件',
|
|
22
|
+
code: '代码文件',
|
|
23
|
+
data: '数据文件',
|
|
24
|
+
web: '网页文件',
|
|
25
|
+
diagram: '图表流程',
|
|
26
|
+
archive: '压缩包',
|
|
27
|
+
other: '其他文件',
|
|
28
|
+
};
|
|
29
|
+
export const COLLECTION_CATEGORY_NAMES = {
|
|
30
|
+
document: 'documents',
|
|
31
|
+
spreadsheet: 'spreadsheets',
|
|
32
|
+
image: 'images',
|
|
33
|
+
cad: 'cad',
|
|
34
|
+
code: 'code',
|
|
35
|
+
data: 'data',
|
|
36
|
+
web: 'web',
|
|
37
|
+
diagram: 'diagrams',
|
|
38
|
+
archive: 'archives',
|
|
39
|
+
other: 'other',
|
|
40
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FileClassifier } from '../classification/classifier.js';
|
|
2
|
+
import type { DiffResult } from '../types.js';
|
|
3
|
+
import type { DiskFileStat } from './file-scanner.js';
|
|
4
|
+
import type { IndexStateStore } from './index-state-store.js';
|
|
5
|
+
export declare class ChangeTracker {
|
|
6
|
+
private readonly store;
|
|
7
|
+
constructor(store: IndexStateStore);
|
|
8
|
+
computeDiff(diskFiles: Map<string, DiskFileStat>, classifier: FileClassifier, kbPath: string): Promise<DiffResult>;
|
|
9
|
+
hashFile(filePath: string): string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
export class ChangeTracker {
|
|
5
|
+
store;
|
|
6
|
+
constructor(store) {
|
|
7
|
+
this.store = store;
|
|
8
|
+
}
|
|
9
|
+
async computeDiff(diskFiles, classifier, kbPath) {
|
|
10
|
+
const startTime = Date.now();
|
|
11
|
+
const indexedFiles = this.store.loadActiveRecords();
|
|
12
|
+
const newFiles = [];
|
|
13
|
+
const modifiedFiles = [];
|
|
14
|
+
const deletedFiles = [];
|
|
15
|
+
const skippedFiles = [];
|
|
16
|
+
let unchangedCount = 0;
|
|
17
|
+
let mtimeOnlyCount = 0;
|
|
18
|
+
for (const [relativePath, diskStat] of diskFiles) {
|
|
19
|
+
const absolutePath = path.join(kbPath, relativePath);
|
|
20
|
+
const stat = fs.statSync(absolutePath);
|
|
21
|
+
const classified = classifier.classify(absolutePath, relativePath, stat);
|
|
22
|
+
const skipReason = classifier.shouldSkip(classified);
|
|
23
|
+
if (skipReason) {
|
|
24
|
+
skippedFiles.push({ file: classified, reason: skipReason });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const indexed = indexedFiles.get(relativePath);
|
|
28
|
+
if (!indexed) {
|
|
29
|
+
newFiles.push(classified);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (Math.round(diskStat.mtime) !== Math.round(indexed.mtime) || diskStat.size !== indexed.fileSize) {
|
|
33
|
+
const contentHash = this.hashFile(absolutePath);
|
|
34
|
+
if (contentHash !== indexed.contentHash) {
|
|
35
|
+
modifiedFiles.push(classified);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
mtimeOnlyCount += 1;
|
|
39
|
+
this.store.updateVerified(relativePath, diskStat.mtime);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
unchangedCount += 1;
|
|
44
|
+
this.store.updateVerified(relativePath, diskStat.mtime);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [relativePath, record] of indexedFiles) {
|
|
48
|
+
if (!diskFiles.has(relativePath))
|
|
49
|
+
deletedFiles.push(record);
|
|
50
|
+
}
|
|
51
|
+
const hasChanges = newFiles.length > 0 || modifiedFiles.length > 0 || deletedFiles.length > 0;
|
|
52
|
+
return {
|
|
53
|
+
newFiles,
|
|
54
|
+
modifiedFiles,
|
|
55
|
+
deletedFiles,
|
|
56
|
+
unchangedCount,
|
|
57
|
+
mtimeOnlyCount,
|
|
58
|
+
skippedFiles,
|
|
59
|
+
hasChanges,
|
|
60
|
+
diffTimeMs: Date.now() - startTime,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
hashFile(filePath) {
|
|
64
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
export class KnowledgeFileScanner {
|
|
5
|
+
async scan(kbPath, ignorePatterns = []) {
|
|
6
|
+
if (!fs.existsSync(kbPath))
|
|
7
|
+
return new Map();
|
|
8
|
+
const entries = await fg('**/*', {
|
|
9
|
+
cwd: kbPath,
|
|
10
|
+
onlyFiles: true,
|
|
11
|
+
dot: true,
|
|
12
|
+
ignore: ['.kbignore', ...ignorePatterns],
|
|
13
|
+
unique: true,
|
|
14
|
+
});
|
|
15
|
+
const files = new Map();
|
|
16
|
+
for (const relativePath of entries) {
|
|
17
|
+
const absolutePath = path.join(kbPath, relativePath);
|
|
18
|
+
const stat = fs.statSync(absolutePath);
|
|
19
|
+
files.set(relativePath, { size: stat.size, mtime: stat.mtimeMs });
|
|
20
|
+
}
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
loadKbIgnore(kbPath) {
|
|
24
|
+
const ignorePath = path.join(kbPath, '.kbignore');
|
|
25
|
+
if (!fs.existsSync(ignorePath))
|
|
26
|
+
return [];
|
|
27
|
+
return fs.readFileSync(ignorePath, 'utf8')
|
|
28
|
+
.split(/\r?\n/u)
|
|
29
|
+
.map(line => line.trim())
|
|
30
|
+
.filter(line => line.length > 0 && !line.startsWith('#'));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { TextChunk } from '../chunking/text-chunker.js';
|
|
2
|
+
import type { FileCategory, IndexStateRecord } from '../types.js';
|
|
3
|
+
export interface StoredChunk {
|
|
4
|
+
id: string;
|
|
5
|
+
relativePath: string;
|
|
6
|
+
chunkIndex: number;
|
|
7
|
+
content: string;
|
|
8
|
+
category: FileCategory;
|
|
9
|
+
format: string;
|
|
10
|
+
collectionName: string;
|
|
11
|
+
tokenCount: number;
|
|
12
|
+
sectionTitle?: string;
|
|
13
|
+
metadataJson?: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ChunkSearchResult extends StoredChunk {
|
|
17
|
+
score: number;
|
|
18
|
+
}
|
|
19
|
+
export interface FileHashRecord {
|
|
20
|
+
contentHash: string;
|
|
21
|
+
filePath: string;
|
|
22
|
+
fileSize: number;
|
|
23
|
+
category: FileCategory;
|
|
24
|
+
normalizedHash?: string;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
updatedAt: number;
|
|
27
|
+
}
|
|
28
|
+
export interface MinHashRecord {
|
|
29
|
+
filePath: string;
|
|
30
|
+
signature: number[];
|
|
31
|
+
shingleCount: number;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
}
|
|
34
|
+
export interface FileRelationship {
|
|
35
|
+
id?: number;
|
|
36
|
+
sourceFile: string;
|
|
37
|
+
targetFile: string;
|
|
38
|
+
relationshipType: 'exact_duplicate' | 'format_variant' | 'translation' | 'near_duplicate' | 'revision' | 'version_chain' | 'derived' | 'complementary';
|
|
39
|
+
confidence: number;
|
|
40
|
+
detail?: string;
|
|
41
|
+
userConfirmed: number;
|
|
42
|
+
createdAt: number;
|
|
43
|
+
}
|
|
44
|
+
export declare class IndexStateStore {
|
|
45
|
+
private readonly db;
|
|
46
|
+
constructor(dbPath: string);
|
|
47
|
+
loadActiveRecords(): Map<string, IndexStateRecord>;
|
|
48
|
+
upsertRecord(record: IndexStateRecord): void;
|
|
49
|
+
updateVerified(relativePath: string, mtime: number): void;
|
|
50
|
+
listRecords(): IndexStateRecord[];
|
|
51
|
+
replaceChunks(relativePath: string, chunks: TextChunk[], file: {
|
|
52
|
+
category: FileCategory;
|
|
53
|
+
format: string;
|
|
54
|
+
collectionName: string;
|
|
55
|
+
}): void;
|
|
56
|
+
listChunks(options?: {
|
|
57
|
+
collectionName?: string;
|
|
58
|
+
relativePath?: string;
|
|
59
|
+
limit?: number;
|
|
60
|
+
}): StoredChunk[];
|
|
61
|
+
searchChunks(query: string, limit?: number): ChunkSearchResult[];
|
|
62
|
+
findExactDuplicate(contentHash: string, excludePath?: string): FileHashRecord | undefined;
|
|
63
|
+
findNormalizedDuplicate(normalizedHash: string, excludePath?: string): FileHashRecord | undefined;
|
|
64
|
+
upsertFileHash(record: Omit<FileHashRecord, 'createdAt' | 'updatedAt'>): void;
|
|
65
|
+
upsertMinHash(record: Omit<MinHashRecord, 'createdAt'>): void;
|
|
66
|
+
listMinHashes(excludePath?: string): MinHashRecord[];
|
|
67
|
+
addRelationship(relationship: Omit<FileRelationship, 'id' | 'createdAt'>): void;
|
|
68
|
+
listRelationships(filePath?: string): FileRelationship[];
|
|
69
|
+
setTags(relativePath: string, tags: string[]): void;
|
|
70
|
+
listTags(relativePath?: string): Array<{
|
|
71
|
+
filePath: string;
|
|
72
|
+
tag: string;
|
|
73
|
+
createdAt: number;
|
|
74
|
+
}>;
|
|
75
|
+
addIgnoreRule(pattern: string): void;
|
|
76
|
+
listIgnoreRules(): Array<{
|
|
77
|
+
id: number;
|
|
78
|
+
pattern: string;
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
createdAt: number;
|
|
81
|
+
}>;
|
|
82
|
+
deleteRecord(relativePath: string): void;
|
|
83
|
+
setMetadata(key: string, value: string): void;
|
|
84
|
+
getStats(): {
|
|
85
|
+
fileCount: number;
|
|
86
|
+
chunkCount: number;
|
|
87
|
+
totalSizeBytes: number;
|
|
88
|
+
lastIndexedAt: number;
|
|
89
|
+
};
|
|
90
|
+
listContentHashes(): Array<{
|
|
91
|
+
contentHash: string;
|
|
92
|
+
relativePath: string;
|
|
93
|
+
}>;
|
|
94
|
+
close(): void;
|
|
95
|
+
private initTables;
|
|
96
|
+
private rowToMinHash;
|
|
97
|
+
private rowToFileHash;
|
|
98
|
+
private rowToRelationship;
|
|
99
|
+
private rowToChunk;
|
|
100
|
+
private expandSearchTerms;
|
|
101
|
+
private scoreChunk;
|
|
102
|
+
private rowToRecord;
|
|
103
|
+
}
|