@descix/cli 1.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.
Files changed (94) hide show
  1. package/README.md +95 -0
  2. package/SERVICE_README_sdk.md +399 -0
  3. package/agent-assets/guides/README.md +61 -0
  4. package/agent-assets/guides/dynamic-site-pattern.md +125 -0
  5. package/agent-assets/guides/knowledge-only-pattern.md +93 -0
  6. package/agent-assets/guides/microservice-pattern.md +152 -0
  7. package/agent-assets/guides/static-site-pattern.md +111 -0
  8. package/agent-assets/instructions/app-asset-validation.md +477 -0
  9. package/agent-assets/instructions/onboarding-flow.md +408 -0
  10. package/agent-assets/instructions/standard-folder-structure.md +125 -0
  11. package/agent-assets/reference/app-creation.md +381 -0
  12. package/agent-assets/reference/architecture.md +406 -0
  13. package/agent-assets/reference/local-dev.md +138 -0
  14. package/agent-assets/reference/mcp-integration.md +424 -0
  15. package/agent-assets/reference/workspace-config.md +415 -0
  16. package/agent-assets/rules/descix_mcp.template.mdc +243 -0
  17. package/bin/descix.js +3930 -0
  18. package/bin/mcp-server.js +382 -0
  19. package/descix-cli-1.0.0.tgz +0 -0
  20. package/lib/agent-files.js +162 -0
  21. package/lib/api-client.js +458 -0
  22. package/lib/auth-guard.js +67 -0
  23. package/lib/commands/app-wizard.js +627 -0
  24. package/lib/commands/auth.js +407 -0
  25. package/lib/commands/buy.js +207 -0
  26. package/lib/commands/clone.js +91 -0
  27. package/lib/commands/config.js +155 -0
  28. package/lib/commands/doctor.js +277 -0
  29. package/lib/commands/folder.js +291 -0
  30. package/lib/commands/init.js +257 -0
  31. package/lib/commands/kb.js +654 -0
  32. package/lib/commands/mcp.js +273 -0
  33. package/lib/commands/serve.js +12 -0
  34. package/lib/commands/status.js +239 -0
  35. package/lib/commands/update.js +653 -0
  36. package/lib/core/Chunker.js +610 -0
  37. package/lib/core/Hydrator.js +801 -0
  38. package/lib/core/PathContext.js +335 -0
  39. package/lib/core/Syncer.js +310 -0
  40. package/lib/core/index.js +32 -0
  41. package/lib/global-config.js +86 -0
  42. package/lib/google-storage-adc.js +511 -0
  43. package/lib/wallet-file.js +232 -0
  44. package/lib/wizard/setup.js +502 -0
  45. package/lib/workspace-config.js +712 -0
  46. package/lib/workspace-utils.js +126 -0
  47. package/package.json +41 -0
  48. package/templates/agent-claude.md +226 -0
  49. package/templates/agent-cline.md +133 -0
  50. package/templates/agent-copilot.md +133 -0
  51. package/templates/agent-cursor.md +133 -0
  52. package/templates/descix_mcp.mdc +122 -0
  53. package/templates/drive/README.md +42 -0
  54. package/templates/drive/app/assets/app_description.md +13 -0
  55. package/templates/drive/app/assets/icon.png +0 -0
  56. package/templates/drive/app/assets/system_instructions.md +17 -0
  57. package/templates/drive/app/kb/General/README.md +25 -0
  58. package/templates/drive/community/Apps/.gitkeep +4 -0
  59. package/templates/drive/community/community_assets/community_description.md +15 -0
  60. package/templates/drive/community/community_assets/icon.png +0 -0
  61. package/templates/drive_templates/README.md +104 -0
  62. package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
  63. package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
  64. package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
  65. package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
  66. package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
  67. package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
  68. package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
  69. package/templates/scaffolds/README.md +62 -0
  70. package/templates/scaffolds/microservice/Dockerfile +19 -0
  71. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
  72. package/templates/scaffolds/microservice/app.js +50 -0
  73. package/templates/scaffolds/microservice/app.yaml +8 -0
  74. package/templates/scaffolds/microservice/defaults-config.json +5 -0
  75. package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
  76. package/templates/scaffolds/microservice/manifest.json +49 -0
  77. package/templates/scaffolds/microservice/package-lock.json +2645 -0
  78. package/templates/scaffolds/microservice/package.json +29 -0
  79. package/templates/scaffolds/microservice/scripts/register.js +55 -0
  80. package/templates/scaffolds/microservice/services/apiFront.js +75 -0
  81. package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
  82. package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
  83. package/templates/scaffolds/microservice/services/utils.js +247 -0
  84. package/templates/scaffolds/microservice/setup-schema.json +18 -0
  85. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
  86. package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
  87. package/templates/scaffolds/site/README.md +51 -0
  88. package/templates/scaffolds/site/app.js +22 -0
  89. package/templates/scaffolds/site/index.html +42 -0
  90. package/templates/scaffolds/site/styles.css +123 -0
  91. package/test-hydration.js +150 -0
  92. package/vendor/mcp/transports/http-sse-transport.js +196 -0
  93. package/vendor/mcp/transports/index.js +15 -0
  94. package/vendor/mcp/transports/stdio-transport.js +118 -0
