vzcode 2.2.0 → 2.4.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 (47) hide show
  1. package/dist/assets/{buildWorker-Dsx6rOdK.js → buildWorker-wLkQ0yz1.js} +64 -64
  2. package/dist/assets/{index-BM8tDh6M.css → index-BoFeK4GL.css} +1 -1
  3. package/dist/assets/{index-CpxqGhXj.js → index-DuUjtrt6.js} +150 -150
  4. package/dist/assets/{worker-BeBZGG1n.js → worker-DHxSPM7N.js} +78 -78
  5. package/dist/cli.js +113 -0
  6. package/dist/index.html +2 -2
  7. package/dist/ot.js +12 -0
  8. package/dist/randomId.js +14 -0
  9. package/dist/runCode.js +20 -0
  10. package/dist/server/aiChatHandler/aiEditing.js +89 -0
  11. package/dist/server/aiChatHandler/chatOperations.js +360 -0
  12. package/dist/server/aiChatHandler/errorHandling.js +49 -0
  13. package/dist/server/aiChatHandler/index.js +112 -0
  14. package/dist/server/aiChatHandler/llmStreaming.js +284 -0
  15. package/dist/server/aiChatHandler/validation.js +19 -0
  16. package/dist/server/computeInitialDocument.js +146 -0
  17. package/dist/server/config.js +21 -0
  18. package/dist/server/featureFlags.js +5 -0
  19. package/dist/server/generateAIResponse.js +129 -0
  20. package/dist/server/handleAIAssist.js +39 -0
  21. package/dist/server/handleAIChatMessage.js +2 -0
  22. package/dist/server/handleAICopilot.js +81 -0
  23. package/dist/server/index.js +297 -0
  24. package/dist/server/isDirectory.js +1 -0
  25. package/dist/server/livekit.js +17 -0
  26. package/dist/server/prettier.js +90 -0
  27. package/dist/server/setupEnv.js +7 -0
  28. package/dist/submitOperation.js +14 -0
  29. package/dist/types.js +1 -0
  30. package/dist/utils/fileDiff.js +66 -0
  31. package/package.json +21 -20
  32. package/src/cli.ts +208 -0
  33. package/src/client/App/index.tsx +0 -1
  34. package/src/client/VZCodeContext/types.ts +3 -4
  35. package/src/client/VZCodeContext/useVZCodeState.ts +24 -6
  36. package/src/client/VZSidebar/AIChat/ChatInput.tsx +4 -0
  37. package/src/client/VZSidebar/AIChat/Message.tsx +2 -66
  38. package/src/client/VZSidebar/AIChat/MessageList.tsx +0 -15
  39. package/src/client/VZSidebar/AIChat/index.tsx +91 -7
  40. package/src/client/VZSidebar/AIChat/styles.scss +19 -0
  41. package/src/server/aiChatHandler/aiEditing.ts +0 -1
  42. package/src/server/aiChatHandler/chatOperations.ts +0 -34
  43. package/src/server/aiChatHandler/index.ts +0 -1
  44. package/src/server/aiChatHandler/llmStreaming.ts +3 -3
  45. package/src/server/index.ts +1 -10
  46. package/src/server/aiChatHandler/undoHandler.ts +0 -97
  47. package/src/server/handleAIChatUndo.ts +0 -2
