@pixelbyte-software/pixcode 1.53.10 → 1.53.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-BEyC4q_s.css +32 -0
- package/dist/assets/{index-ChssgA1r.js → index-kP_gZ-wL.js} +178 -178
- package/dist/index.html +2 -2
- package/dist-server/server/database/db.js +26 -0
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +290 -57
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/projects.js +126 -43
- package/dist-server/server/projects.js.map +1 -1
- package/dist-server/server/routes/git.js +16 -0
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/plugins.js +364 -0
- package/dist-server/server/routes/plugins.js.map +1 -1
- package/dist-server/server/services/telegram/bot.js +43 -5
- package/dist-server/server/services/telegram/bot.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +169 -5
- package/dist-server/server/services/telegram/control-center.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +28 -2
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/package.json +1 -1
- package/server/database/db.js +26 -0
- package/server/index.js +312 -62
- package/server/projects.js +128 -41
- package/server/routes/git.js +17 -0
- package/server/routes/plugins.js +370 -0
- package/server/services/telegram/bot.js +45 -5
- package/server/services/telegram/control-center.js +173 -5
- package/server/services/telegram/translations.js +28 -2
- package/dist/assets/index-FhOsv2Yy.css +0 -32
package/server/projects.js
CHANGED
|
@@ -69,6 +69,15 @@ import sessionManager from './sessionManager.js';
|
|
|
69
69
|
import { applyCustomSessionNames } from './database/db.js';
|
|
70
70
|
|
|
71
71
|
const FILE_COUNT_LIMIT = 20000;
|
|
72
|
+
const FILE_COUNT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
73
|
+
const FILE_COUNT_CACHE_MAX_ENTRIES = 300;
|
|
74
|
+
const FILE_COUNT_SCAN_MAX_MS = 2500;
|
|
75
|
+
const CODEX_SESSION_INDEX_TTL_MS = 60 * 1000;
|
|
76
|
+
const CODEX_SESSION_INDEX_MAX_FILES = 1200;
|
|
77
|
+
const CODEX_SESSION_INDEX_MAX_DIRS = 2500;
|
|
78
|
+
const GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS = 60 * 1000;
|
|
79
|
+
const CLI_CHAT_FILE_READ_MAX_BYTES = 2 * 1024 * 1024;
|
|
80
|
+
const CLI_CHAT_FILES_PER_PROJECT_LIMIT = 150;
|
|
72
81
|
const FILE_COUNT_IGNORED_DIRECTORIES = new Set([
|
|
73
82
|
'.cache',
|
|
74
83
|
'.git',
|
|
@@ -88,11 +97,45 @@ const FILE_COUNT_IGNORED_DIRECTORIES = new Set([
|
|
|
88
97
|
'target',
|
|
89
98
|
'vendor'
|
|
90
99
|
]);
|
|
100
|
+
const fileCountCache = new Map();
|
|
101
|
+
let codexSessionsIndexCache = null;
|
|
102
|
+
let geminiCliProjectEntriesCache = null;
|
|
103
|
+
let qwenCliProjectEntriesCache = null;
|
|
104
|
+
|
|
105
|
+
function pruneMapToMaxEntries(map, maxEntries) {
|
|
106
|
+
while (map.size > maxEntries) {
|
|
107
|
+
const firstKey = map.keys().next().value;
|
|
108
|
+
if (firstKey === undefined) break;
|
|
109
|
+
map.delete(firstKey);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readFreshCacheEntry(cache, key, now = Date.now()) {
|
|
114
|
+
const entry = cache.get(key);
|
|
115
|
+
if (!entry || entry.expiresAt <= now) {
|
|
116
|
+
if (entry) cache.delete(key);
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return entry.value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeCacheEntry(cache, key, value, ttlMs, maxEntries) {
|
|
123
|
+
cache.set(key, {
|
|
124
|
+
value,
|
|
125
|
+
expiresAt: Date.now() + ttlMs,
|
|
126
|
+
});
|
|
127
|
+
pruneMapToMaxEntries(cache, maxEntries);
|
|
128
|
+
}
|
|
91
129
|
|
|
92
130
|
async function countProjectFiles(projectPath, maxFiles = FILE_COUNT_LIMIT) {
|
|
93
131
|
if (!projectPath || typeof projectPath !== 'string') {
|
|
94
132
|
return undefined;
|
|
95
133
|
}
|
|
134
|
+
const cacheKey = `${path.resolve(projectPath)}:${maxFiles}`;
|
|
135
|
+
const cached = readFreshCacheEntry(fileCountCache, cacheKey);
|
|
136
|
+
if (cached !== undefined) {
|
|
137
|
+
return cached;
|
|
138
|
+
}
|
|
96
139
|
|
|
97
140
|
try {
|
|
98
141
|
const stats = await fs.stat(projectPath);
|
|
@@ -104,9 +147,15 @@ async function countProjectFiles(projectPath, maxFiles = FILE_COUNT_LIMIT) {
|
|
|
104
147
|
}
|
|
105
148
|
|
|
106
149
|
let count = 0;
|
|
150
|
+
const startedAt = Date.now();
|
|
107
151
|
const pendingDirectories = [projectPath];
|
|
108
152
|
|
|
109
153
|
while (pendingDirectories.length > 0) {
|
|
154
|
+
if (Date.now() - startedAt > FILE_COUNT_SCAN_MAX_MS) {
|
|
155
|
+
writeCacheEntry(fileCountCache, cacheKey, count, FILE_COUNT_CACHE_TTL_MS, FILE_COUNT_CACHE_MAX_ENTRIES);
|
|
156
|
+
return count;
|
|
157
|
+
}
|
|
158
|
+
|
|
110
159
|
const currentDirectory = pendingDirectories.pop();
|
|
111
160
|
let entries = [];
|
|
112
161
|
|
|
@@ -127,12 +176,14 @@ async function countProjectFiles(projectPath, maxFiles = FILE_COUNT_LIMIT) {
|
|
|
127
176
|
if (entry.isFile() || entry.isSymbolicLink()) {
|
|
128
177
|
count += 1;
|
|
129
178
|
if (count >= maxFiles) {
|
|
179
|
+
writeCacheEntry(fileCountCache, cacheKey, count, FILE_COUNT_CACHE_TTL_MS, FILE_COUNT_CACHE_MAX_ENTRIES);
|
|
130
180
|
return count;
|
|
131
181
|
}
|
|
132
182
|
}
|
|
133
183
|
}
|
|
134
184
|
}
|
|
135
185
|
|
|
186
|
+
writeCacheEntry(fileCountCache, cacheKey, count, FILE_COUNT_CACHE_TTL_MS, FILE_COUNT_CACHE_MAX_ENTRIES);
|
|
136
187
|
return count;
|
|
137
188
|
}
|
|
138
189
|
|
|
@@ -207,6 +258,10 @@ function areStringArraysEqual(first, second) {
|
|
|
207
258
|
// Gemini ones rather than sharing a parametric implementation, keeping
|
|
208
259
|
// provider-specific logic discoverable in one place.
|
|
209
260
|
async function listQwenCliProjectEntries() {
|
|
261
|
+
if (qwenCliProjectEntriesCache?.expiresAt > Date.now()) {
|
|
262
|
+
return qwenCliProjectEntriesCache.entries;
|
|
263
|
+
}
|
|
264
|
+
|
|
210
265
|
const qwenTmpDir = path.join(os.homedir(), '.qwen', 'tmp');
|
|
211
266
|
try {
|
|
212
267
|
await fs.access(qwenTmpDir);
|
|
@@ -241,10 +296,18 @@ async function listQwenCliProjectEntries() {
|
|
|
241
296
|
});
|
|
242
297
|
}
|
|
243
298
|
|
|
299
|
+
qwenCliProjectEntriesCache = {
|
|
300
|
+
entries,
|
|
301
|
+
expiresAt: Date.now() + GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS,
|
|
302
|
+
};
|
|
244
303
|
return entries;
|
|
245
304
|
}
|
|
246
305
|
|
|
247
306
|
async function listGeminiCliProjectEntries() {
|
|
307
|
+
if (geminiCliProjectEntriesCache?.expiresAt > Date.now()) {
|
|
308
|
+
return geminiCliProjectEntriesCache.entries;
|
|
309
|
+
}
|
|
310
|
+
|
|
248
311
|
const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
|
|
249
312
|
try {
|
|
250
313
|
await fs.access(geminiTmpDir);
|
|
@@ -282,9 +345,21 @@ async function listGeminiCliProjectEntries() {
|
|
|
282
345
|
});
|
|
283
346
|
}
|
|
284
347
|
|
|
348
|
+
geminiCliProjectEntriesCache = {
|
|
349
|
+
entries,
|
|
350
|
+
expiresAt: Date.now() + GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS,
|
|
351
|
+
};
|
|
285
352
|
return entries;
|
|
286
353
|
}
|
|
287
354
|
|
|
355
|
+
async function readJsonTextFileLimited(filePath, maxBytes = CLI_CHAT_FILE_READ_MAX_BYTES) {
|
|
356
|
+
const stat = await fs.stat(filePath);
|
|
357
|
+
if (!stat.isFile() || stat.size > maxBytes) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
return fs.readFile(filePath, 'utf8');
|
|
361
|
+
}
|
|
362
|
+
|
|
288
363
|
function addDiscoveredProject(discoveredProjectsByPath, projectPath, provider) {
|
|
289
364
|
if (typeof projectPath !== 'string' || projectPath.trim().length === 0) {
|
|
290
365
|
return;
|
|
@@ -1614,27 +1689,46 @@ function normalizeComparablePath(inputPath) {
|
|
|
1614
1689
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
1615
1690
|
}
|
|
1616
1691
|
|
|
1617
|
-
async function findCodexJsonlFiles(dir
|
|
1692
|
+
async function findCodexJsonlFiles(dir, {
|
|
1693
|
+
maxFiles = CODEX_SESSION_INDEX_MAX_FILES,
|
|
1694
|
+
maxDirs = CODEX_SESSION_INDEX_MAX_DIRS,
|
|
1695
|
+
} = {}) {
|
|
1618
1696
|
const files = [];
|
|
1697
|
+
const pendingDirs = [dir];
|
|
1698
|
+
let visitedDirs = 0;
|
|
1619
1699
|
|
|
1620
|
-
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1700
|
+
while (pendingDirs.length > 0 && files.length < maxFiles && visitedDirs < maxDirs) {
|
|
1701
|
+
const currentDir = pendingDirs.pop();
|
|
1702
|
+
visitedDirs += 1;
|
|
1703
|
+
|
|
1704
|
+
try {
|
|
1705
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
1706
|
+
entries.sort((a, b) => b.name.localeCompare(a.name));
|
|
1707
|
+
|
|
1708
|
+
for (const entry of entries) {
|
|
1709
|
+
if (files.length >= maxFiles) break;
|
|
1710
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
1711
|
+
if (entry.isDirectory()) {
|
|
1712
|
+
if (visitedDirs + pendingDirs.length < maxDirs) {
|
|
1713
|
+
pendingDirs.push(fullPath);
|
|
1714
|
+
}
|
|
1715
|
+
} else if (entry.name.endsWith('.jsonl')) {
|
|
1716
|
+
files.push(fullPath);
|
|
1717
|
+
}
|
|
1628
1718
|
}
|
|
1719
|
+
} catch {
|
|
1720
|
+
// Skip directories we can't read
|
|
1629
1721
|
}
|
|
1630
|
-
} catch (error) {
|
|
1631
|
-
// Skip directories we can't read
|
|
1632
1722
|
}
|
|
1633
1723
|
|
|
1634
1724
|
return files;
|
|
1635
1725
|
}
|
|
1636
1726
|
|
|
1637
1727
|
async function buildCodexSessionsIndex() {
|
|
1728
|
+
if (codexSessionsIndexCache?.expiresAt > Date.now()) {
|
|
1729
|
+
return codexSessionsIndexCache.sessionsByProject;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1638
1732
|
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1639
1733
|
const sessionsByProject = new Map();
|
|
1640
1734
|
|
|
@@ -1683,6 +1777,10 @@ async function buildCodexSessionsIndex() {
|
|
|
1683
1777
|
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
1684
1778
|
}
|
|
1685
1779
|
|
|
1780
|
+
codexSessionsIndexCache = {
|
|
1781
|
+
sessionsByProject,
|
|
1782
|
+
expiresAt: Date.now() + CODEX_SESSION_INDEX_TTL_MS,
|
|
1783
|
+
};
|
|
1686
1784
|
return sessionsByProject;
|
|
1687
1785
|
}
|
|
1688
1786
|
|
|
@@ -2049,24 +2147,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2049
2147
|
async function deleteCodexSession(sessionId) {
|
|
2050
2148
|
try {
|
|
2051
2149
|
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
2052
|
-
|
|
2053
|
-
const findJsonlFiles = async (dir) => {
|
|
2054
|
-
const files = [];
|
|
2055
|
-
try {
|
|
2056
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2057
|
-
for (const entry of entries) {
|
|
2058
|
-
const fullPath = path.join(dir, entry.name);
|
|
2059
|
-
if (entry.isDirectory()) {
|
|
2060
|
-
files.push(...await findJsonlFiles(fullPath));
|
|
2061
|
-
} else if (entry.name.endsWith('.jsonl')) {
|
|
2062
|
-
files.push(fullPath);
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
} catch (error) { }
|
|
2066
|
-
return files;
|
|
2067
|
-
};
|
|
2068
|
-
|
|
2069
|
-
const jsonlFiles = await findJsonlFiles(codexSessionsDir);
|
|
2150
|
+
const jsonlFiles = await findCodexJsonlFiles(codexSessionsDir);
|
|
2070
2151
|
|
|
2071
2152
|
for (const filePath of jsonlFiles) {
|
|
2072
2153
|
const sessionData = await parseCodexSessionFile(filePath);
|
|
@@ -2582,13 +2663,14 @@ async function searchGeminiSessionsForProject(
|
|
|
2582
2663
|
continue;
|
|
2583
2664
|
}
|
|
2584
2665
|
|
|
2585
|
-
for (const chatFile of chatFiles) {
|
|
2666
|
+
for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
|
|
2586
2667
|
if (getTotalMatches() >= limit || isAborted()) break;
|
|
2587
2668
|
if (!chatFile.endsWith('.json')) continue;
|
|
2588
2669
|
|
|
2589
2670
|
try {
|
|
2590
2671
|
const filePath = path.join(chatsDir, chatFile);
|
|
2591
|
-
const data = await
|
|
2672
|
+
const data = await readJsonTextFileLimited(filePath);
|
|
2673
|
+
if (!data) continue;
|
|
2592
2674
|
const session = JSON.parse(data);
|
|
2593
2675
|
if (!session.messages || !Array.isArray(session.messages)) continue;
|
|
2594
2676
|
|
|
@@ -2680,11 +2762,12 @@ async function getQwenCliSessions(projectPath, options = {}) {
|
|
|
2680
2762
|
continue;
|
|
2681
2763
|
}
|
|
2682
2764
|
|
|
2683
|
-
for (const chatFile of chatFiles) {
|
|
2765
|
+
for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
|
|
2684
2766
|
if (!chatFile.endsWith('.json')) continue;
|
|
2685
2767
|
try {
|
|
2686
2768
|
const filePath = path.join(chatsDir, chatFile);
|
|
2687
|
-
const data = await
|
|
2769
|
+
const data = await readJsonTextFileLimited(filePath);
|
|
2770
|
+
if (!data) continue;
|
|
2688
2771
|
const session = JSON.parse(data);
|
|
2689
2772
|
if (!session.messages || !Array.isArray(session.messages)) continue;
|
|
2690
2773
|
|
|
@@ -2763,10 +2846,11 @@ async function getOpencodeCliSessions(projectPath) {
|
|
|
2763
2846
|
continue;
|
|
2764
2847
|
}
|
|
2765
2848
|
|
|
2766
|
-
for (const file of sessionFiles) {
|
|
2849
|
+
for (const file of sessionFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
|
|
2767
2850
|
if (!file.startsWith('ses_') || !file.endsWith('.json')) continue;
|
|
2768
2851
|
try {
|
|
2769
|
-
const data = await
|
|
2852
|
+
const data = await readJsonTextFileLimited(path.join(dir, file));
|
|
2853
|
+
if (!data) continue;
|
|
2770
2854
|
const sess = JSON.parse(data);
|
|
2771
2855
|
const sessDir = typeof sess?.directory === 'string'
|
|
2772
2856
|
? normalizeComparablePath(sess.directory)
|
|
@@ -2830,11 +2914,12 @@ async function getQwenCliSessionMessages(sessionId) {
|
|
|
2830
2914
|
continue;
|
|
2831
2915
|
}
|
|
2832
2916
|
|
|
2833
|
-
for (const chatFile of chatFiles) {
|
|
2917
|
+
for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
|
|
2834
2918
|
if (!chatFile.endsWith('.json')) continue;
|
|
2835
2919
|
try {
|
|
2836
2920
|
const filePath = path.join(chatsDir, chatFile);
|
|
2837
|
-
const data = await
|
|
2921
|
+
const data = await readJsonTextFileLimited(filePath);
|
|
2922
|
+
if (!data) continue;
|
|
2838
2923
|
const session = JSON.parse(data);
|
|
2839
2924
|
const fileSessionId = session.sessionId || chatFile.replace('.json', '');
|
|
2840
2925
|
if (fileSessionId !== sessionId) continue;
|
|
@@ -2890,11 +2975,12 @@ async function getGeminiCliSessions(projectPath, options = {}) {
|
|
|
2890
2975
|
continue;
|
|
2891
2976
|
}
|
|
2892
2977
|
|
|
2893
|
-
for (const chatFile of chatFiles) {
|
|
2978
|
+
for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
|
|
2894
2979
|
if (!chatFile.endsWith('.json')) continue;
|
|
2895
2980
|
try {
|
|
2896
2981
|
const filePath = path.join(chatsDir, chatFile);
|
|
2897
|
-
const data = await
|
|
2982
|
+
const data = await readJsonTextFileLimited(filePath);
|
|
2983
|
+
if (!data) continue;
|
|
2898
2984
|
const session = JSON.parse(data);
|
|
2899
2985
|
if (!session.messages || !Array.isArray(session.messages)) continue;
|
|
2900
2986
|
|
|
@@ -2946,11 +3032,12 @@ async function getGeminiCliSessionMessages(sessionId) {
|
|
|
2946
3032
|
continue;
|
|
2947
3033
|
}
|
|
2948
3034
|
|
|
2949
|
-
for (const chatFile of chatFiles) {
|
|
3035
|
+
for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
|
|
2950
3036
|
if (!chatFile.endsWith('.json')) continue;
|
|
2951
3037
|
try {
|
|
2952
3038
|
const filePath = path.join(chatsDir, chatFile);
|
|
2953
|
-
const data = await
|
|
3039
|
+
const data = await readJsonTextFileLimited(filePath);
|
|
3040
|
+
if (!data) continue;
|
|
2954
3041
|
const session = JSON.parse(data);
|
|
2955
3042
|
const fileSessionId = session.sessionId || chatFile.replace('.json', '');
|
|
2956
3043
|
if (fileSessionId !== sessionId) continue;
|
package/server/routes/git.js
CHANGED
|
@@ -51,6 +51,14 @@ function isNotGitRepositoryMessage(message = '') {
|
|
|
51
51
|
|| message.includes('Project directory is not a git repository');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function isMissingFileError(error) {
|
|
55
|
+
const message = `${error?.message || ''} ${error?.stderr || ''} ${error?.stdout || ''}`.toLowerCase();
|
|
56
|
+
return error?.code === 'ENOENT'
|
|
57
|
+
|| message.includes('enoent')
|
|
58
|
+
|| message.includes('no such file or directory')
|
|
59
|
+
|| message.includes('path does not exist');
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
function shouldSkipFilesystemEntry(entryName) {
|
|
55
63
|
return FILESYSTEM_SCAN_EXCLUDED_DIRS.has(entryName)
|
|
56
64
|
|| entryName.endsWith('.log')
|
|
@@ -746,6 +754,15 @@ router.get('/file-with-diff', async (req, res) => {
|
|
|
746
754
|
isUntracked
|
|
747
755
|
});
|
|
748
756
|
} catch (error) {
|
|
757
|
+
if (isMissingFileError(error)) {
|
|
758
|
+
return res.status(404).json({
|
|
759
|
+
error: 'File not found',
|
|
760
|
+
currentContent: '',
|
|
761
|
+
oldContent: '',
|
|
762
|
+
isDeleted: true,
|
|
763
|
+
isUntracked: false,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
749
766
|
console.error('Git file-with-diff error:', error);
|
|
750
767
|
res.json({ error: error.message });
|
|
751
768
|
}
|