doc2vec 2.0.0 → 2.2.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.
package/dist/doc2vec.js CHANGED
@@ -37,11 +37,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.Doc2Vec = void 0;
40
41
  const axios_1 = __importDefault(require("axios"));
41
42
  const crypto_1 = __importDefault(require("crypto"));
42
43
  const yaml = __importStar(require("js-yaml"));
43
44
  const fs = __importStar(require("fs"));
44
45
  const path = __importStar(require("path"));
46
+ const os = __importStar(require("os"));
47
+ const child_process_1 = require("child_process");
48
+ const util_1 = require("util");
45
49
  const buffer_1 = require("buffer");
46
50
  const openai_1 = require("openai");
47
51
  const dotenv = __importStar(require("dotenv"));
@@ -50,9 +54,11 @@ const utils_1 = require("./utils");
50
54
  const database_1 = require("./database");
51
55
  const content_processor_1 = require("./content-processor");
52
56
  const GITHUB_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
57
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
53
58
  dotenv.config();
54
59
  class Doc2Vec {
55
60
  constructor(configPath) {
61
+ this.brokenLinksByWebsite = {};
56
62
  this.logger = new logger_1.Logger('Doc2Vec', {
57
63
  level: logger_1.LogLevel.DEBUG,
58
64
  useTimestamp: true,
@@ -61,7 +67,40 @@ class Doc2Vec {
61
67
  });
62
68
  this.logger.info('Initializing Doc2Vec');
63
69
  this.config = this.loadConfig(configPath);
64
- this.openai = new openai_1.OpenAI({ apiKey: process.env.OPENAI_API_KEY });
70
+ this.configDir = path.dirname(path.resolve(configPath));
71
+ // Initialize OpenAI or Azure OpenAI based on configuration
72
+ // Check environment variable if not specified in config
73
+ const embeddingProvider = this.config.embedding?.provider || process.env.EMBEDDING_PROVIDER || 'openai';
74
+ const embeddingConfig = this.config.embedding || { provider: embeddingProvider };
75
+ if (embeddingProvider === 'azure') {
76
+ const azureApiKey = embeddingConfig.azure?.api_key || process.env.AZURE_OPENAI_KEY;
77
+ const azureEndpoint = embeddingConfig.azure?.endpoint || process.env.AZURE_OPENAI_ENDPOINT;
78
+ const azureDeploymentName = embeddingConfig.azure?.deployment_name || process.env.AZURE_OPENAI_DEPLOYMENT_NAME || 'text-embedding-3-large';
79
+ const azureApiVersion = embeddingConfig.azure?.api_version || process.env.AZURE_OPENAI_API_VERSION || '2024-10-21';
80
+ if (!azureApiKey || !azureEndpoint) {
81
+ this.logger.error('Azure OpenAI requires api_key and endpoint to be configured');
82
+ process.exit(1);
83
+ }
84
+ this.openai = new openai_1.AzureOpenAI({
85
+ apiKey: azureApiKey,
86
+ endpoint: azureEndpoint,
87
+ deployment: azureDeploymentName,
88
+ apiVersion: azureApiVersion,
89
+ });
90
+ this.embeddingModel = azureDeploymentName;
91
+ this.logger.info(`Using Azure OpenAI with deployment: ${azureDeploymentName}`);
92
+ }
93
+ else {
94
+ const openaiApiKey = embeddingConfig.openai?.api_key || process.env.OPENAI_API_KEY;
95
+ const openaiModel = embeddingConfig.openai?.model || process.env.OPENAI_MODEL || 'text-embedding-3-large';
96
+ if (!openaiApiKey) {
97
+ this.logger.error('OpenAI requires api_key to be configured');
98
+ process.exit(1);
99
+ }
100
+ this.openai = new openai_1.OpenAI({ apiKey: openaiApiKey });
101
+ this.embeddingModel = openaiModel;
102
+ this.logger.info(`Using OpenAI with model: ${openaiModel}`);
103
+ }
65
104
  this.contentProcessor = new content_processor_1.ContentProcessor(this.logger);
66
105
  }
67
106
  loadConfig(configPath) {
@@ -81,6 +120,22 @@ class Doc2Vec {
81
120
  });
82
121
  let config = yaml.load(configFile);
83
122
  const typedConfig = config;
123
+ for (const source of typedConfig.sources) {
124
+ if (source.type === 'code') {
125
+ if (!source.version || String(source.version).trim().length === 0) {
126
+ if (source.branch && String(source.branch).trim().length > 0) {
127
+ source.version = source.branch;
128
+ }
129
+ else {
130
+ source.version = 'local';
131
+ }
132
+ }
133
+ }
134
+ else if (!source.version || String(source.version).trim().length === 0) {
135
+ logger.error(`Missing required version for ${source.type} source: ${source.product_name}`);
136
+ process.exit(1);
137
+ }
138
+ }
84
139
  logger.info(`Configuration loaded successfully, found ${typedConfig.sources.length} sources`);
85
140
  return typedConfig;
86
141
  }
@@ -103,6 +158,9 @@ class Doc2Vec {
103
158
  else if (sourceConfig.type === 'local_directory') {
104
159
  await this.processLocalDirectory(sourceConfig, sourceLogger);
105
160
  }
161
+ else if (sourceConfig.type === 'code') {
162
+ await this.processCodeSource(sourceConfig, sourceLogger);
163
+ }
106
164
  else if (sourceConfig.type === 'zendesk') {
107
165
  await this.processZendesk(sourceConfig, sourceLogger);
108
166
  }
@@ -413,6 +471,8 @@ class Doc2Vec {
413
471
  logger.error(`Error during chunking or embedding for ${url}:`, error);
414
472
  }
415
473
  }, logger, visitedUrls);
