doc2vec 1.2.0 → 2.0.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
@@ -42,6 +42,7 @@ const crypto_1 = __importDefault(require("crypto"));
42
42
  const yaml = __importStar(require("js-yaml"));
43
43
  const fs = __importStar(require("fs"));
44
44
  const path = __importStar(require("path"));
45
+ const buffer_1 = require("buffer");
45
46
  const openai_1 = require("openai");
46
47
  const dotenv = __importStar(require("dotenv"));
47
48
  const logger_1 = require("./logger");
@@ -67,7 +68,17 @@ class Doc2Vec {
67
68
  try {
68
69
  const logger = this.logger.child('config');
69
70
  logger.info(`Loading configuration from ${configPath}`);
70
- const configFile = fs.readFileSync(configPath, 'utf8');
71
+ let configFile = fs.readFileSync(configPath, 'utf8');
72
+ // Substitute environment variables in the format ${VAR_NAME}
73
+ configFile = configFile.replace(/\$\{([^}]+)\}/g, (match, varName) => {
74
+ const envValue = process.env[varName];
75
+ if (envValue === undefined) {
76
+ logger.warn(`Environment variable ${varName} not found, keeping placeholder ${match}`);
77
+ return match;
78
+ }
79
+ logger.debug(`Substituted ${match} with environment variable value`);
80
+ return envValue;
81
+ });
71
82
  let config = yaml.load(configFile);
72
83
  const typedConfig = config;
73
84
  logger.info(`Configuration loaded successfully, found ${typedConfig.sources.length} sources`);
@@ -92,6 +103,9 @@ class Doc2Vec {
92
103
  else if (sourceConfig.type === 'local_directory') {
93
104
  await this.processLocalDirectory(sourceConfig, sourceLogger);
94
105
  }
106
+ else if (sourceConfig.type === 'zendesk') {
107
+ await this.processZendesk(sourceConfig, sourceLogger);
108
+ }
95
109
  else {
96
110
  sourceLogger.error(`Unknown source type: ${sourceConfig.type}`);
97
111
  }
@@ -109,26 +123,64 @@ class Doc2Vec {
109
123
  const fetchWithRetry = async (url, params = {}, retries = 5, delay = 5000) => {
110
124
  for (let attempt = 0; attempt < retries; attempt++) {
111
125
  try {
126
+ // Only log on retries to reduce noise during pagination
127
+ if (attempt > 0) {
128
+ logger.debug(`GitHub API retry: ${url} (attempt ${attempt + 1}/${retries})`);
129
+ }
112
130
  const response = await axios_1.default.get(url, {
113
131
  headers: {
114
132
  Authorization: `token ${GITHUB_TOKEN}`,
115
133
  Accept: 'application/vnd.github.v3+json',
116
134
  },
117
135
  params,
136
+ timeout: 30000, // 30 second timeout
118
137
  });
119
138
  return response.data;
120
139
  }
121
140
  catch (error) {
141
+ // Enhanced error logging for debugging
142
+ const errorDetails = {
143
+ code: error.code,
144
+ message: error.message,
145
+ status: error.response?.status,
146
+ isTimeout: error.code === 'ECONNABORTED' || error.message?.includes('timeout'),
147
+ isNetworkError: !error.response && error.code,
148
+ };
149
+ logger.debug(`GitHub API error details: ${JSON.stringify(errorDetails)}`);
122
150
  if (error.response && error.response.status === 403) {
151
+ // Check if this is actually a rate limit error
152
+ const rateLimitRemaining = error.response.headers['x-ratelimit-remaining'];
123
153
  const resetTime = error.response.headers['x-ratelimit-reset'];
124
- const currentTime = Math.floor(Date.now() / 1000);
125
- const waitTime = resetTime ? (resetTime - currentTime) * 1000 : delay * 2;
126
- logger.warn(`GitHub rate limit exceeded. Waiting ${waitTime / 1000}s`);
127
- await new Promise(res => setTimeout(res, waitTime));
154
+ if (rateLimitRemaining === '0' && resetTime) {
155
+ const currentTime = Math.floor(Date.now() / 1000);
156
+ const resetTimestamp = parseInt(resetTime, 10);
157
+ let waitTime = (resetTimestamp - currentTime) * 1000;
158
+ // Ensure waitTime is at least 1 second (in case resetTime is in the past)
159
+ if (waitTime < 1000) {
160
+ waitTime = 1000;
161
+ }
162
+ logger.warn(`GitHub rate limit exceeded. Waiting ${Math.ceil(waitTime / 1000)}s (attempt ${attempt + 1}/${retries})`);
163
+ await new Promise(res => setTimeout(res, waitTime));
164
+ // Retry the request after waiting
165
+ continue;
166
+ }
167
+ else {
168
+ // Other 403 errors (e.g., forbidden access)
169
+ logger.error(`GitHub API returned 403 (not rate limit): ${error.message}`);
170
+ throw error;
171
+ }
128
172
  }
129
173
  else {
130
- logger.error(`GitHub fetch failed: ${error.message}`);
131
- throw error;
174
+ // For non-403 errors, wait before retrying (exponential backoff)
175
+ if (attempt < retries - 1) {
176
+ const backoffDelay = delay * Math.pow(2, attempt);
177
+ logger.warn(`GitHub fetch failed (attempt ${attempt + 1}/${retries}): ${error.message}. Retrying in ${backoffDelay}ms`);
178
+ await new Promise(res => setTimeout(res, backoffDelay));
179
+ }
180
+ else {
181
+ logger.error(`GitHub fetch failed after ${retries} attempts: ${error.message} (code: ${error.code || 'unknown'})`);
182
+ throw error;
183
+ }
132
184
  }
133
185
  }
134
186
  }
@@ -140,6 +192,10 @@ class Doc2Vec {
140
192
  const perPage = 100;
141
193
  const sinceTimestamp = new Date(sinceDate);
142
194
  while (true) {
195
+ // Log progress every 10 pages to reduce noise
196
+ if (page === 1 || page % 10 === 0) {
197
+ logger.debug(`Fetching issues page ${page}... (${issues.length} issues so far)`);
198
+ }
143
199
  const data = await fetchWithRetry(GITHUB_API_URL, {
144
200
  per_page: perPage,
145
201
  page,
@@ -493,6 +549,344 @@ class Doc2Vec {
493
549
  }
494
550
  logger.info(`Finished processing local directory: ${config.path}`);
495
551
  }
552
+ async processZendesk(config, parentLogger) {
553
+ const logger = parentLogger.child('process');
554
+ logger.info(`Starting processing for Zendesk: ${config.zendesk_subdomain}.zendesk.com`);
555
+ const dbConnection = await database_1.DatabaseManager.initDatabase(config, logger);
556
+ // Initialize metadata storage
557
+ await database_1.DatabaseManager.initDatabaseMetadata(dbConnection, logger);
558
+ const fetchTickets = config.fetch_tickets !== false; // default true
559
+ const fetchArticles = config.fetch_articles !== false; // default true
560
+ if (fetchTickets) {
561
+ logger.section('ZENDESK TICKETS');
562
+ await this.fetchAndProcessZendeskTickets(config, dbConnection, logger);
563
+ }
564
+ if (fetchArticles) {
565
+ logger.section('ZENDESK ARTICLES');
566
+ await this.fetchAndProcessZendeskArticles(config, dbConnection, logger);
567
+ }
568
+ logger.info(`Finished processing Zendesk: ${config.zendesk_subdomain}.zendesk.com`);
569
+ }
570
+ async fetchAndProcessZendeskTickets(config, dbConnection, logger) {
571
+ const baseUrl = `https://${config.zendesk_subdomain}.zendesk.com/api/v2`;
572
+ const auth = buffer_1.Buffer.from(`${config.email}/token:${config.api_token}`).toString('base64');
573
+ // Get the last run date from the database
574
+ const startDate = config.start_date || `${new Date().getFullYear()}-01-01`;
575
+ const lastRunDate = await database_1.DatabaseManager.getLastRunDate(dbConnection, `zendesk_tickets_${config.zendesk_subdomain}`, `${startDate}T00:00:00Z`, logger);
576
+ const fetchWithRetry = async (url, retries = 3) => {
577
+ for (let attempt = 0; attempt < retries; attempt++) {
578
+ try {
579
+ const response = await axios_1.default.get(url, {
580
+ headers: {
581
+ 'Authorization': `Basic ${auth}`,
582
+ 'Content-Type': 'application/json',
583
+ },
584
+ });
585
+ if (response.status === 429) {
586
+ const retryAfter = parseInt(response.headers['retry-after'] || '60');
587
+ logger.warn(`Rate limited, waiting ${retryAfter}s before retry`);
588
+ await new Promise(res => setTimeout(res, retryAfter * 1000));
589
+ continue;
590
+ }
591
+ return response.data;
592
+ }
593
+ catch (error) {
594
+ logger.error(`Zendesk API error (attempt ${attempt + 1}):`, error.message);
595
+ if (attempt === retries - 1)
596
+ throw error;
597
+ await new Promise(res => setTimeout(res, 2000 * (attempt + 1)));
598
+ }
599
+ }
600
+ };
601
+ const generateMarkdownForTicket = (ticket, comments) => {
602
+ let md = `# Ticket #${ticket.id}: ${ticket.subject}\n\n`;
603
+ md += `- **Status:** ${ticket.status}\n`;
604
+ md += `- **Priority:** ${ticket.priority || 'None'}\n`;
605
+ md += `- **Type:** ${ticket.type || 'None'}\n`;
606
+ md += `- **Requester:** ${ticket.requester_id}\n`;
607
+ md += `- **Assignee:** ${ticket.assignee_id || 'Unassigned'}\n`;
608
+ md += `- **Created:** ${new Date(ticket.created_at).toDateString()}\n`;
609
+ md += `- **Updated:** ${new Date(ticket.updated_at).toDateString()}\n`;
610
+ if (ticket.tags && ticket.tags.length > 0) {
611
+ md += `- **Tags:** ${ticket.tags.map((tag) => `\`${tag}\``).join(', ')}\n`;
612
+ }
613
+ // Handle ticket description
614
+ const description = ticket.description || '';
615
+ const cleanDescription = description || '_No description._';
616
+ md += `\n## Description\n\n${cleanDescription}\n\n`;
617
+ if (comments && comments.length > 0) {
618
+ md += `## Comments\n\n`;
619
+ for (const comment of comments) {
620
+ if (comment.public) {
621
+ md += `### ${comment.author_id} - ${new Date(comment.created_at).toDateString()}\n\n`;
622
+ // Handle comment body
623
+ const rawBody = comment.plain_body || comment.html_body || comment.body || '';
624
+ const commentBody = rawBody.replace(/&nbsp;/g, " ") || '_No content._';
625
+ md += `${commentBody}\n\n---\n\n`;
626
+ }
627
+ }
628
+ }
629
+ else {
630
+ md += `## Comments\n\n_No comments._\n`;
631
+ }
632
+ return md;
633
+ };
634
+ const processTicket = async (ticket) => {
635
+ const ticketId = ticket.id;
636
+ const url = `https://${config.zendesk_subdomain}.zendesk.com/agent/tickets/${ticketId}`;
637
+ logger.info(`Processing ticket #${ticketId}`);
638
+ // Fetch ticket comments
639
+ const commentsUrl = `${baseUrl}/tickets/${ticketId}/comments.json`;
640
+ const commentsData = await fetchWithRetry(commentsUrl);
641
+ const comments = commentsData?.comments || [];
642
+ // Generate markdown for the ticket
643
+ const markdown = generateMarkdownForTicket(ticket, comments);
644
+ // Chunk the markdown content
645
+ const ticketConfig = {
646
+ ...config,
647
+ product_name: config.product_name || `zendesk_${config.zendesk_subdomain}`,
648
+ max_size: config.max_size || Infinity
649
+ };
650
+ const chunks = await this.contentProcessor.chunkMarkdown(markdown, ticketConfig, url);
651
+ logger.info(`Ticket #${ticketId}: Created ${chunks.length} chunks`);
652
+ // Process and store each chunk
653
+ for (const chunk of chunks) {
654
+ const chunkHash = utils_1.Utils.generateHash(chunk.content);
655
+ const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
656
+ if (dbConnection.type === 'sqlite') {
657
+ const { checkHashStmt } = database_1.DatabaseManager.prepareSQLiteStatements(dbConnection.db);
658
+ const existing = checkHashStmt.get(chunk.metadata.chunk_id);
659
+ if (existing && existing.hash === chunkHash) {
660
+ logger.info(`Skipping unchanged chunk: ${chunkId}`);
661
+ continue;
662
+ }
663
+ const embeddings = await this.createEmbeddings([chunk.content]);
664
+ if (embeddings.length) {
665
+ database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embeddings[0], logger, chunkHash);
666
+ logger.debug(`Stored chunk ${chunkId} in SQLite`);
667
+ }
668
+ else {
669
+ logger.error(`Embedding failed for chunk: ${chunkId}`);
670
+ }
671
+ }
672
+ else if (dbConnection.type === 'qdrant') {
673
+ try {
674
+ let pointId;
675
+ try {
676
+ pointId = chunk.metadata.chunk_id;
677
+ if (!utils_1.Utils.isValidUuid(pointId)) {
678
+ pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
679
+ }
680
+ }
681
+ catch (e) {
682
+ pointId = crypto_1.default.randomUUID();
683
+ }
684
+ const existingPoints = await dbConnection.client.retrieve(dbConnection.collectionName, {
685
+ ids: [pointId],
686
+ with_payload: true,
687
+ with_vector: false,
688
+ });
689
+ if (existingPoints.length > 0 && existingPoints[0].payload && existingPoints[0].payload.hash === chunkHash) {
690
+ logger.info(`Skipping unchanged chunk: ${chunkId}`);
691
+ continue;
692
+ }
693
+ const embeddings = await this.createEmbeddings([chunk.content]);
694
+ if (embeddings.length) {
695
+ await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embeddings[0], chunkHash);
696
+ logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
697
+ }
698
+ else {
699
+ logger.error(`Embedding failed for chunk: ${chunkId}`);
700
+ }
701
+ }
702
+ catch (error) {
703
+ logger.error(`Error processing chunk in Qdrant:`, error);
704
+ }
705
+ }
706
+ }
707
+ };
708
+ logger.info(`Fetching Zendesk tickets updated since ${lastRunDate}`);
709
+ // Build query parameters
710
+ const statusFilter = config.ticket_status || ['new', 'open', 'pending', 'hold', 'solved'];
711
+ const query = `updated>${lastRunDate.split('T')[0]} status:${statusFilter.join(',status:')}`;
712
+ let nextPage = `${baseUrl}/search.json?query=${encodeURIComponent(query)}&sort_by=updated_at&sort_order=asc`;
713
+ let totalTickets = 0;
714
+ while (nextPage) {
715
+ const data = await fetchWithRetry(nextPage);
716
+ const tickets = data.results || [];
717
+ logger.info(`Processing batch of ${tickets.length} tickets`);
718
+ for (const ticket of tickets) {
719
+ await processTicket(ticket);
720
+ totalTickets++;
721
+ }
722
+ nextPage = data.next_page;
723
+ if (nextPage) {
724
+ logger.debug(`Fetching next page: ${nextPage}`);
725
+ // Rate limiting: wait between requests
726
+ await new Promise(res => setTimeout(res, 1000));
727
+ }
728
+ }
729
+ // Update the last run date in the database
730
+ await database_1.DatabaseManager.updateLastRunDate(dbConnection, `zendesk_tickets_${config.zendesk_subdomain}`, logger);
731
+ logger.info(`Successfully processed ${totalTickets} tickets`);
732
+ }
733
+ async fetchAndProcessZendeskArticles(config, dbConnection, logger) {
734
+ const baseUrl = `https://${config.zendesk_subdomain}.zendesk.com/api/v2/help_center`;
735
+ const auth = buffer_1.Buffer.from(`${config.email}/token:${config.api_token}`).toString('base64');
736
+ // Get the start date for filtering
737
+ const startDate = config.start_date || `${new Date().getFullYear()}-01-01`;
738
+ const startDateObj = new Date(startDate);
739
+ const fetchWithRetry = async (url, retries = 3) => {
740
+ for (let attempt = 0; attempt < retries; attempt++) {
741
+ try {
742
+ const response = await axios_1.default.get(url, {
743
+ headers: {
744
+ 'Authorization': `Basic ${auth}`,
745
+ 'Content-Type': 'application/json',
746
+ },
747
+ });
748
+ if (response.status === 429) {
749
+ const retryAfter = parseInt(response.headers['retry-after'] || '60');
750
+ logger.warn(`Rate limited, waiting ${retryAfter}s before retry`);
751
+ await new Promise(res => setTimeout(res, retryAfter * 1000));
752
+ continue;
753
+ }
754
+ return response.data;
755
+ }
756
+ catch (error) {
757
+ logger.error(`Zendesk API error (attempt ${attempt + 1}):`, error.message);
758
+ if (attempt === retries - 1)
759
+ throw error;
760
+ await new Promise(res => setTimeout(res, 2000 * (attempt + 1)));
761
+ }
762
+ }
763
+ };
764
+ const generateMarkdownForArticle = (article) => {
765
+ let md = `# ${article.title}\n\n`;
766
+ md += `- **Author:** ${article.author_id}\n`;
767
+ md += `- **Section:** ${article.section_id}\n`;
768
+ md += `- **Created:** ${new Date(article.created_at).toDateString()}\n`;
769
+ md += `- **Updated:** ${new Date(article.updated_at).toDateString()}\n`;
770
+ md += `- **Vote Sum:** ${article.vote_sum || 0}\n`;
771
+ md += `- **Vote Count:** ${article.vote_count || 0}\n`;
772
+ if (article.label_names && article.label_names.length > 0) {
773
+ md += `- **Labels:** ${article.label_names.map((label) => `\`${label}\``).join(', ')}\n`;
774
+ }
775
+ // Handle article content - convert HTML to markdown
776
+ const articleBody = article.body || '';
777
+ let cleanContent = '_No content._';
778
+ if (articleBody.trim()) {
779
+ if (articleBody.includes('<')) {
780
+ // HTML content - use ContentProcessor to convert to markdown
781
+ cleanContent = this.contentProcessor.convertHtmlToMarkdown(articleBody);
782
+ }
783
+ else {
784
+ // Plain text content
785
+ cleanContent = articleBody;
786
+ }
787
+ }
788
+ md += `\n## Content\n\n${cleanContent}\n`;
789
+ return md;
790
+ };
791
+ const processArticle = async (article) => {
792
+ const articleId = article.id;
793
+ const url = article.html_url || `https://${config.zendesk_subdomain}.zendesk.com/hc/articles/${articleId}`;
794
+ logger.info(`Processing article #${articleId}: ${article.title}`);
795
+ // Generate markdown for the article
796
+ const markdown = generateMarkdownForArticle(article);
797
+ // Chunk the markdown content
798
+ const articleConfig = {
799
+ ...config,
800
+ product_name: config.product_name || `zendesk_${config.zendesk_subdomain}`,
801
+ max_size: config.max_size || Infinity
802
+ };
803
+ const chunks = await this.contentProcessor.chunkMarkdown(markdown, articleConfig, url);
804
+ logger.info(`Article #${articleId}: Created ${chunks.length} chunks`);
805
+ // Process and store each chunk (similar to ticket processing)
806
+ for (const chunk of chunks) {
807
+ const chunkHash = utils_1.Utils.generateHash(chunk.content);
808
+ const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
809
+ if (dbConnection.type === 'sqlite') {
810
+ const { checkHashStmt } = database_1.DatabaseManager.prepareSQLiteStatements(dbConnection.db);
811
+ const existing = checkHashStmt.get(chunk.metadata.chunk_id);
812
+ if (existing && existing.hash === chunkHash) {
813
+ logger.info(`Skipping unchanged chunk: ${chunkId}`);
814
+ continue;
815
+ }
816
+ const embeddings = await this.createEmbeddings([chunk.content]);
817
+ if (embeddings.length) {
818
+ database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embeddings[0], logger, chunkHash);
819
+ logger.debug(`Stored chunk ${chunkId} in SQLite`);
820
+ }
821
+ else {
822
+ logger.error(`Embedding failed for chunk: ${chunkId}`);
823
+ }
824
+ }
825
+ else if (dbConnection.type === 'qdrant') {
826
+ try {
827
+ let pointId;
828
+ try {
829
+ pointId = chunk.metadata.chunk_id;
830
+ if (!utils_1.Utils.isValidUuid(pointId)) {
831
+ pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
832
+ }
833
+ }
834
+ catch (e) {
835
+ pointId = crypto_1.default.randomUUID();
836
+ }
837
+ const existingPoints = await dbConnection.client.retrieve(dbConnection.collectionName, {
838
+ ids: [pointId],
839
+ with_payload: true,
840
+ with_vector: false,
841
+ });
842
+ if (existingPoints.length > 0 && existingPoints[0].payload && existingPoints[0].payload.hash === chunkHash) {
843
+ logger.info(`Skipping unchanged chunk: ${chunkId}`);
844
+ continue;
845
+ }
846
+ const embeddings = await this.createEmbeddings([chunk.content]);
847
+ if (embeddings.length) {
848
+ await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embeddings[0], chunkHash);
849
+ logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
850
+ }
851
+ else {
852
+ logger.error(`Embedding failed for chunk: ${chunkId}`);
853
+ }
854
+ }
855
+ catch (error) {
856
+ logger.error(`Error processing chunk in Qdrant:`, error);
857
+ }
858
+ }
859
+ }
860
+ };
861
+ logger.info(`Fetching Zendesk help center articles updated since ${startDate}`);
862
+ let nextPage = `${baseUrl}/articles.json`;
863
+ let totalArticles = 0;
864
+ let processedArticles = 0;
865
+ while (nextPage) {
866
+ const data = await fetchWithRetry(nextPage);
867
+ const articles = data.articles || [];
868
+ logger.info(`Processing batch of ${articles.length} articles`);
869
+ for (const article of articles) {
870
+ totalArticles++;
871
+ // Check if article was updated since the start date
872
+ const updatedAt = new Date(article.updated_at);
873
+ if (updatedAt >= startDateObj) {
874
+ await processArticle(article);
875
+ processedArticles++;
876
+ }
877
+ else {
878
+ logger.debug(`Skipping article #${article.id} (updated ${article.updated_at}, before ${startDate})`);
879
+ }
880
+ }
881
+ nextPage = data.next_page;
882
+ if (nextPage) {
883
+ logger.debug(`Fetching next page: ${nextPage}`);
884
+ // Rate limiting: wait between requests
885
+ await new Promise(res => setTimeout(res, 1000));
886
+ }
887
+ }
888
+ logger.info(`Successfully processed ${processedArticles} of ${totalArticles} articles (filtered by date >= ${startDate})`);
889
+ }
496
890
  async createEmbeddings(texts) {
497
891
  const logger = this.logger.child('embeddings');
498
892
  try {
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.DatabaseManager = void 0;
40
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
41
+ const sqliteVec = __importStar(require("sqlite-vec"));
42
+ class DatabaseManager {
43
+ constructor(dbPath) {
44
+ // Cache counts for immediate UI feedback
45
+ this._trackedFilesCount = 0;
46
+ this._totalChunksCount = 0;
47
+ this.db = new better_sqlite3_1.default(dbPath);
48
+ sqliteVec.load(this.db);
49
+ this.createTables();
50
+ // Prepare statements for insert/update (matching doc2vec's approach)
51
+ this.insertStmt = this.db.prepare(`
52
+ INSERT INTO vec_items (embedding, version, heading_hierarchy, section, chunk_id, content, url, hash, chunk_index, total_chunks)
53
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
54
+ `);
55
+ this.updateStmt = this.db.prepare(`
56
+ UPDATE vec_items SET embedding = ?, version = ?, heading_hierarchy = ?, section = ?, content = ?, url = ?, hash = ?, chunk_index = ?, total_chunks = ?
57
+ WHERE chunk_id = ?
58
+ `);
59
+ // Initialize counts from database
60
+ this._trackedFilesCount = this._queryTrackedFilesCount();
61
+ this._totalChunksCount = this._queryTotalChunksCount();
62
+ console.log(`Database initialized at: ${dbPath}`);
63
+ }
64
+ _queryTrackedFilesCount() {
65
+ const stmt = this.db.prepare('SELECT COUNT(*) as count FROM files');
66
+ const row = stmt.get();
67
+ return Number(row.count);
68
+ }
69
+ _queryTotalChunksCount() {
70
+ const stmt = this.db.prepare('SELECT COUNT(*) as count FROM vec_items');
71
+ const row = stmt.get();
72
+ return Number(row.count);
73
+ }
74
+ createTables() {
75
+ // Create vec0 virtual table (sqlite-vec)
76
+ this.db.exec(`
77
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_items USING vec0(
78
+ embedding FLOAT[3072],
79
+ version TEXT,
80
+ heading_hierarchy TEXT,
81
+ section TEXT,
82
+ chunk_id TEXT UNIQUE,
83
+ content TEXT,
84
+ url TEXT,
85
+ hash TEXT,
86
+ chunk_index INTEGER,
87
+ total_chunks INTEGER
88
+ )
89
+ `);
90
+ // Create files tracking table
91
+ this.db.exec(`
92
+ CREATE TABLE IF NOT EXISTS files (
93
+ path TEXT PRIMARY KEY,
94
+ hash TEXT,
95
+ modified_at TEXT,
96
+ chunk_count INTEGER
97
+ )
98
+ `);
99
+ console.log('Database tables created');
100
+ }
101
+ insertChunk(chunk, embedding) {
102
+ const embeddingData = embedding ? new Float32Array(embedding) : new Float32Array(3072).fill(0);
103
+ // Use JSON.stringify for heading_hierarchy to match doc2vec format
104
+ const headingHierarchyJson = JSON.stringify(chunk.headingHierarchy);
105
+ try {
106
+ this.insertStmt.run(embeddingData, chunk.version, headingHierarchyJson, chunk.section, chunk.chunkId, chunk.content, chunk.url, chunk.hash, BigInt(chunk.chunkIndex), BigInt(chunk.totalChunks));
107
+ this._totalChunksCount++;
108
+ }
109
+ catch (error) {
110
+ // If insert fails due to UNIQUE constraint, update instead
111
+ if (error.code === 'SQLITE_CONSTRAINT_UNIQUE' || error.message?.includes('UNIQUE constraint failed')) {
112
+ this.updateStmt.run(embeddingData, chunk.version, headingHierarchyJson, chunk.section, chunk.content, chunk.url, chunk.hash, BigInt(chunk.chunkIndex), BigInt(chunk.totalChunks), chunk.chunkId);
113
+ // Update doesn't change count
114
+ }
115
+ else {
116
+ throw error;
117
+ }
118
+ }
119
+ }
120
+ removeChunksForFile(filePath) {
121
+ const url = `file://${filePath}`;
122
+ // Get count before delete
123
+ const countStmt = this.db.prepare('SELECT COUNT(*) as count FROM vec_items WHERE url = ?');
124
+ const countRow = countStmt.get(url);
125
+ const deletedCount = Number(countRow.count);
126
+ const stmt = this.db.prepare('DELETE FROM vec_items WHERE url = ?');
127
+ stmt.run(url);
128
+ this._totalChunksCount -= deletedCount;
129
+ }
130
+ getExistingChunkHash(chunkId) {
131
+ const stmt = this.db.prepare('SELECT hash FROM vec_items WHERE chunk_id = ?');
132
+ const row = stmt.get(chunkId);
133
+ return row?.hash ?? null;
134
+ }
135
+ getTotalChunksCount() {
136
+ return this._totalChunksCount;
137
+ }
138
+ upsertFileInfo(filePath, hash, modifiedAt, chunkCount) {
139
+ // Check if file already exists
140
+ const checkStmt = this.db.prepare('SELECT 1 FROM files WHERE path = ?');
141
+ const exists = checkStmt.get(filePath);
142
+ const stmt = this.db.prepare(`
143
+ INSERT OR REPLACE INTO files (path, hash, modified_at, chunk_count)
144
+ VALUES (?, ?, ?, ?)
145
+ `);
146
+ stmt.run(filePath, hash, modifiedAt.toISOString(), chunkCount);
147
+ // Only increment if this is a new file
148
+ if (!exists) {
149
+ this._trackedFilesCount++;
150
+ }
151
+ }
152
+ getFileInfo(filePath) {
153
+ const stmt = this.db.prepare('SELECT * FROM files WHERE path = ?');
154
+ const row = stmt.get(filePath);
155
+ if (!row)
156
+ return null;
157
+ return {
158
+ path: row.path,
159
+ hash: row.hash,
160
+ modifiedAt: new Date(row.modified_at),
161
+ chunkCount: row.chunk_count
162
+ };
163
+ }
164
+ removeFileInfo(filePath) {
165
+ // Check if file exists before deleting
166
+ const checkStmt = this.db.prepare('SELECT 1 FROM files WHERE path = ?');
167
+ const exists = checkStmt.get(filePath);
168
+ const stmt = this.db.prepare('DELETE FROM files WHERE path = ?');
169
+ stmt.run(filePath);
170
+ if (exists) {
171
+ this._trackedFilesCount--;
172
+ }
173
+ }
174
+ getAllTrackedFiles() {
175
+ const stmt = this.db.prepare('SELECT path FROM files');
176
+ const rows = stmt.all();
177
+ return rows.map(r => r.path);
178
+ }
179
+ getAllTrackedFilesWithInfo() {
180
+ const stmt = this.db.prepare('SELECT path, chunk_count, modified_at FROM files ORDER BY modified_at DESC');
181
+ const rows = stmt.all();
182
+ return rows.map(r => ({
183
+ path: r.path,
184
+ chunkCount: r.chunk_count,
185
+ modifiedAt: r.modified_at
186
+ }));
187
+ }
188
+ getChunksForFile(filePath) {
189
+ const url = `file://${filePath}`;
190
+ const stmt = this.db.prepare(`
191
+ SELECT chunk_id, content, section, chunk_index
192
+ FROM vec_items
193
+ WHERE url = ?
194
+ ORDER BY chunk_index
195
+ `);
196
+ const rows = stmt.all(url);
197
+ return rows.map(r => ({
198
+ chunkId: r.chunk_id,
199
+ content: r.content,
200
+ section: r.section,
201
+ chunkIndex: Number(r.chunk_index)
202
+ }));
203
+ }
204
+ getTrackedFilesCount() {
205
+ return this._trackedFilesCount;
206
+ }
207
+ clearAllData() {
208
+ this.db.exec('DELETE FROM vec_items');
209
+ this.db.exec('DELETE FROM files');
210
+ this._trackedFilesCount = 0;
211
+ this._totalChunksCount = 0;
212
+ console.log('Cleared all data');
213
+ }
214
+ close() {
215
+ this.db.close();
216
+ }
217
+ }
218
+ exports.DatabaseManager = DatabaseManager;