codexmate 0.1.8 → 0.1.10

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.
@@ -1,292 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- const TEXT_FILE_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.md', '.txt', '.svg', '.ts', '.tsx', '.jsx', '.yml', '.yaml']);
7
- const MAX_FILE_BYTES = 512 * 1024;
8
- const MAX_CONTEXT_FILE_BYTES = 16 * 1024;
9
- const MAX_CONTEXT_TOTAL_BYTES = 64 * 1024;
10
- const MAX_HISTORY_MESSAGES = 20;
11
- const SENSITIVE_CONTEXT_NAME_RE = /(^|[._\-/])(credential|credentials|secret|secrets|token|tokens|apikey|api-key|api_key|password|passwd|private-key|private_key|\.env)([._\-/]|$)/i;
12
-
13
- function ensureDir(dir) {
14
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
15
- }
16
-
17
- function sanitizeThreadId(value) {
18
- const text = typeof value === 'string' ? value.trim() : '';
19
- if (!text) return '';
20
- return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(text) ? text : '';
21
- }
22
-
23
- function normalizeWorkspacePath(rawPath, cwd) {
24
- const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd());
25
- const text = typeof rawPath === 'string' ? rawPath.trim().replace(/^['"]|['"]$/g, '') : '';
26
- if (!text || text.length > 240) {
27
- return { error: 'workspace file path is empty or too long' };
28
- }
29
- if (path.isAbsolute(text) || text.includes('\\')) {
30
- return { error: `workspace file path must be a safe relative path: ${text}` };
31
- }
32
- const normalized = path.normalize(text);
33
- if (!normalized || normalized === '.' || normalized.startsWith('..') || normalized.split(path.sep).includes('..')) {
34
- return { error: `workspace file path escapes cwd: ${text}` };
35
- }
36
- const ext = path.extname(normalized).toLowerCase();
37
- if (!TEXT_FILE_EXTENSIONS.has(ext)) {
38
- return { error: `workspace file extension is not allowed: ${ext || '(none)'}` };
39
- }
40
- const targetPath = path.resolve(baseDir, normalized);
41
- const relative = path.relative(baseDir, targetPath);
42
- if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
43
- return { error: `workspace file path escapes cwd: ${text}` };
44
- }
45
- return { path: targetPath, relativePath: normalized, baseDir };
46
- }
47
-
48
- function validateParentPath(targetPath, baseDir) {
49
- const resolvedBase = fs.realpathSync.native(baseDir);
50
- let current = path.dirname(targetPath);
51
- const pending = [];
52
- while (true) {
53
- if (current === resolvedBase) return { ok: true };
54
- if (current === path.dirname(current)) return { ok: false, error: 'workspace parent escapes cwd' };
55
- if (fs.existsSync(current)) {
56
- const stats = fs.lstatSync(current);
57
- if (stats.isSymbolicLink()) return { ok: false, error: `workspace parent is a symlink: ${path.relative(baseDir, current) || current}` };
58
- const resolvedCurrent = fs.realpathSync.native(current);
59
- const relative = path.relative(resolvedBase, resolvedCurrent);
60
- if (relative.startsWith('..') || path.isAbsolute(relative)) return { ok: false, error: 'workspace parent resolves outside cwd' };
61
- for (const child of pending) {
62
- const childRelative = path.relative(resolvedBase, child);
63
- if (childRelative.startsWith('..') || path.isAbsolute(childRelative)) return { ok: false, error: 'workspace parent escapes cwd' };
64
- }
65
- return { ok: true };
66
- }
67
- pending.push(current);
68
- current = path.dirname(current);
69
- }
70
- }
71
-
72
- function parseAttributes(info) {
73
- const attrs = {};
74
- const text = typeof info === 'string' ? info : '';
75
- const attrRe = /([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s]+))/g;
76
- let match;
77
- while ((match = attrRe.exec(text)) !== null) {
78
- attrs[match[1]] = match[3] || match[4] || match[5] || '';
79
- }
80
- return attrs;
81
- }
82
-
83
- function parseWorkspaceFileOperations(text) {
84
- const content = typeof text === 'string' ? text : '';
85
- const operations = [];
86
- const fenceRe = /```([^\n`]*)\n([\s\S]*?)```/g;
87
- let match;
88
- while ((match = fenceRe.exec(content)) !== null) {
89
- const info = String(match[1] || '').trim();
90
- if (!/^codexmate-file\b/i.test(info)) continue;
91
- const attrs = parseAttributes(info);
92
- const action = String(attrs.action || attrs.op || 'write').trim().toLowerCase();
93
- const filePath = String(attrs.path || attrs.file || attrs.filename || '').trim();
94
- operations.push({ action: action === 'update' ? 'write' : action, path: filePath, content: String(match[2] || '') });
95
- }
96
-
97
- const deleteRe = /^\s*CODEXMATE_(?:DELETE|REMOVE)_FILE\s*:\s*(.+?)\s*$/gmi;
98
- while ((match = deleteRe.exec(content)) !== null) {
99
- operations.push({ action: 'delete', path: String(match[1] || '').trim(), content: '' });
100
- }
101
- return operations;
102
- }
103
-
104
- function isSensitiveWorkspaceContextFile(relativePath) {
105
- const normalized = String(relativePath || '').replace(/\\+/g, '/').toLowerCase();
106
- return SENSITIVE_CONTEXT_NAME_RE.test(normalized);
107
- }
108
-
109
- function isWorkspaceFileMentioned(relativePath, promptText) {
110
- const normalizedPath = String(relativePath || '').toLowerCase();
111
- const baseName = path.basename(normalizedPath);
112
- return !!normalizedPath && (promptText.includes(normalizedPath) || (!!baseName && promptText.includes(baseName)));
113
- }
114
-
115
- function applyWorkspaceFileOperations(text, cwd, options = {}) {
116
- const allowWrite = options.allowWrite === true;
117
- const operations = parseWorkspaceFileOperations(text);
118
- const files = [];
119
- const warnings = [];
120
- if (!allowWrite && operations.length > 0) {
121
- return { files, warnings: ['workspace file operations skipped because allowWrite is false'] };
122
- }
123
- for (const operation of operations) {
124
- const action = operation.action === 'delete' ? 'delete' : 'write';
125
- const normalized = normalizeWorkspacePath(operation.path, cwd);
126
- if (normalized.error) {
127
- warnings.push(normalized.error);
128
- continue;
129
- }
130
- try {
131
- if (action === 'delete') {
132
- if (!fs.existsSync(normalized.path)) {
133
- files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: 0, operation: 'delete', existed: false });
134
- continue;
135
- }
136
- const stats = fs.lstatSync(normalized.path);
137
- if (stats.isSymbolicLink()) {
138
- warnings.push(`workspace target is a symlink: ${normalized.relativePath}`);
139
- continue;
140
- }
141
- if (!stats.isFile()) {
142
- warnings.push(`workspace target is not a file: ${normalized.relativePath}`);
143
- continue;
144
- }
145
- fs.unlinkSync(normalized.path);
146
- files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: 0, operation: 'delete', existed: true });
147
- continue;
148
- }
149
-
150
- const body = String(operation.content || '');
151
- const bytes = Buffer.byteLength(body, 'utf-8');
152
- if (bytes > MAX_FILE_BYTES) {
153
- warnings.push(`workspace artifact too large and skipped: ${normalized.relativePath}`);
154
- continue;
155
- }
156
- const parentValidation = validateParentPath(normalized.path, normalized.baseDir);
157
- if (!parentValidation.ok) {
158
- warnings.push(parentValidation.error || `workspace parent is unsafe: ${normalized.relativePath}`);
159
- continue;
160
- }
161
- ensureDir(path.dirname(normalized.path));
162
- try {
163
- if (fs.lstatSync(normalized.path).isSymbolicLink()) {
164
- warnings.push(`workspace target is a symlink: ${normalized.relativePath}`);
165
- continue;
166
- }
167
- } catch (error) {
168
- if (!error || error.code !== 'ENOENT') throw error;
169
- }
170
- fs.writeFileSync(normalized.path, body, { encoding: 'utf-8', mode: 0o600 });
171
- files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes, operation: 'write' });
172
- } catch (error) {
173
- warnings.push(`workspace operation failed for ${normalized.relativePath}: ${error && error.message ? error.message : String(error)}`);
174
- }
175
- }
176
- return { files, warnings };
177
- }
178
-
179
- function walkWorkspace(cwd, options = {}) {
180
- const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd());
181
- const maxFiles = Number.isFinite(options.maxFiles) ? options.maxFiles : 120;
182
- const files = [];
183
- const skipNames = new Set(['.git', 'node_modules', '.codexmate', '.DS_Store']);
184
- function walk(dir) {
185
- if (files.length >= maxFiles) return;
186
- let entries = [];
187
- try {
188
- entries = fs.readdirSync(dir, { withFileTypes: true });
189
- } catch (_) {
190
- return;
191
- }
192
- entries.sort((a, b) => a.name.localeCompare(b.name));
193
- for (const entry of entries) {
194
- if (files.length >= maxFiles) break;
195
- if (skipNames.has(entry.name)) continue;
196
- const fullPath = path.join(dir, entry.name);
197
- let stats;
198
- try {
199
- stats = fs.lstatSync(fullPath);
200
- } catch (_) {
201
- continue;
202
- }
203
- if (stats.isSymbolicLink()) continue;
204
- const relativePath = path.relative(baseDir, fullPath);
205
- if (stats.isDirectory()) {
206
- walk(fullPath);
207
- } else if (stats.isFile()) {
208
- files.push({ path: fullPath, relativePath, bytes: stats.size, ext: path.extname(relativePath).toLowerCase() });
209
- }
210
- }
211
- }
212
- if (fs.existsSync(baseDir)) walk(baseDir);
213
- return files;
214
- }
215
-
216
- function buildWorkspaceChatContext(cwd, prompt = '', historyMessages = []) {
217
- const files = walkWorkspace(cwd);
218
- const promptText = [prompt, ...historyMessages.map((item) => item && item.content ? item.content : '')].join('\n').toLowerCase();
219
- const treeLines = files.map((file) => `- ${file.relativePath} (${file.bytes} bytes)`);
220
- const contentBlocks = [];
221
- let total = 0;
222
- for (const file of files) {
223
- if (!TEXT_FILE_EXTENSIONS.has(file.ext)) continue;
224
- if (file.bytes > MAX_CONTEXT_FILE_BYTES) continue;
225
- const mentioned = isWorkspaceFileMentioned(file.relativePath, promptText);
226
- if (!mentioned) continue;
227
- if (isSensitiveWorkspaceContextFile(file.relativePath)) continue;
228
- if (total + file.bytes > MAX_CONTEXT_TOTAL_BYTES) break;
229
- try {
230
- const body = fs.readFileSync(file.path, 'utf-8');
231
- total += Buffer.byteLength(body, 'utf-8');
232
- contentBlocks.push(`### ${file.relativePath}\n\`\`\`\n${body}\n\`\`\``);
233
- } catch (_) {}
234
- }
235
- return [
236
- '当前工作区文件列表:',
237
- treeLines.length ? treeLines.join('\n') : '(empty workspace)',
238
- contentBlocks.length ? '\n当前工作区可读文本文件内容快照:\n' + contentBlocks.join('\n\n') : '\n当前没有可注入的文本文件内容快照。'
239
- ].join('\n');
240
- }
241
-
242
- function getThreadFile(storeDir, threadId) {
243
- const safe = sanitizeThreadId(threadId);
244
- if (!safe) return '';
245
- return path.join(storeDir, `${safe}.json`);
246
- }
247
-
248
- function loadWorkspaceChatThread(storeDir, threadId) {
249
- const file = getThreadFile(storeDir, threadId);
250
- if (!file || !fs.existsSync(file)) return { threadId: sanitizeThreadId(threadId), messages: [] };
251
- try {
252
- const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
253
- const messages = Array.isArray(parsed.messages) ? parsed.messages.filter((item) => item && (item.role === 'user' || item.role === 'assistant') && typeof item.content === 'string') : [];
254
- return { ...parsed, threadId: sanitizeThreadId(parsed.threadId || threadId), messages: messages.slice(-MAX_HISTORY_MESSAGES) };
255
- } catch (_) {
256
- return { threadId: sanitizeThreadId(threadId), messages: [] };
257
- }
258
- }
259
-
260
- function appendWorkspaceChatThread(storeDir, threadId, messages, metadata = {}) {
261
- const safe = sanitizeThreadId(threadId);
262
- if (!safe) return { ok: false, error: 'invalid thread id' };
263
- ensureDir(storeDir);
264
- const existing = loadWorkspaceChatThread(storeDir, safe);
265
- const now = new Date().toISOString();
266
- const nextMessages = existing.messages.concat((Array.isArray(messages) ? messages : []).map((item) => ({
267
- role: item.role === 'assistant' ? 'assistant' : 'user',
268
- content: String(item.content || ''),
269
- at: item.at || now
270
- }))).slice(-MAX_HISTORY_MESSAGES);
271
- const payload = {
272
- threadId: safe,
273
- cwd: metadata.cwd || existing.cwd || '',
274
- updatedAt: now,
275
- messages: nextMessages
276
- };
277
- const file = getThreadFile(storeDir, safe);
278
- fs.writeFileSync(file, JSON.stringify(payload, null, 2), { encoding: 'utf-8', mode: 0o600 });
279
- return { ok: true, file, messageCount: nextMessages.length };
280
- }
281
-
282
- module.exports = {
283
- TEXT_FILE_EXTENSIONS,
284
- sanitizeThreadId,
285
- normalizeWorkspacePath,
286
- parseWorkspaceFileOperations,
287
- applyWorkspaceFileOperations,
288
- walkWorkspace,
289
- buildWorkspaceChatContext,
290
- loadWorkspaceChatThread,
291
- appendWorkspaceChatThread
292
- };