474
+ this.recordBrokenLinks(config.url, crawlResult.brokenLinks);
475
+ this.writeBrokenLinksReport();
416
476
  logger.info(`Found ${validChunkIds.size} valid chunks across processed pages for ${config.url}`);
417
477
  logger.section('CLEANUP');
418
478
  if (crawlResult.hasNetworkErrors) {
@@ -430,6 +490,38 @@ class Doc2Vec {
430
490
  }
431
491
  logger.info(`Finished processing website: ${config.url}`);
432
492
  }
493
+ recordBrokenLinks(baseUrl, brokenLinks) {
494
+ const uniqueByKey = new Map();
495
+ for (const link of brokenLinks) {
496
+ const key = `${link.source} -> ${link.target}`;
497
+ if (!uniqueByKey.has(key)) {
498
+ uniqueByKey.set(key, link);
499
+ }
500
+ }
501
+ const unique = Array.from(uniqueByKey.values()).sort((a, b) => {
502
+ const sourceCompare = a.source.localeCompare(b.source);
503
+ if (sourceCompare !== 0)
504
+ return sourceCompare;
505
+ return a.target.localeCompare(b.target);
506
+ });
507
+ this.brokenLinksByWebsite[baseUrl] = unique;
508
+ }
509
+ writeBrokenLinksReport() {
510
+ const reportPath = path.join(this.configDir, '404.yaml');
511
+ const orderedEntries = Object.entries(this.brokenLinksByWebsite)
512
+ .sort(([a], [b]) => a.localeCompare(b));
513
+ const reportPayload = orderedEntries.map(([website, links]) => ({
514
+ website,
515
+ 'broken-links': links
516
+ }));
517
+ try {
518
+ fs.writeFileSync(reportPath, yaml.dump(reportPayload, { noRefs: true }), 'utf8');
519
+ this.logger.info(`Wrote broken link report to ${reportPath}`);
520
+ }
521
+ catch (error) {
522
+ this.logger.error(`Failed to write broken link report to ${reportPath}:`, error);
523
+ }
524
+ }
433
525
  async processLocalDirectory(config, parentLogger) {
434
526
  const logger = parentLogger.child('process');
435
527
  logger.info(`Starting processing for local directory: ${config.path}`);
@@ -549,6 +641,383 @@ class Doc2Vec {
549
641
  }
550
642
  logger.info(`Finished processing local directory: ${config.path}`);
551
643
  }
