@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.
- package/README.md +95 -0
- package/SERVICE_README_sdk.md +399 -0
- package/agent-assets/guides/README.md +61 -0
- package/agent-assets/guides/dynamic-site-pattern.md +125 -0
- package/agent-assets/guides/knowledge-only-pattern.md +93 -0
- package/agent-assets/guides/microservice-pattern.md +152 -0
- package/agent-assets/guides/static-site-pattern.md +111 -0
- package/agent-assets/instructions/app-asset-validation.md +477 -0
- package/agent-assets/instructions/onboarding-flow.md +408 -0
- package/agent-assets/instructions/standard-folder-structure.md +125 -0
- package/agent-assets/reference/app-creation.md +381 -0
- package/agent-assets/reference/architecture.md +406 -0
- package/agent-assets/reference/local-dev.md +138 -0
- package/agent-assets/reference/mcp-integration.md +424 -0
- package/agent-assets/reference/workspace-config.md +415 -0
- package/agent-assets/rules/descix_mcp.template.mdc +243 -0
- package/bin/descix.js +3930 -0
- package/bin/mcp-server.js +382 -0
- package/descix-cli-1.0.0.tgz +0 -0
- package/lib/agent-files.js +162 -0
- package/lib/api-client.js +458 -0
- package/lib/auth-guard.js +67 -0
- package/lib/commands/app-wizard.js +627 -0
- package/lib/commands/auth.js +407 -0
- package/lib/commands/buy.js +207 -0
- package/lib/commands/clone.js +91 -0
- package/lib/commands/config.js +155 -0
- package/lib/commands/doctor.js +277 -0
- package/lib/commands/folder.js +291 -0
- package/lib/commands/init.js +257 -0
- package/lib/commands/kb.js +654 -0
- package/lib/commands/mcp.js +273 -0
- package/lib/commands/serve.js +12 -0
- package/lib/commands/status.js +239 -0
- package/lib/commands/update.js +653 -0
- package/lib/core/Chunker.js +610 -0
- package/lib/core/Hydrator.js +801 -0
- package/lib/core/PathContext.js +335 -0
- package/lib/core/Syncer.js +310 -0
- package/lib/core/index.js +32 -0
- package/lib/global-config.js +86 -0
- package/lib/google-storage-adc.js +511 -0
- package/lib/wallet-file.js +232 -0
- package/lib/wizard/setup.js +502 -0
- package/lib/workspace-config.js +712 -0
- package/lib/workspace-utils.js +126 -0
- package/package.json +41 -0
- package/templates/agent-claude.md +226 -0
- package/templates/agent-cline.md +133 -0
- package/templates/agent-copilot.md +133 -0
- package/templates/agent-cursor.md +133 -0
- package/templates/descix_mcp.mdc +122 -0
- package/templates/drive/README.md +42 -0
- package/templates/drive/app/assets/app_description.md +13 -0
- package/templates/drive/app/assets/icon.png +0 -0
- package/templates/drive/app/assets/system_instructions.md +17 -0
- package/templates/drive/app/kb/General/README.md +25 -0
- package/templates/drive/community/Apps/.gitkeep +4 -0
- package/templates/drive/community/community_assets/community_description.md +15 -0
- package/templates/drive/community/community_assets/icon.png +0 -0
- package/templates/drive_templates/README.md +104 -0
- package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
- package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
- package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
- package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
- package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
- package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
- package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
- package/templates/scaffolds/README.md +62 -0
- package/templates/scaffolds/microservice/Dockerfile +19 -0
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
- package/templates/scaffolds/microservice/app.js +50 -0
- package/templates/scaffolds/microservice/app.yaml +8 -0
- package/templates/scaffolds/microservice/defaults-config.json +5 -0
- package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
- package/templates/scaffolds/microservice/manifest.json +49 -0
- package/templates/scaffolds/microservice/package-lock.json +2645 -0
- package/templates/scaffolds/microservice/package.json +29 -0
- package/templates/scaffolds/microservice/scripts/register.js +55 -0
- package/templates/scaffolds/microservice/services/apiFront.js +75 -0
- package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
- package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
- package/templates/scaffolds/microservice/services/utils.js +247 -0
- package/templates/scaffolds/microservice/setup-schema.json +18 -0
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
- package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
- package/templates/scaffolds/site/README.md +51 -0
- package/templates/scaffolds/site/app.js +22 -0
- package/templates/scaffolds/site/index.html +42 -0
- package/templates/scaffolds/site/styles.css +123 -0
- package/test-hydration.js +150 -0
- package/vendor/mcp/transports/http-sse-transport.js +196 -0
- package/vendor/mcp/transports/index.js +15 -0
- 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
|
+
};
|