@@ -0,0 +1,112 @@
1
+ import { validateRequest } from './validation.js';
2
+ import { ensureChatsExist, ensureChatExists, addUserMessage, addDiffToAIMessage, setAIStatus, } from './chatOperations.js';
3
+ import { createLLMFunction } from './llmStreaming.js';
4
+ import { performAIEditing, performAIChat, } from './aiEditing.js';
5
+ import { handleError, handleBackgroundError, } from './errorHandling.js';
6
+ import { createRunCodeFunction } from '../../runCode.js';
7
+ import { createSubmitOperation } from '../../submitOperation.js';
8
+ const DEBUG = false;
9
+ export const handleAIChatMessage = ({ shareDBDoc, createAIEditLocalPresence, onCreditDeduction, getCurrentCommitId, model, aiRequestOptions, }) => async (req, res) => {
10
+ const { content, chatId, mode = 'edit' } = req.body;
11
+ if (DEBUG) {
12
+ console.log('[handleAIChatMessage] content:', content, 'chatId:', chatId, 'shareDBDoc:', shareDBDoc);
13
+ }
14
+ // Validate request
15
+ if (!validateRequest(req, res)) {
16
+ return;
17
+ }
18
+ try {
19
+ // Ensure chats structure exists
20
+ ensureChatsExist(shareDBDoc);
21
+ ensureChatExists(shareDBDoc, chatId);
22
+ // Add user message to chat
23
+ addUserMessage(shareDBDoc, chatId, content);
24
+ // Return success immediately - AI generation continues in background
25
+ res.status(200).json('success');
26
+ // Set AI status to indicate generation is starting
27
+ setAIStatus(shareDBDoc, chatId, 'generating');
28
+ // Continue AI processing in background (don't await)
29
+ processAIRequestAsync({
30
+ shareDBDoc,
31
+ chatId,
32
+ content,
33
+ mode,
34
+ createAIEditLocalPresence,
35
+ getCurrentCommitId,
36
+ model,
37
+ aiRequestOptions,
38
+ onCreditDeduction,
39
+ }).catch((error) => {
40
+ console.error('Background AI processing error:', error);
41
+ // Handle error without HTTP response
42
+ handleBackgroundError(shareDBDoc, chatId, error);
43
+ });
44
+ }
45
+ catch (error) {
46
+ handleError(shareDBDoc, chatId, error, res);
47
+ }
48
+ };
49
+ /**
50
+ * Processes the AI request asynchronously in the background
51
+ */
52
+ const processAIRequestAsync = async ({ shareDBDoc, chatId, content, mode, createAIEditLocalPresence, getCurrentCommitId, model, aiRequestOptions, onCreditDeduction, }) => {
53
+ try {
54
+ // Capture the current commit ID before making changes (for VizHub integration)
55
+ const beforeCommitId = getCurrentCommitId
56
+ ? getCurrentCommitId()
57
+ : null;
58
+ // Create LLM function for streaming
59
+ const llmFunction = createLLMFunction({
60
+ shareDBDoc,
61
+ createAIEditLocalPresence,
62
+ chatId,
63
+ model,
64
+ aiRequestOptions,
65
+ });
66
+ // Create server-side runCode function using shareDBDoc
67
+ const submitOperation = createSubmitOperation(shareDBDoc);
68
+ const runCode = createRunCodeFunction(submitOperation);
69
+ // Perform AI editing or chat based on mode
70
+ const editResult = mode === 'ask'
71
+ ? await performAIChat({
72
+ prompt: content,
73
+ shareDBDoc,
74
+ llmFunction,
75
+ })
76
+ : await performAIEditing({
77
+ prompt: content,
78
+ shareDBDoc,
79
+ llmFunction,
80
+ runCode,
81
+ });
82
+ // Add diff data to the AI message if there are changes
83
+ if (editResult.diffData &&
84
+ Object.keys(editResult.diffData).length > 0) {
85
+ addDiffToAIMessage(shareDBDoc, chatId, editResult.diffData, beforeCommitId);
86
+ }
87
+ // Handle credit deduction if callback is provided
88
+ if (onCreditDeduction &&
89
+ editResult.upstreamCostCents) {
90
+ try {
91
+ await onCreditDeduction({
92
+ upstreamCostCents: editResult
93
+ .upstreamCostCents,
94
+ provider: editResult.provider,
95
+ inputTokens: editResult.inputTokens,
96
+ outputTokens: editResult.outputTokens,
97
+ });
98
+ }
99
+ catch (creditError) {
100
+ console.error('Credit deduction error:', creditError);
101
+ // Don't fail the request if credit deduction fails
102
+ }
103
+ }
104
+ // Clear the AI status to indicate completion
105
+ setAIStatus(shareDBDoc, chatId, undefined);
106
+ }
107
+ catch (error) {
108
+ // Set error status and add error message to chat
109
+ setAIStatus(shareDBDoc, chatId, 'error');
110
+ handleBackgroundError(shareDBDoc, chatId, error);
111
+ }
112
+ };
@@ -0,0 +1,284 @@
1
+ import { parseMarkdownFiles, StreamingMarkdownParser, } from 'llm-code-format';
2
+ import OpenAI from 'openai';
3
+ import fs from 'fs';
4
+ import { generateRunId } from '@vizhub/viz-utils';
5
+ import { updateAIStatus, createAIMessage, updateAIMessageContent, finalizeAIMessage, ensureFileExists, clearFileContent, appendLineToFile, updateFiles, updateAIScratchpad, } from './chatOperations.js';
6
+ import { mergeFileChanges } from 'editcodewithai';
7
+ import { diff } from '../../ot.js';
8
+ const DEBUG = false;
9
+ // Useful for testing/debugging the streaming behavior
10
+ const slowMode = false;
11
+ // Throttle the streaming updates, so that we don't
12
+ // overwhelm the ShareDB server with too many updates.
13
+ // It happened actually, before adding this.
14
+ // MongoDB VizHub server got in fact overloaded with
15
+ // too many updates from the AI streaming response, with
16
+ // warning: "Replication Oplog Window has gone below 1 hour"
17
+ const THROTTLE_INTERVAL_MS = 100;
18
+ // Feature flag to enable/disable streaming editing.
19
+ // * If `true`, the AI streaming response will be used to
20
+ // edit files in real-time by submitting ShareDB ops.
21
+ // * If `false`, the updates to code files will be applied
22
+ // only after the AI has finished generating the entire response.
23
+ //
24
+ // Current status: there's a tricky bug with the streaming
25
+ // where the AI edits sometimes don't apply correctly in CodeMirror.
26
+ // It seems that sometimes the op that clears the file content
27
+ // is not applied correctly in the front end, leading to
28
+ // a situation where the AI streaming edits are concatenated into the middle
29
+ // of the file instead of replacing it.
30
+ // See https://github.com/codemirror/codemirror.next/issues/1234
31
+ const enableStreamingEditing = false;
32
+ /**
33
+ * Creates and configures the LLM function for streaming with reasoning tokens
34
+ */
35
+ export const createLLMFunction = ({ shareDBDoc, createAIEditLocalPresence, chatId,
36
+ // Feature flag to enable/disable reasoning tokens.
37
+ // When false, reasoning tokens are not requested from the API
38
+ // and reasoning content is not processed in the streaming response.
39
+ enableReasoningTokens = false, model, aiRequestOptions, }) => {
40
+ return async (fullPrompt) => {
41
+ const localPresence = enableStreamingEditing
42
+ ? createAIEditLocalPresence()
43
+ : null;
44
+ // Create OpenRouter client for reasoning token support
45
+ const openRouterClient = new OpenAI({
46
+ apiKey: process.env.VZCODE_EDIT_WITH_AI_API_KEY,
47
+ baseURL: process.env.VZCODE_EDIT_WITH_AI_BASE_URL ||
48
+ 'https://openrouter.ai/api/v1',
49
+ defaultHeaders: {
50
+ 'HTTP-Referer': 'https://vizhub.com',
51
+ 'X-Title': 'VizHub',
52
+ },
53
+ });
54
+ let fullContent = '';
55
+ let generationId = '';
56
+ let currentEditingFileId = null;
57
+ let currentEditingFileName = null;
58
+ // Create initial AI message for streaming
59
+ const aiMessageId = createAIMessage(shareDBDoc, chatId);
60
+ // --- throttle wrapper ---
61
+ function makeThrottledUpdater() {
62
+ let lastCall = 0;
63
+ let latestContent = '';
64
+ let timer = null;
65
+ function invoke() {
66
+ updateAIMessageContent(shareDBDoc, chatId, aiMessageId, latestContent);
67
+ lastCall = Date.now();
68
+ }
69
+ const fn = (content) => {
70
+ latestContent = content;
71
+ const now = Date.now();
72
+ if (now - lastCall >= THROTTLE_INTERVAL_MS) {
73
+ // safe to call immediately
74
+ invoke();
75
+ }
76
+ else if (!timer) {
77
+ // schedule for later
78
+ timer = setTimeout(() => {
79
+ timer = null;
80
+ invoke();
81
+ }, THROTTLE_INTERVAL_MS - (now - lastCall));
82
+ }
83
+ };
84
+ // expose a flush() helper to force an immediate write
85
+ fn.flush = () => {
86
+ if (timer) {
87
+ clearTimeout(timer);
88
+ timer = null;
89
+ }
90
+ invoke();
91
+ };
92
+ return fn;
93
+ }
94
+ const throttledUpdateAIMessageContent = makeThrottledUpdater();
95
+ // Function to report file edited
96
+ // This is called when the AI has finished editing a file
97
+ // and we want to update the message content with the file name.
98
+ const reportFileEdited = () => {
99
+ if (currentEditingFileName) {
100
+ fullContent += ` * Edited ${currentEditingFileName}\n`;
101
+ throttledUpdateAIMessageContent(fullContent);
102
+ currentEditingFileName = null;
103
+ }
104
+ };
105
+ // Define callbacks for streaming parser
106
+ const callbacks = {
107
+ onFileNameChange: async (fileName, format) => {
108
+ DEBUG &&
109
+ console.log(`File changed to: ${fileName} (${format})`);
110
+ // Find existing file or create new one
111
+ currentEditingFileId = ensureFileExists(shareDBDoc, fileName);
112
+ reportFileEdited();
113
+ currentEditingFileName = fileName;
114
+ // Clear the file content to start fresh
115
+ // (AI will regenerate the entire file content)
116
+ clearFileContent(shareDBDoc, currentEditingFileId);
117
+ // Update AI status
118
+ updateAIStatus(shareDBDoc, chatId, 'Editing ' + fileName);
119
+ },
120
+ onCodeLine: async (line) => {
121
+ DEBUG && console.log(`Code line: ${line}`);
122
+ // If streaming is enabled, we apply the line immediately
123
+ if (currentEditingFileId) {
124
+ // Apply OT operation for this line immediately
125
+ appendLineToFile(shareDBDoc, currentEditingFileId, line);
126
+ // Update AI presence to show cursor at the end of the file
127
+ const currentFile = shareDBDoc.data.files[currentEditingFileId];
128
+ if (currentFile && currentFile.text) {
129
+ const textLength = currentFile.text.length;
130
+ const filePresence = {
131
+ username: 'AI Editor',
132
+ start: [
133
+ 'files',
134
+ currentEditingFileId,
135
+ 'text',
136
+ textLength,
137
+ ],
138
+ end: [
139
+ 'files',
140
+ currentEditingFileId,
141
+ 'text',
142
+ textLength,
143
+ ],
144
+ };
145
+ if (localPresence) {
146
+ localPresence.submit(filePresence, (error) => {
147
+ if (error) {
148
+ console.warn('AI Editor line presence submission error:', error);
149
+ }
150
+ });
151
+ }
152
+ }
153
+ }
154
+ },
155
+ onNonCodeLine: async (line) => {
156
+ // We want to report a file edited only if the line is not empty,
157
+ // because sometimes the LLMs leave a newline between the file name
158
+ // declaration and th
159
+ if (line.trim() !== '') {
160
+ reportFileEdited();
161
+ }
162
+ fullContent += line + '\n';
163
+ throttledUpdateAIMessageContent(fullContent);
164
+ },
165
+ };
166
+ const parser = new StreamingMarkdownParser(callbacks);
167
+ const chunks = [];
168
+ let reasoningContent = '';
169
+ // Stream the response with reasoning tokens
170
+ const modelName = model ||
171
+ process.env.VZCODE_EDIT_WITH_AI_MODEL_NAME ||
172
+ 'anthropic/claude-3.5-sonnet';
173
+ // Configure reasoning tokens based on enableReasoningTokens flag
174
+ const requestConfig = {
175
+ model: modelName,
176
+ messages: [{ role: 'user', content: fullPrompt }],
177
+ usage: { include: true },
178
+ stream: true,
179
+ ...aiRequestOptions,
180
+ };
181
+ // Only include reasoning configuration if reasoning tokens are enabled
182
+ if (enableReasoningTokens) {
183
+ requestConfig.reasoning = {
184
+ effort: 'low',
185
+ exclude: false,
186
+ };
187
+ }
188
+ const stream = await openRouterClient.chat.completions.create(requestConfig);
189
+ let reasoningStarted = false;
190
+ let contentStarted = false;
191
+ for await (const chunk of stream) {
192
+ if (slowMode) {
193
+ await new Promise((resolve) => setTimeout(resolve, 500));
194
+ }
195
+ const delta = chunk.choices[0]?.delta; // Type assertion for OpenRouter-specific reasoning fields
196
+ if (delta?.reasoning && enableReasoningTokens) {
197
+ // Handle reasoning tokens (thinking) - only if enabled
198
+ if (!reasoningStarted) {
199
+ reasoningStarted = true;
200
+ updateAIStatus(shareDBDoc, chatId, 'Thinking...');
201
+ }
202
+ reasoningContent += delta.reasoning;
203
+ updateAIScratchpad(shareDBDoc, chatId, reasoningContent);
204
+ }
205
+ else if (delta?.content) {
206
+ // Handle regular content tokens
207
+ if (reasoningStarted && !contentStarted) {
208
+ // Clear reasoning when content starts
209
+ contentStarted = true;
210
+ updateAIScratchpad(shareDBDoc, chatId, '');
211
+ updateAIStatus(shareDBDoc, chatId, 'Generating response...');
212
+ }
213
+ const chunkContent = delta.content;
214
+ chunks.push(chunkContent);
215
+ if (enableStreamingEditing) {
216
+ await parser.processChunk(chunkContent);
217
+ }
218
+ else {
219
+ fullContent += chunkContent;
220
+ throttledUpdateAIMessageContent(fullContent);
221
+ }
222
+ }
223
+ else if (chunk.usage) {
224
+ // Handle usage information
225
+ DEBUG && console.log('Usage:', chunk.usage);
226
+ }
227
+ if (!generationId && chunk.id) {
228
+ generationId = chunk.id;
229
+ }
230
+ }
231
+ await parser.flushRemaining();
232
+ reportFileEdited();
233
+ // Final cleanup - clear scratchpad and set final status
234
+ updateAIScratchpad(shareDBDoc, chatId, '');
235
+ updateAIStatus(shareDBDoc, chatId, 'Done editing.');
236
+ throttledUpdateAIMessageContent(fullContent);
237
+ // Flush to ensure the final content is written immediately
238
+ throttledUpdateAIMessageContent.flush();
239
+ // Finalize the AI message by clearing temporary fields
240
+ finalizeAIMessage(shareDBDoc, chatId);
241
+ // Clear AI Editor presence when done
242
+ DEBUG &&
243
+ console.log('AI editing done, clearing AI Editor presence');
244
+ if (localPresence) {
245
+ localPresence.submit(null, (error) => {
246
+ DEBUG && console.log('AI Editor presence cleared');
247
+ if (error) {
248
+ console.warn('AI Editor presence cleanup error:', error);
249
+ }
250
+ });
251
+ }
252
+ // If streaming editing is not enabled, we need to
253
+ // apply all the edits at once
254
+ if (!enableStreamingEditing) {
255
+ updateFiles(shareDBDoc, mergeFileChanges(shareDBDoc.data.files, parseMarkdownFiles(fullContent, 'bold').files));
256
+ }
257
+ // Generate a new runId to trigger a run when AI finishes editing
258
+ // This will trigger a re-run without hot reloading
259
+ const newRunId = generateRunId();
260
+ const runIdOp = diff(shareDBDoc.data, {
261
+ ...shareDBDoc.data,
262
+ runId: newRunId,
263
+ });
264
+ shareDBDoc.submitOp(runIdOp, (error) => {
265
+ if (error) {
266
+ console.warn('Error setting runId after AI editing:', error);
267
+ }
268
+ else {
269
+ DEBUG &&
270
+ console.log('Set new runId after AI editing:', newRunId);
271
+ }
272
+ });
273
+ // Write chunks file for debugging
274
+ if (DEBUG) {
275
+ const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
276
+ fs.writeFileSync(chunksFileJSONpath, JSON.stringify(chunks, null, 2));
277
+ console.log(`AI chunks written to ${chunksFileJSONpath}`);
278
+ }
279
+ return {
280
+ content: fullContent,
281
+ generationId: generationId,
282
+ };
283
+ };
284
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Validates incoming request data for AI chat messages
3
+ */
4
+ export const validateRequest = (req, res) => {
5
+ const { content, chatId } = req.body;
6
+ if (!content || typeof content !== 'string') {
7
+ res.status(400).json({
8
+ error: 'Invalid request: content is required and must be a string',
9
+ });
10
+ return false;
11
+ }
12
+ if (!chatId || typeof chatId !== 'string') {
13
+ res.status(400).json({
14
+ error: 'Invalid request: chatId is required and must be a string',
15
+ });
16
+ return false;
17
+ }
18
+ return true;
19
+ };
@@ -0,0 +1,146 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { randomId } from '../randomId.js';
4
+ import { enableDirectories, debugDirectories, debugIgnore, } from './featureFlags.js';
5
+ import createIgnore from 'ignore';
6
+ import { ignoreFilePattern, baseIgnore } from './config.js';
7
+ import { isDirectory } from './isDirectory.js';
8
+ // Import the image file utility
9
+ const isImageFile = (fileName) => {
10
+ return (fileName.match(/\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i) !== null);
11
+ };
12
+ /**
13
+ * @param {string} fullPath - absolut path of the workspace root
14
+ * @param {string} currentDirectoryPath - path where the ignore file is found, relative to fullPath
15
+ * @param {string} fileName - name of the ignore file
16
+ * @returns {string[]} parsed lines
17
+ */
18
+ const parseIgnoreFile = (fullPath, currentDirectory, fileName) => {
19
+ const filePath = path.join(fullPath, currentDirectory, fileName);
20
+ const content = fs.readFileSync(filePath, 'utf8');
21
+ const globs = content
22
+ .split(/[\n\r]+/)
23
+ .filter(
24
+ // remove blank line and comments
25
+ (line) => line.length > 0 && !line.startsWith('#'))
26
+ .map((line) => {
27
+ const { bang, slash, glob } = line.match(/^(?<bang>!?)(?<slash>\/?)(?<glob>.*)$/).groups;
28
+ const hasSlash = Boolean(slash) || /\/.*\S/.test(glob);
29
+ const relativeGlob = path.posix.join(currentDirectory.replace(
30
+ // escape characters with special meaning in glob expressions
31
+ /[*?!# \[\]\\]/g, (char) => '\\' + char),
32
+ // a pattern that doesn't include a slash (not counting a trailing one) matches files in any descendant directory od the current one
33
+ hasSlash ? '' : '**', glob);
34
+ // preserve leading `!` and `/` characters
35
+ return bang + slash + relativeGlob;
36
+ });
37
+ if (debugIgnore) {
38
+ console.debug('at', currentDirectory, 'parsing', fileName, 'obtained globs', globs);
39
+ }
40
+ return globs;
41
+ };
42
+ // Lists files from the file system,
43
+ // converts them into the VZCode internal
44
+ // ShareDB-compatible data structure.
45
+ export const computeInitialDocument = ({ fullPath }) => {
46
+ // Isolate files, not directories.
47
+ // Inspired by https://stackoverflow.com/questions/41472161/fs-readdir-ignore-directories
48
+ // Initialize the document using our data structure for representing files.
49
+ const initialDocument = {
50
+ // `files`
51
+ // * Keys are file ids, which are random numbers.
52
+ // * Values are objects with properties:
53
+ // * text - the text content of the file
54
+ // * name - the file name
55
+ files: {},
56
+ // `isInteracting`
57
+ // * Whether the user is currently interacting with the document
58
+ // using an interactive code widget such as number dragger.
59
+ // * When true, the auto-save to the file system is changed to be
60
+ // more frequent (throttled not debounced).
61
+ // isInteracting: false,
62
+ // This is `undefined` initially.
63
+ };
64
+ /**
65
+ * Stack for recursively traversing directories.
66
+ * @type {string[]}
67
+ */
68
+ let files = [];
69
+ const ignoreFileMatcher = createIgnore().add(ignoreFilePattern);
70
+ const isIgnoreFile = (fileName) => ignoreFileMatcher.ignores(fileName);
71
+ const unsearchedDirectories = [
72
+ {
73
+ currentDirectory: '.',
74
+ ignore: createIgnore().add(baseIgnore),
75
+ },
76
+ ];
77
+ while (unsearchedDirectories.length !== 0) {
78
+ const { currentDirectory, ignore: parentIgnore } = unsearchedDirectories.pop();
79
+ const currentDirectoryPath = path.join(fullPath, currentDirectory);
80
+ const dirEntries = fs
81
+ .readdirSync(currentDirectoryPath, {
82
+ withFileTypes: true,
83
+ })
84
+ .filter((dirent) => enableDirectories ? true : dirent.isFile());
85
+ // find .ignore or .gitignore files in the current directory
86
+ const ignoreFiles = dirEntries
87
+ .filter((dirent) => dirent.isFile() && isIgnoreFile(dirent.name))
88
+ .map((file) => file.name);
89
+ let ignore = parentIgnore;
90
+ if (ignoreFiles.length > 0) {
91
+ const globs = ignoreFiles.flatMap((fileName) => parseIgnoreFile(fullPath, currentDirectory, fileName));
92
+ ignore = createIgnore().add(parentIgnore).add(globs);
93
+ }
94
+ const newFiles = dirEntries
95
+ .filter((dirent) => {
96
+ const relativePath = path.posix.join(currentDirectory, dirent.name) +
97
+ (dirent.isDirectory() ? '/' : '');
98
+ const keep = !ignore.ignores(relativePath);
99
+ if (debugIgnore && !keep) {
100
+ console.debug('at', currentDirectory, 'ignoring', relativePath);
101
+ }
102
+ return keep;
103
+ })
104
+ // Add a trailing slash for directories
105
+ .map((dirent) => {
106
+ const relativePath = path.posix.join(currentDirectory, dirent.name);
107
+ if (!dirent.isDirectory()) {
108
+ return relativePath;
109
+ }
110
+ unsearchedDirectories.push({
111
+ currentDirectory: relativePath,
112
+ ignore,
113
+ });
114
+ return relativePath + '/';
115
+ });
116
+ // console.log(currentDirectory);
117
+ files.push(...newFiles);
118
+ }
119
+ files.forEach((file) => {
120
+ const id = randomId();
121
+ let text = null;
122
+ if (!isDirectory(file)) {
123
+ const filePath = path.join(fullPath, file);
124
+ if (isImageFile(file)) {
125
+ // Read image files as binary and convert to base64
126
+ const buffer = fs.readFileSync(filePath);
127
+ text = buffer.toString('base64');
128
+ }
129
+ else {
130
+ // Read non-image files as UTF-8 text
131
+ text = fs.readFileSync(filePath, 'utf-8');
132
+ }
133
+ }
134
+ initialDocument.files[id] = {
135
+ text,
136
+ name: file,
137
+ };
138
+ });
139
+ if (debugDirectories) {
140
+ console.log('files:');
141
+ console.log(files);
142
+ console.log('initialDocument:');
143
+ console.log(JSON.stringify(initialDocument, null, 2));
144
+ }
145
+ return initialDocument;
146
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ *patterns describing which files should e comsidered as ignore files when creating the file tree.
3
+ *
4
+ * Format: one pattern per line, in gitignore format as defimed by https://git-scm.com/docs/gitignore#_pattern_format
5
+ */
6
+ export const ignoreFilePattern = process.env.IGNORE_FILE_PATTERN ??
7
+ `
8
+ .vzignore
9
+ .ignore
10
+ .gitignore
11
+ `;
12
+ /**
13
+ * the base ignore patterns, that should be applied before any ignore file found when creating the file tree.
14
+ *
15
+ * These patterns apply even in the absence of an ignore file.
16
+ */
17
+ export const baseIgnore = process.env.BASE_IGNORE ??
18
+ `
19
+ .git/
20
+ node_modules/
21
+ `;
@@ -0,0 +1,5 @@
1
+ // Feature flag for directories, disabled until it's fully working.
2
+ export const enableDirectories = true;
3
+ export const debugDirectories = false;
4
+ export const debugIgnore = false;
5
+ export const enableESLint = true;