644
+ async processCodeSource(config, parentLogger) {
645
+ const logger = parentLogger.child('process');
646
+ logger.info(`Starting processing for code source (${config.source})`);
647
+ const dbConnection = await database_1.DatabaseManager.initDatabase(config, logger);
648
+ const validChunkIds = new Set();
649
+ const processedFiles = new Set();
650
+ let basePath;
651
+ let cleanupPathConfig;
652
+ let tempDir = null;
653
+ let repoUrlPrefix;
654
+ let repoBranch;
655
+ let incrementalMode = false;
656
+ let deleteUrls = [];
657
+ let allowedFiles;
658
+ let mtimeCutoff;
659
+ let fileListKey;
660
+ let lastMtimeKey;
661
+ let trackedFiles;
662
+ let maxObservedMtime = 0;
663
+ if (config.source === 'local_directory') {
664
+ if (!config.path) {
665
+ logger.error('Code source type local_directory requires a path.');
666
+ return;
667
+ }
668
+ basePath = config.path;
669
+ cleanupPathConfig = config.url_rewrite_prefix
670
+ ? { path: basePath, url_rewrite_prefix: config.url_rewrite_prefix }
671
+ : basePath;
672
+ const resolvedPath = path.resolve(basePath);
673
+ const pathKey = resolvedPath.replace(/[^a-zA-Z0-9]+/g, '_');
674
+ lastMtimeKey = `code_last_mtime_${pathKey}`;
675
+ fileListKey = `code_filelist_${pathKey}`;
676
+ await database_1.DatabaseManager.initDatabaseMetadata(dbConnection, logger);
677
+ const lastMtimeValue = await database_1.DatabaseManager.getMetadataValue(dbConnection, lastMtimeKey, '0', logger);
678
+ mtimeCutoff = lastMtimeValue ? parseFloat(lastMtimeValue) : 0;
679
+ trackedFiles = new Set();
680
+ incrementalMode = true;
681
+ }
682
+ else if (config.source === 'github') {
683
+ if (!config.repo) {
684
+ logger.error('Code source type github requires a repo in owner/repo format.');
685
+ return;
686
+ }
687
+ const cloneResult = await this.cloneGithubRepo(config, logger);
688
+ basePath = cloneResult.path;
689
+ tempDir = cloneResult.path;
690
+ repoUrlPrefix = cloneResult.urlPrefix;
691
+ repoBranch = cloneResult.branch;
692
+ cleanupPathConfig = { path: basePath, url_rewrite_prefix: repoUrlPrefix };
693
+ await database_1.DatabaseManager.initDatabaseMetadata(dbConnection, logger);
694
+ const shaKey = this.buildCodeShaMetadataKey(config.repo, repoBranch);
695
+ const lastSha = await database_1.DatabaseManager.getMetadataValue(dbConnection, shaKey, undefined, logger);
696
+ const headSha = await this.getRepoHeadSha(basePath, logger);
697
+ if (lastSha && headSha) {
698
+ if (headSha === lastSha) {
699
+ incrementalMode = true;
700
+ allowedFiles = new Set();
701
+ deleteUrls = [];
702
+ }
703
+ else {
704
+ const diffResult = await this.getGitChangedFiles(basePath, lastSha, repoBranch, logger);
705
+ if (diffResult.mode === 'incremental') {
706
+ incrementalMode = true;
707
+ allowedFiles = diffResult.changedFiles;
708
+ deleteUrls = diffResult.deletedPaths
709
+ .map((relativePath) => this.buildCodeFileUrl(path.join(basePath, relativePath), basePath, config, repoUrlPrefix));
710
+ }
711
+ else {
712
+ logger.warn('Falling back to full scan for GitHub code source.');
713
+ }
714
+ }
715
+ }
716
+ }
717
+ else {
718
+ logger.error(`Unknown code source: ${config.source}`);
719
+ return;
720
+ }
721
+ logger.section('CODE SCANNING AND EMBEDDING');
722
+ try {
723
+ const scanResult = await this.contentProcessor.processCodeDirectory(basePath, config, async (filePath, content) => {
724
+ processedFiles.add(filePath);
725
+ const relativePath = path.relative(basePath, filePath).replace(/\\/g, '/');
726
+ const fileUrl = this.buildCodeFileUrl(filePath, basePath, config, repoUrlPrefix);
727
+ logger.info(`Processing code from ${relativePath || filePath} (${content.length} chars)`);
728
+ try {
729
+ const chunks = await this.contentProcessor.chunkCode(content, config, fileUrl, relativePath || filePath, repoBranch || config.branch, config.repo);
730
+ logger.info(`Created ${chunks.length} chunks`);
731
+ if (chunks.length > 0) {
732
+ const chunkProgress = logger.progress(`Embedding chunks for ${relativePath || filePath}`, chunks.length);
733
+ for (let i = 0; i < chunks.length; i++) {
734
+ const chunk = chunks[i];
735
+ validChunkIds.add(chunk.metadata.chunk_id);
736
+ const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
737
+ let needsEmbedding = true;
738
+ const chunkHash = utils_1.Utils.generateHash(chunk.content);
739
+ if (dbConnection.type === 'sqlite') {
740
+ const { checkHashStmt } = database_1.DatabaseManager.prepareSQLiteStatements(dbConnection.db);
741
+ const existing = checkHashStmt.get(chunk.metadata.chunk_id);
742
+ if (existing && existing.hash === chunkHash) {
743
+ needsEmbedding = false;
744
+ chunkProgress.update(1, `Skipping unchanged chunk ${chunkId}`);
745
+ logger.info(`Skipping unchanged chunk: ${chunkId}`);
746
+ }
747
+ }
748
+ else if (dbConnection.type === 'qdrant') {
749
+ try {
750
+ let pointId;
751
+ try {
752
+ pointId = chunk.metadata.chunk_id;
753
+ if (!utils_1.Utils.isValidUuid(pointId)) {
754
+ pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
755
+ }
756
+ }
757
+ catch (e) {
758
+ pointId = crypto_1.default.randomUUID();
759
+ }
760
+ const existingPoints = await dbConnection.client.retrieve(dbConnection.collectionName, {
761
+ ids: [pointId],
762
+ with_payload: true,
763
+ with_vector: false,
764
+ });
765
+ if (existingPoints.length > 0 && existingPoints[0].payload && existingPoints[0].payload.hash === chunkHash) {
766
+ needsEmbedding = false;
767
+ chunkProgress.update(1, `Skipping unchanged chunk ${chunkId}`);
768
+ logger.info(`Skipping unchanged chunk: ${chunkId}`);
769
+ }
770
+ }
771
+ catch (error) {
772
+ logger.error(`Error checking existing point in Qdrant:`, error);
773
+ }
774
+ }
775
+ if (needsEmbedding) {
776
+ const embeddings = await this.createEmbeddings([chunk.content]);
777
+ if (embeddings.length > 0) {
778
+ const embedding = embeddings[0];
779
+ if (dbConnection.type === 'sqlite') {
780
+ database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embedding, logger, chunkHash);
781
+ chunkProgress.update(1, `Stored chunk ${chunkId} in SQLite`);
782
+ }
783
+ else if (dbConnection.type === 'qdrant') {
784
+ await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embedding, chunkHash);
785
+ chunkProgress.update(1, `Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
786
+ }
787
+ }
788
+ else {
789
+ logger.error(`Embedding failed for chunk: ${chunkId}`);
790
+ chunkProgress.update(1, `Failed to embed chunk ${chunkId}`);
791
+ }
792
+ }
793
+ }
794
+ chunkProgress.complete();
795
+ }
796
+ }
797
+ catch (error) {
798
+ logger.error(`Error during code chunking or embedding for ${filePath}:`, error);
799
+ }
800
+ }, logger, undefined, {
801
+ allowedFiles,
802
+ mtimeCutoff,
803
+ trackFiles: trackedFiles
804
+ });
805
+ if (trackedFiles) {
806
+ maxObservedMtime = scanResult.maxMtime;
807
+ }
808
+ }
809
+ finally {
810
+ logger.section('CLEANUP');
811
+ if (incrementalMode) {
812
+ if (deleteUrls.length > 0) {
813
+ logger.info(`Cleaning up ${deleteUrls.length} deleted/renamed files`);
814
+ for (const url of deleteUrls) {
815
+ if (dbConnection.type === 'sqlite') {
816
+ database_1.DatabaseManager.removeChunksByUrlSQLite(dbConnection.db, url, logger);
817
+ }
818
+ else if (dbConnection.type === 'qdrant') {
819
+ await database_1.DatabaseManager.removeChunksByUrlQdrant(dbConnection, url, logger);
820
+ }
821
+ }
822
+ }
823
+ else {
824
+ logger.info('No deleted/renamed files to clean up.');
825
+ }
826
+ if (trackedFiles && fileListKey) {
827
+ const previousListValue = await database_1.DatabaseManager.getMetadataValue(dbConnection, fileListKey, '[]', logger);
828
+ const previousList = previousListValue ? JSON.parse(previousListValue) : [];
829
+ const currentList = Array.from(trackedFiles);
830
+ const deletedFiles = previousList.filter((filePath) => !trackedFiles?.has(filePath));
831
+ for (const deletedFile of deletedFiles) {
832
+ const url = this.buildCodeFileUrl(deletedFile, basePath, config, repoUrlPrefix);
833
+ if (dbConnection.type === 'sqlite') {
834
+ database_1.DatabaseManager.removeChunksByUrlSQLite(dbConnection.db, url, logger);
835
+ }
836
+ else if (dbConnection.type === 'qdrant') {
837
+ await database_1.DatabaseManager.removeChunksByUrlQdrant(dbConnection, url, logger);
838
+ }
839
+ }
840
+ await database_1.DatabaseManager.setMetadataValue(dbConnection, fileListKey, JSON.stringify(currentList), logger);
841
+ if (lastMtimeKey) {
842
+ const nextMtime = maxObservedMtime > 0 ? maxObservedMtime : Date.now();
843
+ await database_1.DatabaseManager.setMetadataValue(dbConnection, lastMtimeKey, `${nextMtime}`, logger);
844
+ }
845
+ }
846
+ }
847
+ else {
848
+ if (dbConnection.type === 'sqlite') {
849
+ logger.info(`Running SQLite cleanup for code source ${basePath}`);
850
+ database_1.DatabaseManager.removeObsoleteFilesSQLite(dbConnection.db, processedFiles, cleanupPathConfig, logger);
851
+ }
852
+ else if (dbConnection.type === 'qdrant') {
853
+ logger.info(`Running Qdrant cleanup for code source ${basePath} in collection ${dbConnection.collectionName}`);
854
+ await database_1.DatabaseManager.removeObsoleteFilesQdrant(dbConnection, processedFiles, cleanupPathConfig, logger);
855
+ }
856
+ }
857
+ if (config.source === 'github' && basePath && repoBranch) {
858
+ const headSha = await this.getRepoHeadSha(basePath, logger);
859
+ if (headSha) {
860
+ const shaKey = this.buildCodeShaMetadataKey(config.repo, repoBranch);
861
+ await database_1.DatabaseManager.setMetadataValue(dbConnection, shaKey, headSha, logger);
862
+ }
863
+ }
864
+ if (tempDir) {
865
+ try {
866
+ fs.rmSync(tempDir, { recursive: true, force: true });
867
+ logger.debug(`Removed temporary repo at ${tempDir}`);
868
+ }
869
+ catch (error) {
870
+ logger.warn(`Failed to remove temporary repo at ${tempDir}:`, error);
871
+ }
872
+ }
873
+ }
874
+ logger.info(`Finished processing code source (${config.source})`);
875
+ }
876
+ buildCodeShaMetadataKey(repo, branch) {
877
+ const normalizedRepo = repo.replace(/[^a-zA-Z0-9]+/g, '_');
878
+ const normalizedBranch = branch.replace(/[^a-zA-Z0-9]+/g, '_');
879
+ return `code_last_sha_${normalizedRepo}_${normalizedBranch}`;
880
+ }
881
+ async getRepoHeadSha(repoPath, logger) {
882
+ try {
883
+ const { stdout } = await execAsync(`git -C "${repoPath}" rev-parse HEAD`);
884
+ return stdout.trim() || undefined;
885
+ }
886
+ catch (error) {
887
+ logger.warn(`Failed to resolve HEAD sha for ${repoPath}:`, error);
888
+ return undefined;
889
+ }
890
+ }
891
+ async getGitChangedFiles(repoPath, lastSha, branch, logger) {
892
+ const diffCommand = `git -C "${repoPath}" diff --name-status ${lastSha}..HEAD`;
893
+ const attemptDiff = async () => {
894
+ const { stdout } = await execAsync(diffCommand);
895
+ return stdout;
896
+ };
897
+ let diffOutput;
898
+ try {
899
+ diffOutput = await attemptDiff();
900
+ }
901
+ catch (error) {
902
+ logger.warn(`Failed to diff against ${lastSha}. Fetching more history...`);
903
+ const fetchDepths = [200, 1000, 5000];
904
+ let fetched = false;
905
+ for (const depth of fetchDepths) {
906
+ try {
907
+ logger.info(`Fetching with --depth=${depth}...`);
908
+ await execAsync(`git -C "${repoPath}" fetch --depth=${depth} origin "${branch}"`);
909
+ diffOutput = await attemptDiff();
910
+ fetched = true;
911
+ break;
912
+ }
913
+ catch (fetchError) {
914
+ logger.warn(`Diff still failed at --depth=${depth}.`);
915
+ }
916
+ }
917
+ if (!fetched) {
918
+ try {
919
+ logger.info(`Attempting full unshallow fetch...`);
920
+ await execAsync(`git -C "${repoPath}" fetch --unshallow origin "${branch}"`);
921
+ diffOutput = await attemptDiff();
922
+ }
923
+ catch (unshallowError) {
924
+ logger.warn(`Failed to diff even after full unshallow. Falling back to full scan.`, unshallowError);
925
+ return { mode: 'full', changedFiles: new Set(), deletedPaths: [] };
926
+ }
927
+ }
928
+ }
929
+ if (!diffOutput) {
930
+ logger.warn('No diff output available. Falling back to full scan.');
931
+ return { mode: 'full', changedFiles: new Set(), deletedPaths: [] };
932
+ }
933
+ const changedFiles = new Set();
934
+ const deletedPaths = [];
935
+ for (const line of diffOutput.split('\n')) {
936
+ const trimmed = line.trim();
937
+ if (!trimmed)
938
+ continue;
939
+ const parts = trimmed.split('\t');
940
+ const status = parts[0];
941
+ if (status.startsWith('R')) {
942
+ const oldPath = parts[1];
943
+ const newPath = parts[2];
944
+ if (oldPath)
945
+ deletedPaths.push(oldPath);
946
+ if (newPath)
947
+ changedFiles.add(path.join(repoPath, newPath));
948
+ }
949
+ else if (status === 'D') {
950
+ const deletedPath = parts[1];
951
+ if (deletedPath)
952
+ deletedPaths.push(deletedPath);
953
+ }
954
+ else if (status === 'A' || status === 'M') {
955
+ const changedPath = parts[1];
956
+ if (changedPath)
957
+ changedFiles.add(path.join(repoPath, changedPath));
958
+ }
959
+ }
960
+ logger.info(`Git diff changes: ${changedFiles.size} modified/added, ${deletedPaths.length} deleted/renamed.`);
961
+ return { mode: 'incremental', changedFiles, deletedPaths };
962
+ }
963
+ buildCodeFileUrl(filePath, basePath, config, repoUrlPrefix) {
964
+ const relativePath = path.relative(basePath, filePath).replace(/\\/g, '/');
965
+ if (repoUrlPrefix) {
966
+ return `${repoUrlPrefix}/${relativePath}`;
967
+ }
968
+ if (config.url_rewrite_prefix) {
969
+ if (relativePath.startsWith('..')) {
970
+ return `file://${filePath}`;
971
+ }
972
+ const prefix = config.url_rewrite_prefix.endsWith('/')
973
+ ? config.url_rewrite_prefix.slice(0, -1)
974
+ : config.url_rewrite_prefix;
975
+ return `${prefix}/${relativePath}`;
976
+ }
977
+ return `file://${filePath}`;
978
+ }
979
+ async cloneGithubRepo(config, logger) {
980
+ const repo = config.repo;
981
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc2vec-code-'));
982
+ const requestedBranch = config.branch;
983
+ const encodedToken = GITHUB_TOKEN ? encodeURIComponent(GITHUB_TOKEN) : '';
984
+ const repoUrl = encodedToken
985
+ ? `https://x-access-token:${encodedToken}@github.com/${repo}.git`
986
+ : `https://github.com/${repo}.git`;
987
+ const branchArg = requestedBranch ? `--branch "${requestedBranch}"` : '';
988
+ logger.info(`Cloning ${repo} to ${tempDir}`);
989
+ try {
990
+ await execAsync(`git clone --depth 1 ${branchArg} "${repoUrl}" "${tempDir}"`);
991
+ }
992
+ catch (error) {
993
+ try {
994
+ fs.rmSync(tempDir, { recursive: true, force: true });
995
+ }
996
+ catch (cleanupError) {
997
+ logger.warn(`Failed to clean up temp dir after clone failure: ${tempDir}`, cleanupError);
998
+ }
999
+ logger.error(`Failed to clone repo ${repo}:`, error);
1000
+ throw error;
1001
+ }
1002
+ let resolvedBranch = requestedBranch;
1003
+ if (!resolvedBranch) {
1004
+ resolvedBranch = await this.getRepoBranch(tempDir, logger);
1005
+ }
1006
+ const branch = resolvedBranch || 'main';
1007
+ const urlPrefix = `https://github.com/${repo}/blob/${branch}`;
1008
+ return { path: tempDir, branch, urlPrefix };
1009
+ }
1010
+ async getRepoBranch(repoPath, logger) {
1011
+ try {
1012
+ const { stdout } = await execAsync(`git -C "${repoPath}" symbolic-ref --short HEAD`);
1013
+ const branch = stdout.trim();
1014
+ return branch || undefined;
1015
+ }
1016
+ catch (error) {
1017
+ logger.warn(`Failed to resolve repo branch for ${repoPath}:`, error);
1018
+ return undefined;
1019
+ }
1020
+ }
552
1021
  async processZendesk(config, parentLogger) {
553
1022
  const logger = parentLogger.child('process');
554
1023
  logger.info(`Starting processing for Zendesk: ${config.zendesk_subdomain}.zendesk.com`);
@@ -892,7 +1361,7 @@ class Doc2Vec {
892
1361
  try {
893
1362
  logger.debug(`Creating embeddings for ${texts.length} texts`);
894
1363
  const response = await this.openai.embeddings.create({
895
- model: "text-embedding-3-large",
1364
+ model: this.embeddingModel,
896
1365
  input: texts,
897
1366
  });
898
1367
  logger.debug(`Successfully created ${response.data.length} embeddings`);
@@ -904,6 +1373,7 @@ class Doc2Vec {
904
1373
  }
905
1374
  }
906
1375
  }
1376
+ exports.Doc2Vec = Doc2Vec;
907
1377
  if (require.main === module) {
908
1378
  const configPath = process.argv[2] || 'config.yaml';
909
1379
  if (!fs.existsSync(configPath)) {
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_1 = require("vitest/config");
4
+ exports.default = (0, config_1.defineConfig)({
5
+ test: {
6
+ globals: true,
7
+ environment: 'node',
8
+ include: ['tests/**/*.test.ts'],
9
+ testTimeout: 30000,
10
+ coverage: {
11
+ provider: 'v8',
12
+ include: ['utils.ts', 'logger.ts', 'content-processor.ts', 'database.ts', 'code-chunker.ts', 'doc2vec.ts'],
13
+ exclude: ['mcp/**', 'dist/**', 'tests/**'],
14
+ },
15
+ },
16
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc2vec",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "type": "commonjs",
5
5
  "description": "",
6
6
  "main": "dist/doc2vec.js",
@@ -19,6 +19,9 @@
19
19
  "scripts": {
20
20
  "build": "tsc && chmod 755 dist/doc2vec.js",
21
21
  "start": "node dist/doc2vec.js",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "test:coverage": "vitest run --coverage",
22
25
  "prepublishOnly": "npm run build",
23
26
  "prepare": "npm run build"
24
27
  },
@@ -26,6 +29,7 @@
26
29
  "author": "",
27
30
  "license": "ISC",
28
31
  "dependencies": {
32
+ "@chonkiejs/core": "^0.0.7",
29
33
  "@mozilla/readability": "^0.4.4",
30
34
  "@qdrant/js-client-rest": "^1.13.0",
31
35
  "@qdrant/qdrant-js": "^1.13.0",
@@ -42,7 +46,9 @@
42
46
  "puppeteer": "^24.1.1",
43
47
  "sanitize-html": "^2.11.0",
44
48
  "sqlite-vec": "0.1.7-alpha.2",
49
+ "tree-sitter-wasms": "^0.1.0",
45
50
  "turndown": "^7.1.2",
51
+ "web-tree-sitter": "^0.25.4",
46
52
  "word-extractor": "^1.0.4"
47
53
  },
48
54
  "devDependencies": {
@@ -52,7 +58,9 @@
52
58
  "@types/node": "^20.10.0",
53
59
  "@types/sanitize-html": "^2.9.5",
54
60
  "@types/turndown": "^5.0.4",
61
+ "@vitest/coverage-v8": "^4.0.18",
55
62
  "ts-node": "^10.9.1",
56
- "typescript": "^5.3.2"
63
+ "typescript": "^5.3.2",
64
+ "vitest": "^4.0.18"
57
65
  }
58
66
  }