@@ -0,0 +1,610 @@
1
+ /**
2
+ * Chunker - Canonical module for document chunking
3
+ *
4
+ * Responsibilities:
5
+ * - Chunk documents for RAG embedding
6
+ * - Support multiple chunking strategies (semantic, sliding window)
7
+ * - Generate chunk records with proper metadata for Pinecone
8
+ *
9
+ * Ported from: DeSciX_Cloud/services/chunkingUtils.js
10
+ *
11
+ * Architecture:
12
+ * - All chunking happens locally in the SDK
13
+ * - Chunks are saved as JSON files in kb/chunks/
14
+ * - Pinecone handles embedding server-side via kb_sync_chunks API
15
+ *
16
+ * Chunk ID Format (v2.1 - Multi-tenancy):
17
+ * - Composite: ${community_id}:${app_id}:${kb_id}:${file_id}:${chunk_idx}
18
+ * - file_id: Drive ID for Drive-backed files, "local:${hash}" for local-only
19
+ * - chunk_idx: Sequential number (not string) for contiguous retrieval
20
+ */
21
+
22
+ import * as fs from 'fs/promises';
23
+ import * as path from 'path';
24
+ import { createHash } from 'crypto';
25
+
26
+ // Default chunking parameters (from server)
27
+ const DEFAULT_MAX_CHUNK_SIZE = 2000;
28
+ const DEFAULT_MAX_CODE_FILE_SIZE = 8000;
29
+ const DEFAULT_OVERLAP_SIZE = 500;
30
+
31
+ /**
32
+ * Categorize file by name and MIME type
33
+ * Determines chunking strategy
34
+ *
35
+ * @param {string} fileName - File name
36
+ * @param {string} mimeType - MIME type (optional)
37
+ * @returns {'code'|'docs'|'papers'|'generic'}
38
+ */
39
+ export function categorizeFile(fileName, mimeType = null) {
40
+ const name = (fileName || '').toLowerCase();
41
+
42
+ // Check by extension
43
+ if (name.endsWith('.js') || name.endsWith('.py') || name.endsWith('.ts') ||
44
+ name.endsWith('.jsx') || name.endsWith('.tsx') || name.endsWith('.lean') ||
45
+ name.endsWith('.sol') || name.endsWith('.rs') || name.endsWith('.go')) {
46
+ return 'code';
47
+ }
48
+ if (name.endsWith('.md')) {
49
+ return 'docs';
50
+ }
51
+ if (name.endsWith('.tex')) {
52
+ return 'papers';
53
+ }
54
+
55
+ // Check by MIME type
56
+ if (mimeType === 'application/vnd.google-apps.document' ||
57
+ mimeType === 'text/markdown' ||
58
+ mimeType === 'text/plain') {
59
+ return 'docs';
60
+ }
61
+
62
+ return 'generic';
63
+ }
64
+
65
+ /**
66
+ * Chunk code files
67
+ * Optimizes for embedding by keeping logical units together
68
+ *
69
+ * @param {string} content - File content
70
+ * @param {Object} fileMetadata - { name }
71
+ * @param {Object} rules - { maxCodeFileSize }
72
+ * @returns {Array<{content, metadata}>}
73
+ */
74
+ function chunkCode(content, fileMetadata, rules) {
75
+ const maxSize = rules.maxCodeFileSize || DEFAULT_MAX_CODE_FILE_SIZE;
76
+
77
+ // If small enough, keep as single chunk
78
+ if (content.length <= maxSize) {
79
+ return [{
80
+ content,
81
+ metadata: {
82
+ chunkIndex: 0,
83
+ totalChunks: 1,
84
+ type: 'full-file',
85
+ fileName: fileMetadata.name
86
+ }
87
+ }];
88
+ }
89
+
90
+ // For large files, split by logical boundaries
91
+ return splitByLogicalBoundaries(content, fileMetadata, rules);
92
+ }
93
+
94
+ /**
95
+ * Split code by logical boundaries (functions, classes, etc.)
96
+ */
97
+ function splitByLogicalBoundaries(content, fileMetadata, rules) {
98
+ const chunks = [];
99
+ const lines = content.split('\n');
100
+ let currentChunk = '';
101
+ let currentName = 'Module';
102
+ const maxSize = rules.maxCodeFileSize || DEFAULT_MAX_CODE_FILE_SIZE;
103
+
104
+ // Patterns for logical boundaries
105
+ const patterns = [
106
+ /^(theorem|lemma|def|definition|structure|class|instance)\s+(\w+)/,
107
+ /^(function|const|let|var)\s+(\w+)/,
108
+ /^(export\s+)?(async\s+)?function\s+(\w+)/,
109
+ /^(export\s+)?(class)\s+(\w+)/,
110
+ /^(contract|interface)\s+(\w+)/ // Solidity
111
+ ];
112
+
113
+ for (const line of lines) {
114
+ let foundBoundary = false;
115
+
116
+ for (const pattern of patterns) {
117
+ const match = line.match(pattern);
118
+ if (match && currentChunk.length > 1000) {
119
+ chunks.push({
120
+ content: currentChunk.trim(),
121
+ metadata: {
122
+ chunkIndex: chunks.length,
123
+ name: currentName,
124
+ type: 'code-block',
125
+ fileName: fileMetadata.name
126
+ }
127
+ });
128
+ currentChunk = line + '\n';
129
+ currentName = match[match.length - 1]; // Last group is name
130
+ foundBoundary = true;
131
+ break;
132
+ }
133
+ }
134
+
135
+ if (!foundBoundary) {
136
+ currentChunk += line + '\n';
137
+ }
138
+
139
+ // Force split if too large
140
+ if (currentChunk.length > maxSize) {
141
+ chunks.push({
142
+ content: currentChunk.trim(),
143
+ metadata: {
144
+ chunkIndex: chunks.length,
145
+ name: currentName,
146
+ type: 'code-block',
147
+ fileName: fileMetadata.name
148
+ }
149
+ });
150
+ currentChunk = '';
151
+ }
152
+ }
153
+
154
+ // Add final chunk
155
+ if (currentChunk.trim()) {
156
+ chunks.push({
157
+ content: currentChunk.trim(),
158
+ metadata: {
159
+ chunkIndex: chunks.length,
160
+ name: currentName,
161
+ type: 'code-block',
162
+ fileName: fileMetadata.name
163
+ }
164
+ });
165
+ }
166
+
167
+ // Update total chunks count
168
+ chunks.forEach(chunk => {
169
+ chunk.metadata.totalChunks = chunks.length;
170
+ });
171
+
172
+ return chunks;
173
+ }
174
+
175
+ /**
176
+ * Chunk documentation using markdown-aware section-based chunking
177
+ *
178
+ * @param {string} content - Document content
179
+ * @param {Object} fileMetadata - { name }
180
+ * @param {Object} rules - { maxChunkSize, overlapSize }
181
+ * @returns {Array<{content, metadata}>}
182
+ */
183
+ export function chunkDocument(content, fileMetadata, rules) {
184
+ const maxSize = rules.maxChunkSize || DEFAULT_MAX_CHUNK_SIZE;
185
+
186
+ // If small enough, keep as single chunk
187
+ if (content.length <= maxSize) {
188
+ return [{
189
+ content,
190
+ metadata: {
191
+ chunkIndex: 0,
192
+ totalChunks: 1,
193
+ type: 'full-document',
194
+ fileName: fileMetadata.name
195
+ }
196
+ }];
197
+ }
198
+
199
+ // Use markdown section-based chunking
200
+ return chunkByMarkdownSections(content, fileMetadata, rules);
201
+ }
202
+
203
+ /**
204
+ * Split content by markdown sections (headers)
205
+ */
206
+ function chunkByMarkdownSections(content, fileMetadata, rules) {
207
+ const chunks = [];
208
+ const lines = content.split('\n');
209
+ let currentChunk = '';
210
+ let currentTitle = 'Introduction';
211
+ const maxSize = rules.maxChunkSize || DEFAULT_MAX_CHUNK_SIZE;
212
+ const overlap = rules.overlapSize || DEFAULT_OVERLAP_SIZE;
213
+
214
+ for (let i = 0; i < lines.length; i++) {
215
+ const line = lines[i];
216
+
217
+ // Detect markdown headers (# ## ###)
218
+ const headerMatch = line.match(/^(#{1,3})\s+(.+)$/);
219
+
220
+ if (headerMatch && currentChunk.length > 500) {
221
+ // Save current chunk
222
+ chunks.push({
223
+ content: currentChunk.trim(),
224
+ metadata: {
225
+ chunkIndex: chunks.length,
226
+ title: currentTitle,
227
+ type: 'section',
228
+ fileName: fileMetadata.name
229
+ }
230
+ });
231
+
232
+ // Start new chunk with overlap from end of previous
233
+ const overlapText = currentChunk.slice(-overlap);
234
+ currentChunk = overlapText + '\n\n' + line;
235
+ currentTitle = headerMatch[2];
236
+ } else {
237
+ currentChunk += line + '\n';
238
+
239
+ // If chunk too large, force split
240
+ if (currentChunk.length > maxSize) {
241
+ chunks.push({
242
+ content: currentChunk.trim(),
243
+ metadata: {
244
+ chunkIndex: chunks.length,
245
+ title: currentTitle,
246
+ type: 'section',
247
+ fileName: fileMetadata.name
248
+ }
249
+ });
250
+ // Only keep overlap if it's less than current content
251
+ currentChunk = currentChunk.length > overlap ? currentChunk.slice(-overlap) : '';
252
+ }
253
+ }
254
+ }
255
+
256
+ // Add final chunk
257
+ if (currentChunk.trim()) {
258
+ chunks.push({
259
+ content: currentChunk.trim(),
260
+ metadata: {
261
+ chunkIndex: chunks.length,
262
+ title: currentTitle,
263
+ type: 'section',
264
+ fileName: fileMetadata.name
265
+ }
266
+ });
267
+ }
268
+
269
+ // Update total chunks count
270
+ chunks.forEach(chunk => {
271
+ chunk.metadata.totalChunks = chunks.length;
272
+ });
273
+
274
+ return chunks;
275
+ }
276
+
277
+ /**
278
+ * Generic chunking using sliding window
279
+ * Fallback for file types without semantic structure
280
+ *
281
+ * @param {string} content - File content
282
+ * @param {Object} fileMetadata - { name }
283
+ * @param {Object} rules - { maxChunkSize, overlapSize }
284
+ * @returns {Array<{content, metadata}>}
285
+ */
286
+ function chunkGeneric(content, fileMetadata, rules) {
287
+ const maxSize = rules.maxChunkSize || DEFAULT_MAX_CHUNK_SIZE;
288
+ const overlap = rules.overlapSize || DEFAULT_OVERLAP_SIZE;
289
+ const chunks = [];
290
+
291
+ let start = 0;
292
+ while (start < content.length) {
293
+ const end = Math.min(start + maxSize, content.length);
294
+ const chunk = content.slice(start, end);
295
+
296
+ chunks.push({
297
+ content: chunk,
298
+ metadata: {
299
+ chunkIndex: chunks.length,
300
+ type: 'generic',
301
+ fileName: fileMetadata.name
302
+ }
303
+ });
304
+
305
+ // Calculate next start position with overlap
306
+ // Ensure forward progress to prevent infinite loop
307
+ const nextStart = end - overlap;
308
+ if (nextStart <= start || end >= content.length) {
309
+ break;
310
+ }
311
+ start = nextStart;
312
+ }
313
+
314
+ // Update total chunks count
315
+ chunks.forEach(chunk => {
316
+ chunk.metadata.totalChunks = chunks.length;
317
+ });
318
+
319
+ return chunks;
320
+ }
321
+
322
+ /**
323
+ * Chunk a file's content based on its type
324
+ *
325
+ * @param {Object} fileInfo - { file_name, mime_type, content }
326
+ * @param {Object} options - { maxChunkSize, maxCodeFileSize, overlapSize }
327
+ * @returns {Array<{content, metadata}>}
328
+ */
329
+ export function chunkFile(fileInfo, options = {}) {
330
+ const { file_name, mime_type, content } = fileInfo;
331
+ const fileCategory = categorizeFile(file_name, mime_type);
332
+
333
+ const rules = {
334
+ maxChunkSize: options.maxChunkSize || DEFAULT_MAX_CHUNK_SIZE,
335
+ maxCodeFileSize: options.maxCodeFileSize || DEFAULT_MAX_CODE_FILE_SIZE,
336
+ overlapSize: options.overlapSize || DEFAULT_OVERLAP_SIZE
337
+ };
338
+
339
+ switch (fileCategory) {
340
+ case 'code':
341
+ return chunkCode(content, { name: file_name }, rules);
342
+ case 'docs':
343
+ case 'papers':
344
+ return chunkDocument(content, { name: file_name }, rules);
345
+ default:
346
+ return chunkGeneric(content, { name: file_name }, rules);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Compute SHA256 hash of content
352
+ * @param {string} content - Content to hash
353
+ * @returns {string} Hash prefixed with "sha256:"
354
+ */
355
+ function computeContentHash(content) {
356
+ return `sha256:${createHash('sha256').update(content).digest('hex')}`;
357
+ }
358
+
359
+ /**
360
+ * Generate a local file ID from relative path
361
+ * Used for files that don't have a Drive ID (local-only)
362
+ * @param {string} relativePath - Relative path from app root
363
+ * @returns {string} Local file ID prefixed with "local:"
364
+ */
365
+ function generateLocalFileId(relativePath) {
366
+ const hash = createHash('sha256').update(relativePath).digest('hex').substring(0, 16);
367
+ return `local:${hash}`;
368
+ }
369
+
370
+ /**
371
+ * Create a chunk record formatted for Pinecone sync
372
+ *
373
+ * ID Format: ${community_id}:${app_id}:${kb_id}:${file_id}:${chunk_idx}
374
+ * This composite format ensures multi-tenancy isolation and supports
375
+ * the same file appearing in multiple KBs/Apps.
376
+ *
377
+ * @param {Object} chunk - { content, metadata }
378
+ * @param {Object} context - { community_id, app_id, kb_id, file_id, file_name }
379
+ * @returns {Object} Pinecone-ready chunk record
380
+ */
381
+ function createChunkRecord(chunk, context) {
382
+ const { community_id, app_id, kb_id, file_id, file_name } = context;
383
+ const chunk_idx = chunk.metadata.chunkIndex;
384
+ const text = chunk.content.trim();
385
+
386
+ // Composite ID — app_id is globally unique, no community_id prefix needed
387
+ const id = `${app_id}:${kb_id}:${file_id}:${chunk_idx}`;
388
+
389
+ return {
390
+ id,
391
+ text,
392
+ entity_type: 'CHUNK',
393
+ // Multi-tenancy fields (ALL REQUIRED for filtering)
394
+ community_id,
395
+ app_id,
396
+ knowledgebase_name: kb_id,
397
+ file_id, // Drive ID or "local:hash"
398
+ chunk_idx, // Number for contiguous retrieval
399
+ // Document metadata
400
+ file_name,
401
+ mime_type: 'text/markdown',
402
+ total_chunks: chunk.metadata.totalChunks,
403
+ // Optional content metadata
404
+ chunk_type: chunk.metadata.type,
405
+ chunk_title: chunk.metadata.title || chunk.metadata.name || null,
406
+ content_hash: computeContentHash(text) // For change detection
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Load .descix_metadata.json for a KB directory
412
+ * Returns a map of filename -> drive_id (or null if not found)
413
+ *
414
+ * @param {string} kbDir - Path to KB directory (e.g., kb/General)
415
+ * @returns {Promise<Object>} Map of filename -> { drive_id, ... }
416
+ */
417
+ async function loadKbMetadata(kbDir) {
418
+ const metadataPath = path.join(kbDir, '.descix_metadata.json');
419
+ try {
420
+ const content = await fs.readFile(metadataPath, 'utf-8');
421
+ const metadata = JSON.parse(content);
422
+ return metadata.files || {};
423
+ } catch {
424
+ // No metadata file - all files are local-only
425
+ return {};
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Get file_id for a file - Drive ID if available, otherwise local:hash
431
+ *
432
+ * @param {string} fileName - Local filename
433
+ * @param {Object} fileMetadata - Metadata entry from .descix_metadata.json
434
+ * @param {string} kbId - Knowledge base ID
435
+ * @returns {string} file_id (Drive ID or "local:hash")
436
+ */
437
+ function getFileId(fileName, fileMetadata, kbId) {
438
+ if (fileMetadata && fileMetadata.drive_id) {
439
+ return fileMetadata.drive_id;
440
+ }
441
+ // Generate local ID from relative path within KB
442
+ const relativePath = `kb/${kbId}/${fileName}`;
443
+ return generateLocalFileId(relativePath);
444
+ }
445
+
446
+ /**
447
+ * Process an entire KB directory and generate chunks
448
+ *
449
+ * Uses .descix_metadata.json to get Drive IDs for files.
450
+ * Files without Drive IDs get a local:hash ID for tracking.
451
+ *
452
+ * @param {Object} config - { workspaceRoot, communityId, appId, kbId, localPath }
453
+ * @param {Object} options - { maxChunkSize, overlapSize, verbose }
454
+ * @returns {Promise<{files: number, totalChunks: number}>}
455
+ */
456
+ export async function processKb(config, options = {}) {
457
+ const { workspaceRoot, communityId, appId, kbId = 'General', localPath } = config;
458
+ const { verbose = false } = options;
459
+
460
+ // Find source directory (kb/General for converted files)
461
+ const appLocalPath = localPath || `${communityId}/${appId}`;
462
+ const srcDir = path.join(workspaceRoot, appLocalPath, 'kb', kbId);
463
+ const chunksDir = path.join(workspaceRoot, appLocalPath, 'kb', 'chunks');
464
+
465
+ // Check if source directory exists
466
+ try {
467
+ await fs.access(srcDir);
468
+ } catch {
469
+ throw new Error(`Source directory not found: ${srcDir}\nRun "descix kb pull" first.`);
470
+ }
471
+
472
+ await fs.mkdir(chunksDir, { recursive: true });
473
+
474
+ // Load metadata to get Drive IDs
475
+ const kbMetadata = await loadKbMetadata(srcDir);
476
+
477
+ // Read files (exclude metadata and hidden files)
478
+ const files = await fs.readdir(srcDir);
479
+ const textFiles = files.filter(f =>
480
+ !f.startsWith('.') &&
481
+ (f.endsWith('.md') || f.endsWith('.txt') || f.endsWith('.csv'))
482
+ );
483
+
484
+ if (textFiles.length === 0) {
485
+ return { files: 0, totalChunks: 0 };
486
+ }
487
+
488
+ let totalChunks = 0;
489
+
490
+ for (const fileName of textFiles) {
491
+ try {
492
+ const filePath = path.join(srcDir, fileName);
493
+ const content = await fs.readFile(filePath, 'utf-8');
494
+
495
+ // Get file_id from metadata (Drive ID) or generate local ID
496
+ const fileMetadata = kbMetadata[fileName];
497
+ const file_id = getFileId(fileName, fileMetadata, kbId);
498
+ const isLocal = file_id.startsWith('local:');
499
+
500
+ // Get MIME type from extension
501
+ let mimeType = 'text/plain';
502
+ if (fileName.endsWith('.md')) mimeType = 'text/markdown';
503
+ else if (fileName.endsWith('.csv')) mimeType = 'text/csv';
504
+
505
+ // Generate chunks
506
+ const rawChunks = chunkFile({ file_name: fileName, mime_type: mimeType, content }, options);
507
+
508
+ // Convert to Pinecone-ready records
509
+ const chunks = rawChunks.map(chunk =>
510
+ createChunkRecord(chunk, {
511
+ community_id: communityId,
512
+ app_id: appId,
513
+ kb_id: kbId,
514
+ file_id,
515
+ file_name: fileName
516
+ })
517
+ );
518
+
519
+ // Write chunks file (v2.1 format with source_drive_id)
520
+ const baseName = fileName.replace(/\.[^.]+$/, '');
521
+ const chunksFile = {
522
+ version: '2.1',
523
+ source_file: fileName,
524
+ source_drive_id: isLocal ? null : file_id,
525
+ source_kb: kbId,
526
+ generated_at: new Date().toISOString(),
527
+ chunking_config: {
528
+ strategy: 'semantic',
529
+ max_chunk_size: options.maxChunkSize || DEFAULT_MAX_CHUNK_SIZE,
530
+ overlap_size: options.overlapSize || DEFAULT_OVERLAP_SIZE
531
+ },
532
+ chunks
533
+ };
534
+
535
+ const chunksPath = path.join(chunksDir, `${baseName}.chunks.json`);
536
+ await fs.writeFile(chunksPath, JSON.stringify(chunksFile, null, 2));
537
+
538
+ totalChunks += chunks.length;
539
+
540
+ if (verbose) {
541
+ const idType = isLocal ? '(local)' : '(drive)';
542
+ console.log(` ✓ ${fileName} ${idType}: ${chunks.length} chunks`);
543
+ }
544
+ } catch (error) {
545
+ if (verbose) {
546
+ console.log(` ✗ ${fileName}: ${error.message}`);
547
+ }
548
+ }
549
+ }
550
+
551
+ return { files: textFiles.length, totalChunks };
552
+ }
553
+
554
+ /**
555
+ * Load all chunks from the chunks directory
556
+ *
557
+ * @param {Object} config - { workspaceRoot, communityId, appId, kbId, localPath }
558
+ * @returns {Promise<Array>} All chunk records
559
+ */
560
+ export async function loadChunks(config) {
561
+ const { workspaceRoot, communityId, appId, kbId = 'General', localPath } = config;
562
+
563
+ const appLocalPath = localPath || `${communityId}/${appId}`;
564
+ const chunksDir = path.join(workspaceRoot, appLocalPath, 'kb', 'chunks');
565
+
566
+ try {
567
+ await fs.access(chunksDir);
568
+ } catch {
569
+ return [];
570
+ }
571
+
572
+ const files = await fs.readdir(chunksDir);
573
+ const chunkFiles = files.filter(f => f.endsWith('.chunks.json'));
574
+
575
+ const allChunks = [];
576
+
577
+ for (const fileName of chunkFiles) {
578
+ try {
579
+ const filePath = path.join(chunksDir, fileName);
580
+ const content = await fs.readFile(filePath, 'utf-8');
581
+ const chunkFile = JSON.parse(content);
582
+
583
+ // Filter chunks for this specific KB
584
+ const kbChunks = (chunkFile.chunks || []).filter(c =>
585
+ c.community_id === communityId &&
586
+ c.app_id === appId &&
587
+ c.knowledgebase_name === kbId
588
+ );
589
+
590
+ allChunks.push(...kbChunks);
591
+ } catch {
592
+ // Skip invalid files
593
+ }
594
+ }
595
+
596
+ return allChunks;
597
+ }
598
+
599
+ // Export helper functions for external use
600
+ export { computeContentHash, generateLocalFileId };
601
+
602
+ export default {
603
+ categorizeFile,
604
+ chunkFile,
605
+ chunkDocument,
606
+ processKb,
607
+ loadChunks,
608
+ computeContentHash,
609
+ generateLocalFileId
610
+ };