@contextos/core 0.2.1 → 0.2.3
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/index.js +736 -211
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -92,9 +92,67 @@ var ConfigYamlSchema = z.object({
|
|
|
92
92
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
93
93
|
import { join, dirname } from "path";
|
|
94
94
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
95
|
+
|
|
96
|
+
// src/config/encryption.ts
|
|
97
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
|
|
98
|
+
var ALGORITHM = "aes-256-gcm";
|
|
99
|
+
var KEY_LENGTH = 32;
|
|
100
|
+
var IV_LENGTH = 16;
|
|
101
|
+
var SALT = "contextos-config-salt-v1";
|
|
102
|
+
function encrypt(text, password) {
|
|
103
|
+
try {
|
|
104
|
+
const key = scryptSync(password, SALT, KEY_LENGTH);
|
|
105
|
+
const iv = randomBytes(IV_LENGTH);
|
|
106
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
107
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
108
|
+
encrypted += cipher.final("hex");
|
|
109
|
+
const authTag = cipher.getAuthTag();
|
|
110
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`Encryption failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function decrypt(encrypted, password) {
|
|
116
|
+
try {
|
|
117
|
+
const parts = encrypted.split(":");
|
|
118
|
+
if (parts.length !== 3) {
|
|
119
|
+
throw new Error("Invalid encrypted format");
|
|
120
|
+
}
|
|
121
|
+
const [ivHex, authTagHex, encryptedText] = parts;
|
|
122
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
123
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
124
|
+
const key = scryptSync(password, SALT, KEY_LENGTH);
|
|
125
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
126
|
+
decipher.setAuthTag(authTag);
|
|
127
|
+
let decrypted = decipher.update(encryptedText, "hex", "utf8");
|
|
128
|
+
decrypted += decipher.final("utf8");
|
|
129
|
+
return decrypted;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new Error(`Decryption failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function getEncryptionPassword() {
|
|
135
|
+
const password = process.env.CONTEXTOS_ENCRYPTION_KEY;
|
|
136
|
+
if (!password) {
|
|
137
|
+
if (process.env.NODE_ENV === "production") {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"CONTEXTOS_ENCRYPTION_KEY environment variable must be set in production"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
console.warn("CONTEXTOS_ENCRYPTION_KEY not set, using insecure default (development only)");
|
|
143
|
+
return "contextos-dev-key-change-in-production";
|
|
144
|
+
}
|
|
145
|
+
if (password.length < 16) {
|
|
146
|
+
throw new Error("CONTEXTOS_ENCRYPTION_KEY must be at least 16 characters");
|
|
147
|
+
}
|
|
148
|
+
return password;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/config/loader.ts
|
|
95
152
|
var CONTEXTOS_DIR = ".contextos";
|
|
96
153
|
var CONTEXT_FILE = "context.yaml";
|
|
97
154
|
var CONFIG_FILE = "config.yaml";
|
|
155
|
+
var SECRETS_FILE = ".config.secrets";
|
|
98
156
|
function findContextosRoot(startDir = process.cwd()) {
|
|
99
157
|
let currentDir = startDir;
|
|
100
158
|
while (currentDir !== dirname(currentDir)) {
|
|
@@ -134,7 +192,28 @@ function loadConfigYaml(rootDir) {
|
|
|
134
192
|
throw new Error(`Invalid config.yaml:
|
|
135
193
|
${errors}`);
|
|
136
194
|
}
|
|
137
|
-
|
|
195
|
+
const config = result.data;
|
|
196
|
+
const secretsPath = join(rootDir, CONTEXTOS_DIR, SECRETS_FILE);
|
|
197
|
+
if (existsSync(secretsPath)) {
|
|
198
|
+
try {
|
|
199
|
+
const encryptedSecrets = readFileSync(secretsPath, "utf-8");
|
|
200
|
+
const password = getEncryptionPassword();
|
|
201
|
+
let decryptedSecrets;
|
|
202
|
+
try {
|
|
203
|
+
const secretsJson = decrypt(encryptedSecrets, password);
|
|
204
|
+
decryptedSecrets = JSON.parse(secretsJson);
|
|
205
|
+
} catch (parseError) {
|
|
206
|
+
console.warn(`Failed to parse encrypted secrets: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
207
|
+
return config;
|
|
208
|
+
}
|
|
209
|
+
if (decryptedSecrets.apiKeys) {
|
|
210
|
+
config.apiKeys = decryptedSecrets.apiKeys;
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.warn(`Failed to load encrypted secrets: ${error instanceof Error ? error.message : String(error)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return config;
|
|
138
217
|
}
|
|
139
218
|
function loadConfig(startDir = process.cwd()) {
|
|
140
219
|
const rootDir = findContextosRoot(startDir);
|
|
@@ -164,20 +243,68 @@ function saveContextYaml(rootDir, context) {
|
|
|
164
243
|
});
|
|
165
244
|
writeFileSync(contextPath, content, "utf-8");
|
|
166
245
|
}
|
|
167
|
-
function saveConfigYaml(rootDir, config) {
|
|
246
|
+
function saveConfigYaml(rootDir, config, options = {}) {
|
|
168
247
|
const contextosDir = join(rootDir, CONTEXTOS_DIR);
|
|
169
248
|
const configPath = join(contextosDir, CONFIG_FILE);
|
|
249
|
+
const secretsPath = join(contextosDir, SECRETS_FILE);
|
|
170
250
|
if (!existsSync(contextosDir)) {
|
|
171
251
|
mkdirSync(contextosDir, { recursive: true });
|
|
172
252
|
}
|
|
173
|
-
const
|
|
253
|
+
const shouldEncrypt = !options.skipEncryption && options.encryptSecrets !== false;
|
|
254
|
+
const sensitiveFields = {};
|
|
255
|
+
const sanitizedConfig = { ...config };
|
|
256
|
+
const sensitivePaths = [
|
|
257
|
+
{ path: "apiKeys", key: "apiKeys" },
|
|
258
|
+
{ path: "embedding.api_key", key: "embedding" },
|
|
259
|
+
{ path: "llm.apiKey", key: "llm" }
|
|
260
|
+
];
|
|
261
|
+
for (const { path: fieldPath, key: sectionKey } of sensitivePaths) {
|
|
262
|
+
const keys = fieldPath.split(".");
|
|
263
|
+
let current = sanitizedConfig;
|
|
264
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
265
|
+
if (current[keys[i]] && typeof current[keys[i]] === "object") {
|
|
266
|
+
current = current[keys[i]];
|
|
267
|
+
} else {
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const lastKey = keys[keys.length - 1];
|
|
272
|
+
if (lastKey in current) {
|
|
273
|
+
sensitiveFields[lastKey] = current[lastKey];
|
|
274
|
+
delete current[lastKey];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const content = stringifyYaml(sanitizedConfig, {
|
|
174
278
|
indent: 2,
|
|
175
279
|
lineWidth: 120
|
|
176
280
|
});
|
|
177
281
|
writeFileSync(configPath, content, "utf-8");
|
|
282
|
+
if (shouldEncrypt && Object.keys(sensitiveFields).length > 0) {
|
|
283
|
+
try {
|
|
284
|
+
const password = getEncryptionPassword();
|
|
285
|
+
const encryptedSecrets = encrypt(JSON.stringify(sensitiveFields), password);
|
|
286
|
+
writeFileSync(secretsPath, encryptedSecrets, {
|
|
287
|
+
mode: 384,
|
|
288
|
+
encoding: "utf-8"
|
|
289
|
+
});
|
|
290
|
+
console.log(`Sensitive configuration encrypted and saved to ${SECRETS_FILE}`);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.warn(`Failed to encrypt secrets: ${error instanceof Error ? error.message : String(error)}`);
|
|
293
|
+
console.warn("Secrets were not saved. Set CONTEXTOS_ENCRYPTION_KEY environment variable.");
|
|
294
|
+
}
|
|
295
|
+
} else if (Object.keys(sensitiveFields).length > 0 && !shouldEncrypt) {
|
|
296
|
+
console.warn("Saving secrets in plain text (not recommended)");
|
|
297
|
+
writeFileSync(secretsPath, JSON.stringify(sensitiveFields, null, 2), {
|
|
298
|
+
mode: 384,
|
|
299
|
+
encoding: "utf-8"
|
|
300
|
+
});
|
|
301
|
+
}
|
|
178
302
|
}
|
|
179
303
|
function isInitialized(startDir = process.cwd()) {
|
|
180
|
-
|
|
304
|
+
const rootDir = findContextosRoot(startDir);
|
|
305
|
+
if (!rootDir) return false;
|
|
306
|
+
const contextPath = join(rootDir, CONTEXTOS_DIR, CONTEXT_FILE);
|
|
307
|
+
return existsSync(contextPath);
|
|
181
308
|
}
|
|
182
309
|
|
|
183
310
|
// src/parser/tree-sitter.ts
|
|
@@ -432,12 +559,29 @@ var ASTParser = class {
|
|
|
432
559
|
}
|
|
433
560
|
};
|
|
434
561
|
var parserInstance = null;
|
|
562
|
+
var initializationPromise = null;
|
|
435
563
|
async function getParser() {
|
|
436
|
-
if (
|
|
437
|
-
parserInstance
|
|
438
|
-
|
|
564
|
+
if (parserInstance) {
|
|
565
|
+
return parserInstance;
|
|
566
|
+
}
|
|
567
|
+
if (!initializationPromise) {
|
|
568
|
+
initializationPromise = (async () => {
|
|
569
|
+
try {
|
|
570
|
+
const instance = new ASTParser();
|
|
571
|
+
await instance.initialize();
|
|
572
|
+
parserInstance = instance;
|
|
573
|
+
return instance;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
initializationPromise = null;
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
})();
|
|
439
579
|
}
|
|
440
|
-
return
|
|
580
|
+
return initializationPromise;
|
|
581
|
+
}
|
|
582
|
+
function resetParser() {
|
|
583
|
+
parserInstance = null;
|
|
584
|
+
initializationPromise = null;
|
|
441
585
|
}
|
|
442
586
|
|
|
443
587
|
// src/parser/detector.ts
|
|
@@ -1041,7 +1185,6 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
|
1041
1185
|
import { dirname as dirname3 } from "path";
|
|
1042
1186
|
var VectorStore = class {
|
|
1043
1187
|
db = null;
|
|
1044
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1045
1188
|
embedder = null;
|
|
1046
1189
|
dbPath;
|
|
1047
1190
|
constructor(dbPath) {
|
|
@@ -1078,13 +1221,16 @@ var VectorStore = class {
|
|
|
1078
1221
|
}
|
|
1079
1222
|
/**
|
|
1080
1223
|
* Initialize the embedding model
|
|
1224
|
+
* Fixed: Better error handling for dynamic import failures
|
|
1081
1225
|
*/
|
|
1082
1226
|
async initializeEmbedder() {
|
|
1083
1227
|
try {
|
|
1084
1228
|
const { pipeline } = await import("@xenova/transformers");
|
|
1085
1229
|
this.embedder = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
1086
1230
|
} catch (error) {
|
|
1087
|
-
|
|
1231
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1232
|
+
console.warn(`Failed to initialize embedding model: ${errorMessage}`);
|
|
1233
|
+
console.warn("Vector search will use text-based fallback (less accurate).");
|
|
1088
1234
|
this.embedder = null;
|
|
1089
1235
|
}
|
|
1090
1236
|
}
|
|
@@ -1156,54 +1302,76 @@ var VectorStore = class {
|
|
|
1156
1302
|
}
|
|
1157
1303
|
/**
|
|
1158
1304
|
* Vector-based similarity search
|
|
1305
|
+
* Fixed: Uses pagination to prevent unbounded memory growth
|
|
1159
1306
|
*/
|
|
1160
1307
|
vectorSearch(queryEmbedding, limit) {
|
|
1161
1308
|
if (!this.db) throw new Error("VectorStore not initialized");
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1309
|
+
const pageSize = 1e3;
|
|
1310
|
+
let offset = 0;
|
|
1311
|
+
const allResults = [];
|
|
1312
|
+
while (allResults.length < limit * 2) {
|
|
1313
|
+
const chunks = this.db.prepare(`
|
|
1314
|
+
SELECT id, file_path, content, start_line, end_line, embedding
|
|
1315
|
+
FROM chunks
|
|
1316
|
+
WHERE embedding IS NOT NULL
|
|
1317
|
+
LIMIT ? OFFSET ?
|
|
1318
|
+
`).all(pageSize, offset);
|
|
1319
|
+
if (chunks.length === 0) break;
|
|
1320
|
+
const batchResults = chunks.map((chunk) => {
|
|
1321
|
+
const chunkEmbedding = new Float32Array(chunk.embedding.buffer);
|
|
1322
|
+
const score = cosineSimilarity(queryEmbedding, chunkEmbedding);
|
|
1323
|
+
return {
|
|
1324
|
+
chunkId: chunk.id,
|
|
1325
|
+
filePath: chunk.file_path,
|
|
1326
|
+
content: chunk.content,
|
|
1327
|
+
score,
|
|
1328
|
+
lines: [chunk.start_line, chunk.end_line]
|
|
1329
|
+
};
|
|
1330
|
+
});
|
|
1331
|
+
allResults.push(...batchResults);
|
|
1332
|
+
offset += pageSize;
|
|
1333
|
+
if (chunks.length < pageSize) break;
|
|
1334
|
+
}
|
|
1335
|
+
return allResults.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1179
1336
|
}
|
|
1180
1337
|
/**
|
|
1181
1338
|
* Text-based fallback search
|
|
1339
|
+
* Fixed: Uses pagination to prevent unbounded memory growth
|
|
1182
1340
|
*/
|
|
1183
1341
|
textSearch(query, limit) {
|
|
1184
1342
|
if (!this.db) throw new Error("VectorStore not initialized");
|
|
1185
1343
|
const terms = query.toLowerCase().split(/\s+/);
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1344
|
+
const pageSize = 1e3;
|
|
1345
|
+
let offset = 0;
|
|
1346
|
+
const allResults = [];
|
|
1347
|
+
while (allResults.length < limit * 2) {
|
|
1348
|
+
const chunks = this.db.prepare(`
|
|
1349
|
+
SELECT id, file_path, content, start_line, end_line
|
|
1350
|
+
FROM chunks
|
|
1351
|
+
LIMIT ? OFFSET ?
|
|
1352
|
+
`).all(pageSize, offset);
|
|
1353
|
+
if (chunks.length === 0) break;
|
|
1354
|
+
const batchResults = chunks.map((chunk) => {
|
|
1355
|
+
const contentLower = chunk.content.toLowerCase();
|
|
1356
|
+
let score = 0;
|
|
1357
|
+
for (const term of terms) {
|
|
1358
|
+
const matches = (contentLower.match(new RegExp(term, "g")) || []).length;
|
|
1359
|
+
score += matches;
|
|
1360
|
+
}
|
|
1361
|
+
score = Math.min(1, score / (terms.length * 2));
|
|
1362
|
+
return {
|
|
1363
|
+
chunkId: chunk.id,
|
|
1364
|
+
filePath: chunk.file_path,
|
|
1365
|
+
content: chunk.content,
|
|
1366
|
+
score,
|
|
1367
|
+
lines: [chunk.start_line, chunk.end_line]
|
|
1368
|
+
};
|
|
1369
|
+
});
|
|
1370
|
+
allResults.push(...batchResults);
|
|
1371
|
+
offset += pageSize;
|
|
1372
|
+
if (chunks.length < pageSize) break;
|
|
1373
|
+
}
|
|
1374
|
+
return allResults.filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1207
1375
|
}
|
|
1208
1376
|
/**
|
|
1209
1377
|
* Remove chunks for a file
|
|
@@ -1251,6 +1419,39 @@ var VectorStore = class {
|
|
|
1251
1419
|
embeddedCount: stats.embedded_count
|
|
1252
1420
|
};
|
|
1253
1421
|
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Get paginated chunks
|
|
1424
|
+
* Fixed: Added pagination to prevent loading all chunks at once
|
|
1425
|
+
*/
|
|
1426
|
+
getChunksPaginated(page = 0, pageSize = 100) {
|
|
1427
|
+
if (!this.db) throw new Error("VectorStore not initialized");
|
|
1428
|
+
const totalResult = this.db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
1429
|
+
const total = totalResult.count;
|
|
1430
|
+
const rows = this.db.prepare(`
|
|
1431
|
+
SELECT id, file_path, content, start_line, end_line, hash, type
|
|
1432
|
+
FROM chunks
|
|
1433
|
+
ORDER BY file_path, start_line
|
|
1434
|
+
LIMIT ? OFFSET ?
|
|
1435
|
+
`).all(pageSize, page * pageSize);
|
|
1436
|
+
const chunks = rows.map((row) => ({
|
|
1437
|
+
id: row.id,
|
|
1438
|
+
filePath: row.file_path,
|
|
1439
|
+
content: row.content,
|
|
1440
|
+
startLine: row.start_line,
|
|
1441
|
+
endLine: row.end_line,
|
|
1442
|
+
hash: row.hash,
|
|
1443
|
+
type: row.type
|
|
1444
|
+
}));
|
|
1445
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
1446
|
+
return {
|
|
1447
|
+
chunks,
|
|
1448
|
+
total,
|
|
1449
|
+
page,
|
|
1450
|
+
pageSize,
|
|
1451
|
+
totalPages,
|
|
1452
|
+
hasMore: page * pageSize + chunks.length < total
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1254
1455
|
/**
|
|
1255
1456
|
* Clear all data
|
|
1256
1457
|
*/
|
|
@@ -1714,11 +1915,110 @@ ${content}`;
|
|
|
1714
1915
|
};
|
|
1715
1916
|
|
|
1716
1917
|
// src/context/builder.ts
|
|
1717
|
-
import {
|
|
1718
|
-
import {
|
|
1918
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1919
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
1920
|
+
import { join as join4, relative, normalize } from "path";
|
|
1719
1921
|
import { glob } from "glob";
|
|
1720
1922
|
import { stringify } from "yaml";
|
|
1721
1923
|
|
|
1924
|
+
// src/llm/rate-limiter.ts
|
|
1925
|
+
var RateLimiter = class {
|
|
1926
|
+
requests = [];
|
|
1927
|
+
// Timestamps of requests
|
|
1928
|
+
maxRequests;
|
|
1929
|
+
windowMs;
|
|
1930
|
+
constructor(options) {
|
|
1931
|
+
this.maxRequests = options.requestsPerMinute;
|
|
1932
|
+
this.windowMs = options.windowMs || 6e4;
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Check if a request is allowed and record it
|
|
1936
|
+
* @returns Rate limit info
|
|
1937
|
+
*/
|
|
1938
|
+
async checkLimit() {
|
|
1939
|
+
const now = Date.now();
|
|
1940
|
+
this.requests = this.requests.filter((timestamp) => now - timestamp < this.windowMs);
|
|
1941
|
+
if (this.requests.length >= this.maxRequests) {
|
|
1942
|
+
const oldestRequest = this.requests[0];
|
|
1943
|
+
const waitTime = this.windowMs - (now - oldestRequest);
|
|
1944
|
+
return {
|
|
1945
|
+
allowed: false,
|
|
1946
|
+
waitTime,
|
|
1947
|
+
remaining: 0,
|
|
1948
|
+
resetAt: oldestRequest + this.windowMs
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
this.requests.push(now);
|
|
1952
|
+
return {
|
|
1953
|
+
allowed: true,
|
|
1954
|
+
waitTime: 0,
|
|
1955
|
+
remaining: this.maxRequests - this.requests.length,
|
|
1956
|
+
resetAt: this.requests[0] ? this.requests[0] + this.windowMs : now + this.windowMs
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Wait until a request is allowed (blocking)
|
|
1961
|
+
* Use this for automatic retry with backoff
|
|
1962
|
+
*/
|
|
1963
|
+
async waitForSlot() {
|
|
1964
|
+
const result = await this.checkLimit();
|
|
1965
|
+
if (!result.allowed) {
|
|
1966
|
+
await new Promise((resolve2) => setTimeout(resolve2, result.waitTime + 100));
|
|
1967
|
+
return this.waitForSlot();
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Reset the rate limiter (clear all history)
|
|
1972
|
+
*/
|
|
1973
|
+
reset() {
|
|
1974
|
+
this.requests = [];
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Get current statistics
|
|
1978
|
+
*/
|
|
1979
|
+
getStats() {
|
|
1980
|
+
const now = Date.now();
|
|
1981
|
+
const currentRequests = this.requests.filter((t) => now - t < this.windowMs).length;
|
|
1982
|
+
return {
|
|
1983
|
+
currentRequests,
|
|
1984
|
+
maxRequests: this.maxRequests,
|
|
1985
|
+
windowMs: this.windowMs
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
var DEFAULT_RATE_LIMITS = {
|
|
1990
|
+
// OpenAI (GPT-4, GPT-3.5)
|
|
1991
|
+
"openai": 50,
|
|
1992
|
+
// Free tier: 3 RPM, Paid: 50-5000 RPM depending on model
|
|
1993
|
+
"gpt-4": 10,
|
|
1994
|
+
"gpt-4-turbo": 50,
|
|
1995
|
+
"gpt-3.5-turbo": 200,
|
|
1996
|
+
// Anthropic (Claude)
|
|
1997
|
+
"anthropic": 50,
|
|
1998
|
+
// Claude 3 Sonnet: 50 RPM default
|
|
1999
|
+
"claude-3-opus": 5,
|
|
2000
|
+
"claude-3-sonnet": 50,
|
|
2001
|
+
"claude-3-haiku": 200,
|
|
2002
|
+
// Google (Gemini)
|
|
2003
|
+
"gemini": 60,
|
|
2004
|
+
// Gemini Pro: 60 RPM
|
|
2005
|
+
"gemini-pro": 60,
|
|
2006
|
+
"gemini-flash": 150,
|
|
2007
|
+
// Local models (no rate limit)
|
|
2008
|
+
"local": 999999,
|
|
2009
|
+
"ollama": 999999,
|
|
2010
|
+
"lm-studio": 999999
|
|
2011
|
+
};
|
|
2012
|
+
function getDefaultRateLimit(provider, model) {
|
|
2013
|
+
if (model && model in DEFAULT_RATE_LIMITS) {
|
|
2014
|
+
return DEFAULT_RATE_LIMITS[model];
|
|
2015
|
+
}
|
|
2016
|
+
if (provider in DEFAULT_RATE_LIMITS) {
|
|
2017
|
+
return DEFAULT_RATE_LIMITS[provider];
|
|
2018
|
+
}
|
|
2019
|
+
return 60;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1722
2022
|
// src/llm/gemini-client.ts
|
|
1723
2023
|
var DEFAULT_MODEL = "gemini-3-pro-preview";
|
|
1724
2024
|
var GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
@@ -1728,12 +2028,15 @@ var GeminiClient = class {
|
|
|
1728
2028
|
maxOutputTokens;
|
|
1729
2029
|
temperature;
|
|
1730
2030
|
thinkingLevel;
|
|
2031
|
+
rateLimiter;
|
|
1731
2032
|
constructor(config) {
|
|
1732
2033
|
this.apiKey = config.apiKey;
|
|
1733
2034
|
this.model = config.model || DEFAULT_MODEL;
|
|
1734
2035
|
this.maxOutputTokens = config.maxOutputTokens || 2048;
|
|
1735
2036
|
this.temperature = 1;
|
|
1736
2037
|
this.thinkingLevel = "high";
|
|
2038
|
+
const rateLimit = getDefaultRateLimit("gemini", this.model);
|
|
2039
|
+
this.rateLimiter = new RateLimiter({ requestsPerMinute: rateLimit });
|
|
1737
2040
|
}
|
|
1738
2041
|
/**
|
|
1739
2042
|
* Check if API key is configured
|
|
@@ -1743,11 +2046,13 @@ var GeminiClient = class {
|
|
|
1743
2046
|
}
|
|
1744
2047
|
/**
|
|
1745
2048
|
* Make a request to Gemini API
|
|
2049
|
+
* Fixed: Added rate limiting and retry logic for 429 responses
|
|
1746
2050
|
*/
|
|
1747
|
-
async request(prompt, systemPrompt) {
|
|
2051
|
+
async request(prompt, systemPrompt, retryCount = 0) {
|
|
1748
2052
|
if (!this.apiKey) {
|
|
1749
2053
|
throw new Error("Gemini API key not configured. Set GEMINI_API_KEY environment variable.");
|
|
1750
2054
|
}
|
|
2055
|
+
await this.rateLimiter.waitForSlot();
|
|
1751
2056
|
const url = `${GEMINI_API_URL}/${this.model}:generateContent?key=${this.apiKey}`;
|
|
1752
2057
|
const contents = [];
|
|
1753
2058
|
if (systemPrompt) {
|
|
@@ -1781,6 +2086,16 @@ var GeminiClient = class {
|
|
|
1781
2086
|
}
|
|
1782
2087
|
})
|
|
1783
2088
|
});
|
|
2089
|
+
if (response.status === 429) {
|
|
2090
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
2091
|
+
const waitTime = retryAfter ? parseInt(retryAfter) * 1e3 : 6e4;
|
|
2092
|
+
if (retryCount < 3) {
|
|
2093
|
+
console.warn(`Rate limited. Waiting ${waitTime}ms before retry (${retryCount + 1}/3)...`);
|
|
2094
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitTime));
|
|
2095
|
+
return this.request(prompt, systemPrompt, retryCount + 1);
|
|
2096
|
+
}
|
|
2097
|
+
throw new Error(`Rate limit exceeded after ${retryCount} retries. Please try again later.`);
|
|
2098
|
+
}
|
|
1784
2099
|
if (!response.ok) {
|
|
1785
2100
|
const error = await response.text();
|
|
1786
2101
|
throw new Error(`Gemini API error: ${response.status} - ${error}`);
|
|
@@ -1991,6 +2306,43 @@ var ContextBuilder = class {
|
|
|
1991
2306
|
this.graph = new DependencyGraph();
|
|
1992
2307
|
this.budget = new TokenBudget();
|
|
1993
2308
|
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Sanitize git directory path to prevent command injection
|
|
2311
|
+
*/
|
|
2312
|
+
sanitizeGitPath(cwd) {
|
|
2313
|
+
if (!cwd) return process.cwd();
|
|
2314
|
+
const normalized = normalize(cwd);
|
|
2315
|
+
if (normalized.includes("\0") || normalized.includes("\n") || normalized.includes("\r")) {
|
|
2316
|
+
throw new Error("Invalid git directory path: contains forbidden characters");
|
|
2317
|
+
}
|
|
2318
|
+
return normalized;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Execute git diff with proper sanitization and error handling
|
|
2322
|
+
*/
|
|
2323
|
+
async getGitDiff(type) {
|
|
2324
|
+
try {
|
|
2325
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
2326
|
+
const cwd = this.sanitizeGitPath(this.config?.rootDir);
|
|
2327
|
+
const flag = type === "cached" ? "--cached" : "";
|
|
2328
|
+
return execSync2(
|
|
2329
|
+
`git diff ${flag} --name-only`,
|
|
2330
|
+
{
|
|
2331
|
+
cwd,
|
|
2332
|
+
encoding: "utf-8",
|
|
2333
|
+
maxBuffer: 1024 * 1024,
|
|
2334
|
+
// 1MB
|
|
2335
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2336
|
+
timeout: 5e3
|
|
2337
|
+
// 5 second timeout
|
|
2338
|
+
}
|
|
2339
|
+
).trim();
|
|
2340
|
+
} catch (error) {
|
|
2341
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2342
|
+
console.warn(`Git command failed: ${errorMessage}`);
|
|
2343
|
+
return "";
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
1994
2346
|
/**
|
|
1995
2347
|
* Initialize the context builder for a project
|
|
1996
2348
|
*/
|
|
@@ -2002,8 +2354,13 @@ var ContextBuilder = class {
|
|
|
2002
2354
|
await this.vectorStore.initialize();
|
|
2003
2355
|
const graphPath = join4(this.config.rootDir, CONTEXTOS_DIR2, GRAPH_FILE);
|
|
2004
2356
|
if (existsSync4(graphPath)) {
|
|
2005
|
-
|
|
2006
|
-
|
|
2357
|
+
try {
|
|
2358
|
+
const graphContent = await readFile(graphPath, "utf-8");
|
|
2359
|
+
const graphData = JSON.parse(graphContent);
|
|
2360
|
+
this.graph.fromJSON(graphData);
|
|
2361
|
+
} catch (error) {
|
|
2362
|
+
console.warn(`Failed to load dependency graph: ${error instanceof Error ? error.message : String(error)}`);
|
|
2363
|
+
}
|
|
2007
2364
|
}
|
|
2008
2365
|
this.ranker = new HybridRanker(
|
|
2009
2366
|
this.vectorStore,
|
|
@@ -2040,7 +2397,7 @@ var ContextBuilder = class {
|
|
|
2040
2397
|
const parser = await getParser();
|
|
2041
2398
|
for (const filePath of files) {
|
|
2042
2399
|
try {
|
|
2043
|
-
const content =
|
|
2400
|
+
const content = await readFile(filePath, "utf-8");
|
|
2044
2401
|
const relativePath = relative(rootDir, filePath);
|
|
2045
2402
|
if (!force && !this.graph.hasChanged(relativePath, content)) {
|
|
2046
2403
|
continue;
|
|
@@ -2063,11 +2420,9 @@ var ContextBuilder = class {
|
|
|
2063
2420
|
const graphPath = join4(rootDir, CONTEXTOS_DIR2, GRAPH_FILE);
|
|
2064
2421
|
const graphDir = join4(rootDir, CONTEXTOS_DIR2, "db");
|
|
2065
2422
|
if (!existsSync4(graphDir)) {
|
|
2066
|
-
|
|
2067
|
-
mkdirSync9(graphDir, { recursive: true });
|
|
2423
|
+
await mkdir(graphDir, { recursive: true });
|
|
2068
2424
|
}
|
|
2069
|
-
|
|
2070
|
-
writeFileSync9(graphPath, JSON.stringify(this.graph.toJSON(), null, 2));
|
|
2425
|
+
await writeFile(graphPath, JSON.stringify(this.graph.toJSON(), null, 2), "utf-8");
|
|
2071
2426
|
return {
|
|
2072
2427
|
filesIndexed,
|
|
2073
2428
|
chunksCreated,
|
|
@@ -2103,37 +2458,38 @@ var ContextBuilder = class {
|
|
|
2103
2458
|
}
|
|
2104
2459
|
/**
|
|
2105
2460
|
* Infer goal from git diff, enhanced with Gemini when available
|
|
2461
|
+
* Fixed: Uses sanitized git commands to prevent command injection
|
|
2106
2462
|
*/
|
|
2107
2463
|
async inferGoal() {
|
|
2108
2464
|
let gitDiff = "";
|
|
2109
2465
|
let recentFiles = [];
|
|
2110
2466
|
try {
|
|
2111
2467
|
const { execSync: execSync2 } = await import("child_process");
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
});
|
|
2116
|
-
recentFiles = staged.trim().split("\n").filter(Boolean);
|
|
2468
|
+
const cwd = this.sanitizeGitPath(this.config?.rootDir);
|
|
2469
|
+
const staged = await this.getGitDiff("cached");
|
|
2470
|
+
recentFiles = staged.split("\n").filter(Boolean);
|
|
2117
2471
|
if (recentFiles.length === 0) {
|
|
2118
|
-
const uncommitted =
|
|
2119
|
-
|
|
2120
|
-
encoding: "utf-8"
|
|
2121
|
-
});
|
|
2122
|
-
recentFiles = uncommitted.trim().split("\n").filter(Boolean);
|
|
2472
|
+
const uncommitted = await this.getGitDiff("working");
|
|
2473
|
+
recentFiles = uncommitted.split("\n").filter(Boolean);
|
|
2123
2474
|
}
|
|
2124
2475
|
if (recentFiles.length > 0) {
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
maxBuffer: 1024 * 1024
|
|
2129
|
-
// 1MB max
|
|
2130
|
-
});
|
|
2131
|
-
if (!gitDiff) {
|
|
2132
|
-
gitDiff = execSync2("git diff", {
|
|
2133
|
-
cwd: this.config?.rootDir,
|
|
2476
|
+
try {
|
|
2477
|
+
gitDiff = execSync2("git diff --cached", {
|
|
2478
|
+
cwd,
|
|
2134
2479
|
encoding: "utf-8",
|
|
2135
|
-
maxBuffer: 1024 * 1024
|
|
2480
|
+
maxBuffer: 1024 * 1024,
|
|
2481
|
+
// 1MB max
|
|
2482
|
+
timeout: 5e3
|
|
2136
2483
|
});
|
|
2484
|
+
if (!gitDiff) {
|
|
2485
|
+
gitDiff = execSync2("git diff", {
|
|
2486
|
+
cwd,
|
|
2487
|
+
encoding: "utf-8",
|
|
2488
|
+
maxBuffer: 1024 * 1024,
|
|
2489
|
+
timeout: 5e3
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
} catch {
|
|
2137
2493
|
}
|
|
2138
2494
|
}
|
|
2139
2495
|
} catch {
|
|
@@ -2220,16 +2576,34 @@ var ContextBuilder = class {
|
|
|
2220
2576
|
}
|
|
2221
2577
|
};
|
|
2222
2578
|
var builderInstance = null;
|
|
2579
|
+
var initializationPromise2 = null;
|
|
2223
2580
|
async function getContextBuilder(projectDir) {
|
|
2224
|
-
if (
|
|
2225
|
-
builderInstance
|
|
2226
|
-
await builderInstance.initialize(projectDir);
|
|
2581
|
+
if (builderInstance) {
|
|
2582
|
+
return builderInstance;
|
|
2227
2583
|
}
|
|
2228
|
-
|
|
2584
|
+
if (!initializationPromise2) {
|
|
2585
|
+
initializationPromise2 = (async () => {
|
|
2586
|
+
try {
|
|
2587
|
+
const instance = new ContextBuilder();
|
|
2588
|
+
await instance.initialize(projectDir);
|
|
2589
|
+
builderInstance = instance;
|
|
2590
|
+
return instance;
|
|
2591
|
+
} catch (error) {
|
|
2592
|
+
initializationPromise2 = null;
|
|
2593
|
+
throw error;
|
|
2594
|
+
}
|
|
2595
|
+
})();
|
|
2596
|
+
}
|
|
2597
|
+
return initializationPromise2;
|
|
2598
|
+
}
|
|
2599
|
+
function resetContextBuilder() {
|
|
2600
|
+
builderInstance?.close();
|
|
2601
|
+
builderInstance = null;
|
|
2602
|
+
initializationPromise2 = null;
|
|
2229
2603
|
}
|
|
2230
2604
|
|
|
2231
2605
|
// src/doctor/drift-detector.ts
|
|
2232
|
-
import {
|
|
2606
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2233
2607
|
import { glob as glob2 } from "glob";
|
|
2234
2608
|
var TECH_PATTERNS = {
|
|
2235
2609
|
// Databases
|
|
@@ -2408,33 +2782,40 @@ var DriftDetector = class {
|
|
|
2408
2782
|
}
|
|
2409
2783
|
/**
|
|
2410
2784
|
* Check a specific constraint
|
|
2785
|
+
* Fix N9: Convert to async file operations
|
|
2411
2786
|
*/
|
|
2412
2787
|
async checkConstraint(rule) {
|
|
2413
2788
|
const violations = [];
|
|
2414
2789
|
if (rule.toLowerCase().includes("no direct database access in controllers")) {
|
|
2415
2790
|
for (const file of this.sourceFiles) {
|
|
2416
2791
|
if (file.includes("controller")) {
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2792
|
+
try {
|
|
2793
|
+
const content = await readFile2(file, "utf-8");
|
|
2794
|
+
if (/import.*from.*(prisma|typeorm|sequelize|mongoose)/i.test(content)) {
|
|
2795
|
+
const lines = content.split("\n");
|
|
2796
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2797
|
+
if (/import.*from.*(prisma|typeorm|sequelize|mongoose)/i.test(lines[i])) {
|
|
2798
|
+
violations.push({ file, line: i + 1 });
|
|
2799
|
+
break;
|
|
2800
|
+
}
|
|
2424
2801
|
}
|
|
2425
2802
|
}
|
|
2803
|
+
} catch {
|
|
2426
2804
|
}
|
|
2427
2805
|
}
|
|
2428
2806
|
}
|
|
2429
2807
|
}
|
|
2430
2808
|
if (rule.toLowerCase().includes("no console.log")) {
|
|
2431
2809
|
for (const file of this.sourceFiles) {
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2810
|
+
try {
|
|
2811
|
+
const content = await readFile2(file, "utf-8");
|
|
2812
|
+
const lines = content.split("\n");
|
|
2813
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2814
|
+
if (/console\.log\(/.test(lines[i]) && !lines[i].includes("//")) {
|
|
2815
|
+
violations.push({ file, line: i + 1 });
|
|
2816
|
+
}
|
|
2437
2817
|
}
|
|
2818
|
+
} catch {
|
|
2438
2819
|
}
|
|
2439
2820
|
}
|
|
2440
2821
|
}
|
|
@@ -2458,12 +2839,13 @@ var DriftDetector = class {
|
|
|
2458
2839
|
}
|
|
2459
2840
|
/**
|
|
2460
2841
|
* Find patterns in files
|
|
2842
|
+
* Fix N9: Convert to async file operations
|
|
2461
2843
|
*/
|
|
2462
2844
|
async findPatternInFiles(patterns) {
|
|
2463
2845
|
const results = [];
|
|
2464
2846
|
for (const file of this.sourceFiles) {
|
|
2465
2847
|
try {
|
|
2466
|
-
const content =
|
|
2848
|
+
const content = await readFile2(file, "utf-8");
|
|
2467
2849
|
const lines = content.split("\n");
|
|
2468
2850
|
for (let i = 0; i < lines.length; i++) {
|
|
2469
2851
|
for (const pattern of patterns) {
|
|
@@ -2639,16 +3021,19 @@ var OpenAIAdapter = class {
|
|
|
2639
3021
|
apiKey;
|
|
2640
3022
|
model;
|
|
2641
3023
|
baseUrl;
|
|
3024
|
+
rateLimiter;
|
|
2642
3025
|
constructor(options = {}) {
|
|
2643
3026
|
this.apiKey = options.apiKey || process.env.OPENAI_API_KEY || "";
|
|
2644
3027
|
this.model = options.model || "gpt-5.2";
|
|
2645
3028
|
this.baseUrl = options.baseUrl || "https://api.openai.com/v1";
|
|
2646
3029
|
this.maxContextTokens = getModelContextSize("openai", this.model);
|
|
3030
|
+
const rateLimit = options.requestsPerMinute || getDefaultRateLimit("openai", this.model);
|
|
3031
|
+
this.rateLimiter = new RateLimiter({ requestsPerMinute: rateLimit });
|
|
2647
3032
|
if (!this.apiKey) {
|
|
2648
3033
|
console.warn("OpenAI API key not set. Set OPENAI_API_KEY environment variable.");
|
|
2649
3034
|
}
|
|
2650
3035
|
}
|
|
2651
|
-
async complete(request) {
|
|
3036
|
+
async complete(request, retryCount = 0) {
|
|
2652
3037
|
if (!this.apiKey) {
|
|
2653
3038
|
return {
|
|
2654
3039
|
content: "",
|
|
@@ -2657,6 +3042,7 @@ var OpenAIAdapter = class {
|
|
|
2657
3042
|
error: "OpenAI API key not configured"
|
|
2658
3043
|
};
|
|
2659
3044
|
}
|
|
3045
|
+
await this.rateLimiter.waitForSlot();
|
|
2660
3046
|
try {
|
|
2661
3047
|
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
2662
3048
|
method: "POST",
|
|
@@ -2675,11 +3061,29 @@ var OpenAIAdapter = class {
|
|
|
2675
3061
|
stop: request.stopSequences
|
|
2676
3062
|
})
|
|
2677
3063
|
});
|
|
3064
|
+
if (response.status === 429) {
|
|
3065
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
3066
|
+
const waitTime = retryAfter ? parseInt(retryAfter) * 1e3 : 6e4;
|
|
3067
|
+
if (retryCount < 3) {
|
|
3068
|
+
console.warn(`OpenAI rate limited. Waiting ${waitTime}ms before retry (${retryCount + 1}/3)...`);
|
|
3069
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitTime));
|
|
3070
|
+
return this.complete(request, retryCount + 1);
|
|
3071
|
+
}
|
|
3072
|
+
return {
|
|
3073
|
+
content: "",
|
|
3074
|
+
tokensUsed: { prompt: 0, completion: 0, total: 0 },
|
|
3075
|
+
finishReason: "error",
|
|
3076
|
+
error: `Rate limit exceeded after ${retryCount} retries`
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
2678
3079
|
if (!response.ok) {
|
|
2679
3080
|
const errorData = await response.json().catch(() => ({}));
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
3081
|
+
return {
|
|
3082
|
+
content: "",
|
|
3083
|
+
tokensUsed: { prompt: 0, completion: 0, total: 0 },
|
|
3084
|
+
finishReason: "error",
|
|
3085
|
+
error: `OpenAI API error: ${response.status} - ${JSON.stringify(errorData)}`
|
|
3086
|
+
};
|
|
2683
3087
|
}
|
|
2684
3088
|
const data = await response.json();
|
|
2685
3089
|
const choice = data.choices?.[0];
|
|
@@ -2719,13 +3123,16 @@ var AnthropicAdapter = class {
|
|
|
2719
3123
|
apiKey;
|
|
2720
3124
|
model;
|
|
2721
3125
|
baseUrl;
|
|
3126
|
+
rateLimiter;
|
|
2722
3127
|
constructor(options = {}) {
|
|
2723
3128
|
this.apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY || "";
|
|
2724
3129
|
this.model = options.model || "claude-4.5-opus-20260115";
|
|
2725
3130
|
this.baseUrl = options.baseUrl || "https://api.anthropic.com";
|
|
2726
3131
|
this.maxContextTokens = getModelContextSize("anthropic", this.model);
|
|
3132
|
+
const rateLimit = options.requestsPerMinute || getDefaultRateLimit("anthropic", this.model);
|
|
3133
|
+
this.rateLimiter = new RateLimiter({ requestsPerMinute: rateLimit });
|
|
2727
3134
|
}
|
|
2728
|
-
async complete(request) {
|
|
3135
|
+
async complete(request, retryCount = 0) {
|
|
2729
3136
|
if (!this.apiKey) {
|
|
2730
3137
|
return {
|
|
2731
3138
|
content: "",
|
|
@@ -2734,6 +3141,7 @@ var AnthropicAdapter = class {
|
|
|
2734
3141
|
error: "Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable."
|
|
2735
3142
|
};
|
|
2736
3143
|
}
|
|
3144
|
+
await this.rateLimiter.waitForSlot();
|
|
2737
3145
|
try {
|
|
2738
3146
|
const response = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
2739
3147
|
method: "POST",
|
|
@@ -2753,11 +3161,29 @@ var AnthropicAdapter = class {
|
|
|
2753
3161
|
stop_sequences: request.stopSequences
|
|
2754
3162
|
})
|
|
2755
3163
|
});
|
|
3164
|
+
if (response.status === 429) {
|
|
3165
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
3166
|
+
const waitTime = retryAfter ? parseInt(retryAfter) * 1e3 : 6e4;
|
|
3167
|
+
if (retryCount < 3) {
|
|
3168
|
+
console.warn(`Anthropic rate limited. Waiting ${waitTime}ms before retry (${retryCount + 1}/3)...`);
|
|
3169
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitTime));
|
|
3170
|
+
return this.complete(request, retryCount + 1);
|
|
3171
|
+
}
|
|
3172
|
+
return {
|
|
3173
|
+
content: "",
|
|
3174
|
+
tokensUsed: { prompt: 0, completion: 0, total: 0 },
|
|
3175
|
+
finishReason: "error",
|
|
3176
|
+
error: `Rate limit exceeded after ${retryCount} retries`
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
2756
3179
|
if (!response.ok) {
|
|
2757
3180
|
const errorData = await response.json().catch(() => ({}));
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
3181
|
+
return {
|
|
3182
|
+
content: "",
|
|
3183
|
+
tokensUsed: { prompt: 0, completion: 0, total: 0 },
|
|
3184
|
+
finishReason: "error",
|
|
3185
|
+
error: `Anthropic API error: ${response.status} - ${JSON.stringify(errorData)}`
|
|
3186
|
+
};
|
|
2761
3187
|
}
|
|
2762
3188
|
const data = await response.json();
|
|
2763
3189
|
const content = data.content?.filter((c) => c.type === "text")?.map((c) => c.text || "")?.join("") || "";
|
|
@@ -3040,6 +3466,13 @@ function createContextAPI(rawContext) {
|
|
|
3040
3466
|
}).filter(Boolean);
|
|
3041
3467
|
},
|
|
3042
3468
|
getFile: (path) => {
|
|
3469
|
+
const MAX_PATH_LENGTH = 1e3;
|
|
3470
|
+
if (path.length > MAX_PATH_LENGTH) {
|
|
3471
|
+
return null;
|
|
3472
|
+
}
|
|
3473
|
+
if (!/^[\w\-./\\]+$/.test(path)) {
|
|
3474
|
+
return null;
|
|
3475
|
+
}
|
|
3043
3476
|
const escapedPath = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3044
3477
|
const pattern = new RegExp(
|
|
3045
3478
|
`^={3,}\\s*FILE:\\s*${escapedPath}\\s*={3,}$([\\s\\S]*?)(?=^={3,}\\s*FILE:|$)`,
|
|
@@ -3478,7 +3911,8 @@ var RLMEngine = class {
|
|
|
3478
3911
|
const state = {
|
|
3479
3912
|
depth,
|
|
3480
3913
|
consumedTokens: 0,
|
|
3481
|
-
visitedPaths: /* @__PURE__ */ new
|
|
3914
|
+
visitedPaths: /* @__PURE__ */ new Map(),
|
|
3915
|
+
// Changed to Map with timestamps for memory leak fix
|
|
3482
3916
|
executionLog: [],
|
|
3483
3917
|
iteration: 0,
|
|
3484
3918
|
startTime
|
|
@@ -3588,7 +4022,9 @@ Depth: ${depth}/${this.config.maxDepth}`
|
|
|
3588
4022
|
entry.output = result.success ? result.output || result.stdout : result.error || "Unknown error";
|
|
3589
4023
|
state.executionLog.push(entry);
|
|
3590
4024
|
const pathKey = action.code.slice(0, 100);
|
|
3591
|
-
|
|
4025
|
+
const now = Date.now();
|
|
4026
|
+
const lastSeen = state.visitedPaths.get(pathKey);
|
|
4027
|
+
if (lastSeen !== void 0) {
|
|
3592
4028
|
messages.push({ role: "assistant", content: response.content });
|
|
3593
4029
|
messages.push({
|
|
3594
4030
|
role: "user",
|
|
@@ -3596,7 +4032,14 @@ Depth: ${depth}/${this.config.maxDepth}`
|
|
|
3596
4032
|
});
|
|
3597
4033
|
continue;
|
|
3598
4034
|
}
|
|
3599
|
-
state.visitedPaths.
|
|
4035
|
+
state.visitedPaths.set(pathKey, now);
|
|
4036
|
+
if (state.visitedPaths.size > 50) {
|
|
4037
|
+
const entries = Array.from(state.visitedPaths.entries());
|
|
4038
|
+
entries.sort((a, b) => a[1] - b[1]);
|
|
4039
|
+
for (let i = 0; i < 10; i++) {
|
|
4040
|
+
state.visitedPaths.delete(entries[i][0]);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
3600
4043
|
messages.push({ role: "assistant", content: response.content });
|
|
3601
4044
|
messages.push({
|
|
3602
4045
|
role: "user",
|
|
@@ -3784,7 +4227,7 @@ function createRLMEngine(options = {}) {
|
|
|
3784
4227
|
}
|
|
3785
4228
|
|
|
3786
4229
|
// src/rlm/proposal.ts
|
|
3787
|
-
import { createHash as createHash3 } from "crypto";
|
|
4230
|
+
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
3788
4231
|
var ProposalManager = class {
|
|
3789
4232
|
proposals = /* @__PURE__ */ new Map();
|
|
3790
4233
|
fileSnapshots = /* @__PURE__ */ new Map();
|
|
@@ -3960,7 +4403,10 @@ Rejection reason: ${reason}`;
|
|
|
3960
4403
|
}
|
|
3961
4404
|
// Private helpers
|
|
3962
4405
|
generateId() {
|
|
3963
|
-
|
|
4406
|
+
const bytes = randomBytes2(16);
|
|
4407
|
+
const hex = bytes.toString("hex");
|
|
4408
|
+
const timestamp = Date.now().toString(36);
|
|
4409
|
+
return `prop_${timestamp}_${hex.substring(0, 16)}`;
|
|
3964
4410
|
}
|
|
3965
4411
|
hashContent(content) {
|
|
3966
4412
|
return createHash3("sha256").update(content).digest("hex").substring(0, 16);
|
|
@@ -4598,9 +5044,21 @@ function createWatchdog(config) {
|
|
|
4598
5044
|
|
|
4599
5045
|
// src/sync/team-sync.ts
|
|
4600
5046
|
import { execSync } from "child_process";
|
|
4601
|
-
import { readFileSync as
|
|
5047
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
4602
5048
|
import { join as join5 } from "path";
|
|
4603
5049
|
import { parse, stringify as stringify2 } from "yaml";
|
|
5050
|
+
function validateGitBranchName(branch) {
|
|
5051
|
+
if (!/^[A-Za-z0-9/_-]+$/.test(branch)) {
|
|
5052
|
+
throw new Error(`Invalid git branch name: "${branch}". Only alphanumeric, _, -, and / are allowed.`);
|
|
5053
|
+
}
|
|
5054
|
+
return branch;
|
|
5055
|
+
}
|
|
5056
|
+
function validateGitRemoteName(remote) {
|
|
5057
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(remote)) {
|
|
5058
|
+
throw new Error(`Invalid git remote name: "${remote}". Only alphanumeric, _, -, and . are allowed.`);
|
|
5059
|
+
}
|
|
5060
|
+
return remote;
|
|
5061
|
+
}
|
|
4604
5062
|
var DEFAULT_SYNC_CONFIG = {
|
|
4605
5063
|
enabled: false,
|
|
4606
5064
|
remote: "origin",
|
|
@@ -4616,11 +5074,17 @@ var TeamSync = class {
|
|
|
4616
5074
|
this.rootDir = rootDir;
|
|
4617
5075
|
this.contextosDir = join5(rootDir, ".contextos");
|
|
4618
5076
|
this.syncConfig = this.loadSyncConfig();
|
|
5077
|
+
if (this.syncConfig.remote) {
|
|
5078
|
+
this.syncConfig.remote = validateGitRemoteName(this.syncConfig.remote);
|
|
5079
|
+
}
|
|
5080
|
+
if (this.syncConfig.branch) {
|
|
5081
|
+
this.syncConfig.branch = validateGitBranchName(this.syncConfig.branch);
|
|
5082
|
+
}
|
|
4619
5083
|
}
|
|
4620
5084
|
loadSyncConfig() {
|
|
4621
5085
|
const configPath = join5(this.contextosDir, "sync.yaml");
|
|
4622
|
-
if (
|
|
4623
|
-
const content =
|
|
5086
|
+
if (existsSync5(configPath)) {
|
|
5087
|
+
const content = readFileSync5(configPath, "utf-8");
|
|
4624
5088
|
return { ...DEFAULT_SYNC_CONFIG, ...parse(content) };
|
|
4625
5089
|
}
|
|
4626
5090
|
return DEFAULT_SYNC_CONFIG;
|
|
@@ -4634,7 +5098,8 @@ var TeamSync = class {
|
|
|
4634
5098
|
*/
|
|
4635
5099
|
async initialize(remote = "origin") {
|
|
4636
5100
|
this.syncConfig.enabled = true;
|
|
4637
|
-
this.syncConfig.remote = remote;
|
|
5101
|
+
this.syncConfig.remote = validateGitRemoteName(remote);
|
|
5102
|
+
this.syncConfig.branch = validateGitBranchName(this.syncConfig.branch);
|
|
4638
5103
|
this.saveSyncConfig();
|
|
4639
5104
|
try {
|
|
4640
5105
|
execSync(`git checkout -b ${this.syncConfig.branch}`, {
|
|
@@ -4740,7 +5205,7 @@ var TemplateManager = class {
|
|
|
4740
5205
|
templatesDir;
|
|
4741
5206
|
constructor(rootDir) {
|
|
4742
5207
|
this.templatesDir = join5(rootDir, ".contextos", "templates");
|
|
4743
|
-
if (!
|
|
5208
|
+
if (!existsSync5(this.templatesDir)) {
|
|
4744
5209
|
mkdirSync3(this.templatesDir, { recursive: true });
|
|
4745
5210
|
}
|
|
4746
5211
|
}
|
|
@@ -4753,7 +5218,7 @@ var TemplateManager = class {
|
|
|
4753
5218
|
const files = __require("fs").readdirSync(this.templatesDir);
|
|
4754
5219
|
for (const file of files) {
|
|
4755
5220
|
if (file.endsWith(".yaml")) {
|
|
4756
|
-
const content =
|
|
5221
|
+
const content = readFileSync5(join5(this.templatesDir, file), "utf-8");
|
|
4757
5222
|
templates.push(parse(content));
|
|
4758
5223
|
}
|
|
4759
5224
|
}
|
|
@@ -4772,8 +5237,8 @@ var TemplateManager = class {
|
|
|
4772
5237
|
description,
|
|
4773
5238
|
author,
|
|
4774
5239
|
version: "1.0.0",
|
|
4775
|
-
context:
|
|
4776
|
-
config:
|
|
5240
|
+
context: existsSync5(contextPath) ? parse(readFileSync5(contextPath, "utf-8")) : {},
|
|
5241
|
+
config: existsSync5(configPath) ? parse(readFileSync5(configPath, "utf-8")) : {}
|
|
4777
5242
|
};
|
|
4778
5243
|
const templatePath = join5(this.templatesDir, `${name}.yaml`);
|
|
4779
5244
|
writeFileSync2(templatePath, stringify2(template, { indent: 2 }), "utf-8");
|
|
@@ -4783,10 +5248,10 @@ var TemplateManager = class {
|
|
|
4783
5248
|
*/
|
|
4784
5249
|
apply(name) {
|
|
4785
5250
|
const templatePath = join5(this.templatesDir, `${name}.yaml`);
|
|
4786
|
-
if (!
|
|
5251
|
+
if (!existsSync5(templatePath)) {
|
|
4787
5252
|
throw new Error(`Template '${name}' not found`);
|
|
4788
5253
|
}
|
|
4789
|
-
const template = parse(
|
|
5254
|
+
const template = parse(readFileSync5(templatePath, "utf-8"));
|
|
4790
5255
|
const contextPath = join5(this.templatesDir, "..", "context.yaml");
|
|
4791
5256
|
const configPath = join5(this.templatesDir, "..", "config.yaml");
|
|
4792
5257
|
if (template.context && Object.keys(template.context).length > 0) {
|
|
@@ -4800,8 +5265,10 @@ var TemplateManager = class {
|
|
|
4800
5265
|
|
|
4801
5266
|
// src/sync/cloud-sync.ts
|
|
4802
5267
|
import crypto from "crypto";
|
|
4803
|
-
import {
|
|
5268
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5269
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
4804
5270
|
import { join as join6 } from "path";
|
|
5271
|
+
var { AbortController } = globalThis;
|
|
4805
5272
|
var E2EEncryption = class {
|
|
4806
5273
|
algorithm = "aes-256-gcm";
|
|
4807
5274
|
keyLength = 32;
|
|
@@ -4877,7 +5344,7 @@ var CloudSync = class {
|
|
|
4877
5344
|
this.config.encryptionKey = key;
|
|
4878
5345
|
this.config.enabled = true;
|
|
4879
5346
|
const saltPath = join6(this.rootDir, ".contextos", ".salt");
|
|
4880
|
-
|
|
5347
|
+
await writeFile2(saltPath, salt, "utf-8");
|
|
4881
5348
|
return key;
|
|
4882
5349
|
}
|
|
4883
5350
|
/**
|
|
@@ -4890,9 +5357,11 @@ var CloudSync = class {
|
|
|
4890
5357
|
try {
|
|
4891
5358
|
const contextPath = join6(this.rootDir, ".contextos", "context.yaml");
|
|
4892
5359
|
const configPath = join6(this.rootDir, ".contextos", "config.yaml");
|
|
5360
|
+
const contextContent = existsSync6(contextPath) ? await readFile3(contextPath, "utf-8") : "";
|
|
5361
|
+
const configContent = existsSync6(configPath) ? await readFile3(configPath, "utf-8") : "";
|
|
4893
5362
|
const data = JSON.stringify({
|
|
4894
|
-
context:
|
|
4895
|
-
config:
|
|
5363
|
+
context: contextContent,
|
|
5364
|
+
config: configContent,
|
|
4896
5365
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4897
5366
|
});
|
|
4898
5367
|
const encryptedData = this.encryption.encrypt(data, this.config.encryptionKey);
|
|
@@ -4904,20 +5373,32 @@ var CloudSync = class {
|
|
|
4904
5373
|
data: encryptedData,
|
|
4905
5374
|
checksum
|
|
4906
5375
|
};
|
|
4907
|
-
const
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
5376
|
+
const controller = new AbortController();
|
|
5377
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
5378
|
+
try {
|
|
5379
|
+
const response = await fetch(`${this.config.apiEndpoint}/sync/upload`, {
|
|
5380
|
+
method: "POST",
|
|
5381
|
+
headers: { "Content-Type": "application/json" },
|
|
5382
|
+
body: JSON.stringify(payload),
|
|
5383
|
+
signal: controller.signal
|
|
5384
|
+
});
|
|
5385
|
+
clearTimeout(timeoutId);
|
|
5386
|
+
if (!response.ok) {
|
|
5387
|
+
throw new Error(`Upload failed: ${response.status}`);
|
|
5388
|
+
}
|
|
5389
|
+
return {
|
|
5390
|
+
success: true,
|
|
5391
|
+
action: "upload",
|
|
5392
|
+
message: "Context uploaded and encrypted",
|
|
5393
|
+
timestamp: payload.timestamp
|
|
5394
|
+
};
|
|
5395
|
+
} catch (fetchError) {
|
|
5396
|
+
clearTimeout(timeoutId);
|
|
5397
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
5398
|
+
return { success: false, action: "upload", message: "Upload timed out after 30 seconds" };
|
|
5399
|
+
}
|
|
5400
|
+
throw fetchError;
|
|
4914
5401
|
}
|
|
4915
|
-
return {
|
|
4916
|
-
success: true,
|
|
4917
|
-
action: "upload",
|
|
4918
|
-
message: "Context uploaded and encrypted",
|
|
4919
|
-
timestamp: payload.timestamp
|
|
4920
|
-
};
|
|
4921
5402
|
} catch (error) {
|
|
4922
5403
|
return {
|
|
4923
5404
|
success: false,
|
|
@@ -4934,30 +5415,41 @@ var CloudSync = class {
|
|
|
4934
5415
|
return { success: false, action: "download", message: "Cloud sync not initialized" };
|
|
4935
5416
|
}
|
|
4936
5417
|
try {
|
|
4937
|
-
const
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
5418
|
+
const controller = new AbortController();
|
|
5419
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
5420
|
+
try {
|
|
5421
|
+
const response = await fetch(
|
|
5422
|
+
`${this.config.apiEndpoint}/sync/download?teamId=${this.config.teamId}&userId=${this.config.userId}`,
|
|
5423
|
+
{ method: "GET", signal: controller.signal }
|
|
5424
|
+
);
|
|
5425
|
+
clearTimeout(timeoutId);
|
|
5426
|
+
if (!response.ok) {
|
|
5427
|
+
throw new Error(`Download failed: ${response.status}`);
|
|
5428
|
+
}
|
|
5429
|
+
const payload = await response.json();
|
|
5430
|
+
const decryptedData = this.encryption.decrypt(payload.data, this.config.encryptionKey);
|
|
5431
|
+
const data = JSON.parse(decryptedData);
|
|
5432
|
+
const expectedChecksum = this.encryption.checksum(decryptedData);
|
|
5433
|
+
if (expectedChecksum !== payload.checksum) {
|
|
5434
|
+
return { success: false, action: "download", message: "Checksum mismatch - data corrupted" };
|
|
5435
|
+
}
|
|
5436
|
+
const contextPath = join6(this.rootDir, ".contextos", "context.yaml");
|
|
5437
|
+
const configPath = join6(this.rootDir, ".contextos", "config.yaml");
|
|
5438
|
+
if (data.context) await writeFile2(contextPath, data.context, "utf-8");
|
|
5439
|
+
if (data.config) await writeFile2(configPath, data.config, "utf-8");
|
|
5440
|
+
return {
|
|
5441
|
+
success: true,
|
|
5442
|
+
action: "download",
|
|
5443
|
+
message: "Context downloaded and decrypted",
|
|
5444
|
+
timestamp: payload.timestamp
|
|
5445
|
+
};
|
|
5446
|
+
} catch (fetchError) {
|
|
5447
|
+
clearTimeout(timeoutId);
|
|
5448
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
5449
|
+
return { success: false, action: "download", message: "Download timed out after 30 seconds" };
|
|
5450
|
+
}
|
|
5451
|
+
throw fetchError;
|
|
4950
5452
|
}
|
|
4951
|
-
const contextPath = join6(this.rootDir, ".contextos", "context.yaml");
|
|
4952
|
-
const configPath = join6(this.rootDir, ".contextos", "config.yaml");
|
|
4953
|
-
if (data.context) writeFileSync3(contextPath, data.context, "utf-8");
|
|
4954
|
-
if (data.config) writeFileSync3(configPath, data.config, "utf-8");
|
|
4955
|
-
return {
|
|
4956
|
-
success: true,
|
|
4957
|
-
action: "download",
|
|
4958
|
-
message: "Context downloaded and decrypted",
|
|
4959
|
-
timestamp: payload.timestamp
|
|
4960
|
-
};
|
|
4961
5453
|
} catch (error) {
|
|
4962
5454
|
return {
|
|
4963
5455
|
success: false,
|
|
@@ -4967,19 +5459,19 @@ var CloudSync = class {
|
|
|
4967
5459
|
}
|
|
4968
5460
|
}
|
|
4969
5461
|
/**
|
|
4970
|
-
* Check if encryption key is valid
|
|
5462
|
+
* Check if encryption key is valid - Fix R9: Use async readFile
|
|
4971
5463
|
*/
|
|
4972
|
-
validateKey(password) {
|
|
5464
|
+
async validateKey(password) {
|
|
4973
5465
|
const saltPath = join6(this.rootDir, ".contextos", ".salt");
|
|
4974
|
-
if (!
|
|
4975
|
-
const salt =
|
|
5466
|
+
if (!existsSync6(saltPath)) return false;
|
|
5467
|
+
const salt = await readFile3(saltPath, "utf-8");
|
|
4976
5468
|
const derivedKey = this.encryption.deriveKey(password, salt);
|
|
4977
5469
|
return derivedKey === this.config.encryptionKey;
|
|
4978
5470
|
}
|
|
4979
5471
|
};
|
|
4980
5472
|
|
|
4981
5473
|
// src/analytics/analytics.ts
|
|
4982
|
-
import { existsSync as
|
|
5474
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
4983
5475
|
import { join as join7 } from "path";
|
|
4984
5476
|
var AnalyticsCollector = class {
|
|
4985
5477
|
analyticsDir;
|
|
@@ -4989,7 +5481,7 @@ var AnalyticsCollector = class {
|
|
|
4989
5481
|
this.analyticsDir = join7(rootDir, ".contextos", "analytics");
|
|
4990
5482
|
this.eventsFile = join7(this.analyticsDir, "events.json");
|
|
4991
5483
|
this.statsFile = join7(this.analyticsDir, "stats.json");
|
|
4992
|
-
if (!
|
|
5484
|
+
if (!existsSync7(this.analyticsDir)) {
|
|
4993
5485
|
mkdirSync4(this.analyticsDir, { recursive: true });
|
|
4994
5486
|
}
|
|
4995
5487
|
}
|
|
@@ -5028,9 +5520,9 @@ var AnalyticsCollector = class {
|
|
|
5028
5520
|
});
|
|
5029
5521
|
}
|
|
5030
5522
|
loadEvents() {
|
|
5031
|
-
if (
|
|
5523
|
+
if (existsSync7(this.eventsFile)) {
|
|
5032
5524
|
try {
|
|
5033
|
-
return JSON.parse(
|
|
5525
|
+
return JSON.parse(readFileSync7(this.eventsFile, "utf-8"));
|
|
5034
5526
|
} catch {
|
|
5035
5527
|
return [];
|
|
5036
5528
|
}
|
|
@@ -5056,9 +5548,9 @@ var AnalyticsCollector = class {
|
|
|
5056
5548
|
writeFileSync4(this.statsFile, JSON.stringify(updatedStats, null, 2), "utf-8");
|
|
5057
5549
|
}
|
|
5058
5550
|
loadDailyStats() {
|
|
5059
|
-
if (
|
|
5551
|
+
if (existsSync7(this.statsFile)) {
|
|
5060
5552
|
try {
|
|
5061
|
-
return JSON.parse(
|
|
5553
|
+
return JSON.parse(readFileSync7(this.statsFile, "utf-8"));
|
|
5062
5554
|
} catch {
|
|
5063
5555
|
return [];
|
|
5064
5556
|
}
|
|
@@ -5124,7 +5616,7 @@ var AnalyticsCollector = class {
|
|
|
5124
5616
|
};
|
|
5125
5617
|
|
|
5126
5618
|
// src/compliance/rbac.ts
|
|
5127
|
-
import { readFileSync as
|
|
5619
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
5128
5620
|
import { join as join8 } from "path";
|
|
5129
5621
|
import crypto2 from "crypto";
|
|
5130
5622
|
var BUILT_IN_ROLES = [
|
|
@@ -5189,7 +5681,7 @@ var RBACManager = class {
|
|
|
5189
5681
|
this.rolesFile = join8(this.rbacDir, "roles.json");
|
|
5190
5682
|
this.usersFile = join8(this.rbacDir, "users.json");
|
|
5191
5683
|
this.policiesFile = join8(this.rbacDir, "policies.json");
|
|
5192
|
-
if (!
|
|
5684
|
+
if (!existsSync8(this.rbacDir)) {
|
|
5193
5685
|
mkdirSync5(this.rbacDir, { recursive: true });
|
|
5194
5686
|
}
|
|
5195
5687
|
this.loadData();
|
|
@@ -5198,27 +5690,27 @@ var RBACManager = class {
|
|
|
5198
5690
|
for (const role of BUILT_IN_ROLES) {
|
|
5199
5691
|
this.roles.set(role.id, role);
|
|
5200
5692
|
}
|
|
5201
|
-
if (
|
|
5693
|
+
if (existsSync8(this.rolesFile)) {
|
|
5202
5694
|
try {
|
|
5203
|
-
const customRoles = JSON.parse(
|
|
5695
|
+
const customRoles = JSON.parse(readFileSync8(this.rolesFile, "utf-8"));
|
|
5204
5696
|
for (const role of customRoles) {
|
|
5205
5697
|
this.roles.set(role.id, role);
|
|
5206
5698
|
}
|
|
5207
5699
|
} catch {
|
|
5208
5700
|
}
|
|
5209
5701
|
}
|
|
5210
|
-
if (
|
|
5702
|
+
if (existsSync8(this.usersFile)) {
|
|
5211
5703
|
try {
|
|
5212
|
-
const users = JSON.parse(
|
|
5704
|
+
const users = JSON.parse(readFileSync8(this.usersFile, "utf-8"));
|
|
5213
5705
|
for (const user of users) {
|
|
5214
5706
|
this.users.set(user.id, user);
|
|
5215
5707
|
}
|
|
5216
5708
|
} catch {
|
|
5217
5709
|
}
|
|
5218
5710
|
}
|
|
5219
|
-
if (
|
|
5711
|
+
if (existsSync8(this.policiesFile)) {
|
|
5220
5712
|
try {
|
|
5221
|
-
this.policies = JSON.parse(
|
|
5713
|
+
this.policies = JSON.parse(readFileSync8(this.policiesFile, "utf-8"));
|
|
5222
5714
|
} catch {
|
|
5223
5715
|
}
|
|
5224
5716
|
}
|
|
@@ -5379,7 +5871,7 @@ var RBACManager = class {
|
|
|
5379
5871
|
};
|
|
5380
5872
|
|
|
5381
5873
|
// src/compliance/audit.ts
|
|
5382
|
-
import { existsSync as
|
|
5874
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6, appendFileSync, mkdirSync as mkdirSync6 } from "fs";
|
|
5383
5875
|
import { join as join9 } from "path";
|
|
5384
5876
|
import crypto3 from "crypto";
|
|
5385
5877
|
var AuditLogger = class {
|
|
@@ -5389,7 +5881,7 @@ var AuditLogger = class {
|
|
|
5389
5881
|
constructor(rootDir) {
|
|
5390
5882
|
this.auditDir = join9(rootDir, ".contextos", "audit");
|
|
5391
5883
|
this.indexFile = join9(this.auditDir, "index.json");
|
|
5392
|
-
if (!
|
|
5884
|
+
if (!existsSync9(this.auditDir)) {
|
|
5393
5885
|
mkdirSync6(this.auditDir, { recursive: true });
|
|
5394
5886
|
}
|
|
5395
5887
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -5439,9 +5931,9 @@ var AuditLogger = class {
|
|
|
5439
5931
|
writeFileSync6(this.indexFile, JSON.stringify(index, null, 2), "utf-8");
|
|
5440
5932
|
}
|
|
5441
5933
|
loadIndex() {
|
|
5442
|
-
if (
|
|
5934
|
+
if (existsSync9(this.indexFile)) {
|
|
5443
5935
|
try {
|
|
5444
|
-
return JSON.parse(
|
|
5936
|
+
return JSON.parse(readFileSync9(this.indexFile, "utf-8"));
|
|
5445
5937
|
} catch {
|
|
5446
5938
|
return {};
|
|
5447
5939
|
}
|
|
@@ -5458,7 +5950,7 @@ var AuditLogger = class {
|
|
|
5458
5950
|
for (const logFile of logFiles) {
|
|
5459
5951
|
if (entries.length >= limit) break;
|
|
5460
5952
|
const filePath = join9(this.auditDir, logFile);
|
|
5461
|
-
const content =
|
|
5953
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
5462
5954
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
5463
5955
|
for (const line of lines.reverse()) {
|
|
5464
5956
|
if (entries.length >= limit) break;
|
|
@@ -5495,7 +5987,7 @@ var AuditLogger = class {
|
|
|
5495
5987
|
*/
|
|
5496
5988
|
verifyLogFile(filename) {
|
|
5497
5989
|
const filePath = join9(this.auditDir, filename);
|
|
5498
|
-
const content =
|
|
5990
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
5499
5991
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
5500
5992
|
const result = { valid: 0, invalid: 0, entries: [] };
|
|
5501
5993
|
for (const line of lines) {
|
|
@@ -5549,8 +6041,8 @@ var AuditLogger = class {
|
|
|
5549
6041
|
};
|
|
5550
6042
|
|
|
5551
6043
|
// src/plugins/manager.ts
|
|
5552
|
-
import { existsSync as
|
|
5553
|
-
import { join as join10, resolve } from "path";
|
|
6044
|
+
import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync10, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
|
|
6045
|
+
import { join as join10, resolve, normalize as normalize2 } from "path";
|
|
5554
6046
|
var PluginManager = class {
|
|
5555
6047
|
plugins = /* @__PURE__ */ new Map();
|
|
5556
6048
|
pluginsDir;
|
|
@@ -5559,7 +6051,7 @@ var PluginManager = class {
|
|
|
5559
6051
|
constructor(projectRoot) {
|
|
5560
6052
|
this.projectRoot = projectRoot;
|
|
5561
6053
|
this.pluginsDir = join10(projectRoot, ".contextos", "plugins");
|
|
5562
|
-
if (!
|
|
6054
|
+
if (!existsSync10(this.pluginsDir)) {
|
|
5563
6055
|
mkdirSync7(this.pluginsDir, { recursive: true });
|
|
5564
6056
|
}
|
|
5565
6057
|
}
|
|
@@ -5569,7 +6061,7 @@ var PluginManager = class {
|
|
|
5569
6061
|
async loadAll() {
|
|
5570
6062
|
const loaded = [];
|
|
5571
6063
|
const errors = [];
|
|
5572
|
-
if (!
|
|
6064
|
+
if (!existsSync10(this.pluginsDir)) {
|
|
5573
6065
|
return { loaded, errors };
|
|
5574
6066
|
}
|
|
5575
6067
|
const entries = readdirSync(this.pluginsDir, { withFileTypes: true });
|
|
@@ -5592,17 +6084,32 @@ var PluginManager = class {
|
|
|
5592
6084
|
* Load a single plugin from path
|
|
5593
6085
|
*/
|
|
5594
6086
|
async loadPlugin(pluginPath) {
|
|
6087
|
+
const absolutePluginPath = resolve(pluginPath);
|
|
6088
|
+
const absolutePluginsDir = resolve(this.pluginsDir);
|
|
6089
|
+
const normalizedPlugin = normalize2(absolutePluginPath);
|
|
6090
|
+
const normalizedPluginsDir = normalize2(absolutePluginsDir);
|
|
6091
|
+
if (!normalizedPlugin.startsWith(normalizedPluginsDir)) {
|
|
6092
|
+
throw new Error(`Plugin path "${pluginPath}" is outside plugins directory`);
|
|
6093
|
+
}
|
|
5595
6094
|
const manifestPath = join10(pluginPath, "package.json");
|
|
5596
|
-
if (!
|
|
6095
|
+
if (!existsSync10(manifestPath)) {
|
|
5597
6096
|
throw new Error(`Plugin manifest not found: ${manifestPath}`);
|
|
5598
6097
|
}
|
|
5599
|
-
const manifestContent =
|
|
5600
|
-
|
|
6098
|
+
const manifestContent = readFileSync10(manifestPath, "utf-8");
|
|
6099
|
+
let manifest;
|
|
6100
|
+
try {
|
|
6101
|
+
manifest = JSON.parse(manifestContent);
|
|
6102
|
+
} catch (error) {
|
|
6103
|
+
throw new Error(
|
|
6104
|
+
`Failed to parse plugin manifest at ${manifestPath}: ${error instanceof Error ? error.message : String(error)}
|
|
6105
|
+
The file may contain invalid JSON.`
|
|
6106
|
+
);
|
|
6107
|
+
}
|
|
5601
6108
|
if (!manifest.name || !manifest.version || !manifest.main) {
|
|
5602
6109
|
throw new Error(`Invalid plugin manifest: missing name, version, or main`);
|
|
5603
6110
|
}
|
|
5604
6111
|
const mainPath = join10(pluginPath, manifest.main);
|
|
5605
|
-
if (!
|
|
6112
|
+
if (!existsSync10(mainPath)) {
|
|
5606
6113
|
throw new Error(`Plugin main file not found: ${mainPath}`);
|
|
5607
6114
|
}
|
|
5608
6115
|
const pluginModule = await import(`file://${resolve(mainPath)}`);
|
|
@@ -5669,21 +6176,29 @@ var PluginManager = class {
|
|
|
5669
6176
|
*/
|
|
5670
6177
|
async install(source, options = {}) {
|
|
5671
6178
|
const targetDir = join10(this.pluginsDir, source.split("/").pop() || source);
|
|
5672
|
-
if (
|
|
6179
|
+
if (existsSync10(targetDir) && !options.force) {
|
|
5673
6180
|
throw new Error(`Plugin already installed: ${source}`);
|
|
5674
6181
|
}
|
|
5675
6182
|
if (options.local) {
|
|
5676
6183
|
const sourcePath = resolve(source);
|
|
5677
|
-
if (!
|
|
6184
|
+
if (!existsSync10(sourcePath)) {
|
|
5678
6185
|
throw new Error(`Source path not found: ${sourcePath}`);
|
|
5679
6186
|
}
|
|
5680
|
-
if (options.force &&
|
|
6187
|
+
if (options.force && existsSync10(targetDir)) {
|
|
5681
6188
|
rmSync(targetDir, { recursive: true });
|
|
5682
6189
|
}
|
|
5683
6190
|
mkdirSync7(targetDir, { recursive: true });
|
|
5684
|
-
|
|
6191
|
+
let manifest;
|
|
6192
|
+
try {
|
|
6193
|
+
const manifestContent = readFileSync10(join10(sourcePath, "package.json"), "utf-8");
|
|
6194
|
+
manifest = JSON.parse(manifestContent);
|
|
6195
|
+
} catch (error) {
|
|
6196
|
+
throw new Error(
|
|
6197
|
+
`Failed to parse plugin manifest at ${sourcePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
6198
|
+
);
|
|
6199
|
+
}
|
|
5685
6200
|
writeFileSync7(join10(targetDir, "package.json"), JSON.stringify(manifest, null, 2));
|
|
5686
|
-
const mainContent =
|
|
6201
|
+
const mainContent = readFileSync10(join10(sourcePath, manifest.main), "utf-8");
|
|
5687
6202
|
writeFileSync7(join10(targetDir, manifest.main), mainContent);
|
|
5688
6203
|
} else {
|
|
5689
6204
|
throw new Error("Remote plugin installation not yet implemented");
|
|
@@ -5701,7 +6216,7 @@ var PluginManager = class {
|
|
|
5701
6216
|
const state = this.plugins.get(name);
|
|
5702
6217
|
if (!state) return false;
|
|
5703
6218
|
await this.unloadPlugin(name);
|
|
5704
|
-
if (
|
|
6219
|
+
if (existsSync10(state.path)) {
|
|
5705
6220
|
rmSync(state.path, { recursive: true });
|
|
5706
6221
|
}
|
|
5707
6222
|
return true;
|
|
@@ -5755,7 +6270,7 @@ var PluginManager = class {
|
|
|
5755
6270
|
*/
|
|
5756
6271
|
createPluginScaffold(template) {
|
|
5757
6272
|
const pluginDir = join10(this.pluginsDir, template.name);
|
|
5758
|
-
if (
|
|
6273
|
+
if (existsSync10(pluginDir)) {
|
|
5759
6274
|
throw new Error(`Plugin directory already exists: ${template.name}`);
|
|
5760
6275
|
}
|
|
5761
6276
|
mkdirSync7(pluginDir, { recursive: true });
|
|
@@ -5821,6 +6336,9 @@ ${commandsCode}
|
|
|
5821
6336
|
this.storage.set(pluginName, /* @__PURE__ */ new Map());
|
|
5822
6337
|
}
|
|
5823
6338
|
const pluginStorage = this.storage.get(pluginName);
|
|
6339
|
+
if (!pluginStorage) {
|
|
6340
|
+
throw new Error(`Failed to get plugin storage for ${pluginName}`);
|
|
6341
|
+
}
|
|
5824
6342
|
return {
|
|
5825
6343
|
projectRoot: this.projectRoot,
|
|
5826
6344
|
configDir: join10(this.projectRoot, ".contextos"),
|
|
@@ -5834,8 +6352,13 @@ ${commandsCode}
|
|
|
5834
6352
|
return { files: [], context: "" };
|
|
5835
6353
|
},
|
|
5836
6354
|
readFile: async (path) => {
|
|
5837
|
-
const fullPath =
|
|
5838
|
-
|
|
6355
|
+
const fullPath = resolve(this.projectRoot, path);
|
|
6356
|
+
const normalized = normalize2(fullPath);
|
|
6357
|
+
const rootNormalized = normalize2(this.projectRoot);
|
|
6358
|
+
if (!normalized.startsWith(rootNormalized)) {
|
|
6359
|
+
throw new Error(`Path traversal detected: "${path}" escapes project boundaries`);
|
|
6360
|
+
}
|
|
6361
|
+
return readFileSync10(normalized, "utf-8");
|
|
5839
6362
|
},
|
|
5840
6363
|
getDependencies: async (_path, _depth = 2) => {
|
|
5841
6364
|
return [];
|
|
@@ -5855,7 +6378,7 @@ function createPluginManager(projectRoot) {
|
|
|
5855
6378
|
}
|
|
5856
6379
|
|
|
5857
6380
|
// src/plugins/registry.ts
|
|
5858
|
-
import { existsSync as
|
|
6381
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
|
|
5859
6382
|
import { join as join11 } from "path";
|
|
5860
6383
|
var PluginRegistry = class {
|
|
5861
6384
|
config;
|
|
@@ -5868,7 +6391,7 @@ var PluginRegistry = class {
|
|
|
5868
6391
|
*/
|
|
5869
6392
|
listLocal() {
|
|
5870
6393
|
const plugins = [];
|
|
5871
|
-
if (!
|
|
6394
|
+
if (!existsSync11(this.config.localDir)) {
|
|
5872
6395
|
return plugins;
|
|
5873
6396
|
}
|
|
5874
6397
|
const entries = readdirSync2(this.config.localDir, { withFileTypes: true });
|
|
@@ -5876,13 +6399,13 @@ var PluginRegistry = class {
|
|
|
5876
6399
|
if (!entry.isDirectory()) continue;
|
|
5877
6400
|
const pluginPath = join11(this.config.localDir, entry.name);
|
|
5878
6401
|
const manifestPath = join11(pluginPath, "package.json");
|
|
5879
|
-
if (!
|
|
6402
|
+
if (!existsSync11(manifestPath)) continue;
|
|
5880
6403
|
try {
|
|
5881
6404
|
const manifest = JSON.parse(
|
|
5882
|
-
|
|
6405
|
+
readFileSync11(manifestPath, "utf-8")
|
|
5883
6406
|
);
|
|
5884
6407
|
const disabledPath = join11(pluginPath, ".disabled");
|
|
5885
|
-
const enabled = !
|
|
6408
|
+
const enabled = !existsSync11(disabledPath);
|
|
5886
6409
|
plugins.push({
|
|
5887
6410
|
name: manifest.name,
|
|
5888
6411
|
version: manifest.version,
|
|
@@ -6020,7 +6543,7 @@ function createPluginRegistry(projectRoot) {
|
|
|
6020
6543
|
}
|
|
6021
6544
|
|
|
6022
6545
|
// src/finetuning/collector.ts
|
|
6023
|
-
import { existsSync as
|
|
6546
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "fs";
|
|
6024
6547
|
import { join as join12 } from "path";
|
|
6025
6548
|
import { createHash as createHash4 } from "crypto";
|
|
6026
6549
|
var TrainingDataCollector = class {
|
|
@@ -6029,7 +6552,7 @@ var TrainingDataCollector = class {
|
|
|
6029
6552
|
loaded = false;
|
|
6030
6553
|
constructor(projectRoot) {
|
|
6031
6554
|
this.dataDir = join12(projectRoot, ".contextos", "training");
|
|
6032
|
-
if (!
|
|
6555
|
+
if (!existsSync12(this.dataDir)) {
|
|
6033
6556
|
mkdirSync8(this.dataDir, { recursive: true });
|
|
6034
6557
|
}
|
|
6035
6558
|
}
|
|
@@ -6039,9 +6562,9 @@ var TrainingDataCollector = class {
|
|
|
6039
6562
|
load() {
|
|
6040
6563
|
if (this.loaded) return;
|
|
6041
6564
|
const dataFile = join12(this.dataDir, "examples.json");
|
|
6042
|
-
if (
|
|
6565
|
+
if (existsSync12(dataFile)) {
|
|
6043
6566
|
try {
|
|
6044
|
-
const data = JSON.parse(
|
|
6567
|
+
const data = JSON.parse(readFileSync12(dataFile, "utf-8"));
|
|
6045
6568
|
this.examples = data.examples || [];
|
|
6046
6569
|
} catch {
|
|
6047
6570
|
this.examples = [];
|
|
@@ -6197,7 +6720,7 @@ function createTrainingDataCollector(projectRoot) {
|
|
|
6197
6720
|
}
|
|
6198
6721
|
|
|
6199
6722
|
// src/finetuning/formatter.ts
|
|
6200
|
-
import { createReadStream, createWriteStream, existsSync as
|
|
6723
|
+
import { createReadStream, createWriteStream, existsSync as existsSync13 } from "fs";
|
|
6201
6724
|
import { createInterface } from "readline";
|
|
6202
6725
|
var DatasetFormatter = class {
|
|
6203
6726
|
/**
|
|
@@ -6255,7 +6778,7 @@ var DatasetFormatter = class {
|
|
|
6255
6778
|
dateRange: { earliest: /* @__PURE__ */ new Date(), latest: /* @__PURE__ */ new Date(0) }
|
|
6256
6779
|
}
|
|
6257
6780
|
};
|
|
6258
|
-
if (!
|
|
6781
|
+
if (!existsSync13(filePath)) {
|
|
6259
6782
|
result.valid = false;
|
|
6260
6783
|
result.errors.push({
|
|
6261
6784
|
line: 0,
|
|
@@ -7162,7 +7685,9 @@ export {
|
|
|
7162
7685
|
mergeSmallChunks,
|
|
7163
7686
|
parseWithRegex,
|
|
7164
7687
|
prepareSandboxVariables,
|
|
7688
|
+
resetContextBuilder,
|
|
7165
7689
|
resetGlobalBlackboard,
|
|
7690
|
+
resetParser,
|
|
7166
7691
|
saveConfigYaml,
|
|
7167
7692
|
saveContextYaml,
|
|
7168
7693
|
setGlobalLogger,
|