@contextos/core 0.2.1 → 0.2.2
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 +732 -210
- 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,17 +243,62 @@ 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
|
return findContextosRoot(startDir) !== null;
|
|
@@ -432,12 +556,29 @@ var ASTParser = class {
|
|
|
432
556
|
}
|
|
433
557
|
};
|
|
434
558
|
var parserInstance = null;
|
|
559
|
+
var initializationPromise = null;
|
|
435
560
|
async function getParser() {
|
|
436
|
-
if (
|
|
437
|
-
parserInstance
|
|
438
|
-
await parserInstance.initialize();
|
|
561
|
+
if (parserInstance) {
|
|
562
|
+
return parserInstance;
|
|
439
563
|
}
|
|
440
|
-
|
|
564
|
+
if (!initializationPromise) {
|
|
565
|
+
initializationPromise = (async () => {
|
|
566
|
+
try {
|
|
567
|
+
const instance = new ASTParser();
|
|
568
|
+
await instance.initialize();
|
|
569
|
+
parserInstance = instance;
|
|
570
|
+
return instance;
|
|
571
|
+
} catch (error) {
|
|
572
|
+
initializationPromise = null;
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
})();
|
|
576
|
+
}
|
|
577
|
+
return initializationPromise;
|
|
578
|
+
}
|
|
579
|
+
function resetParser() {
|
|
580
|
+
parserInstance = null;
|
|
581
|
+
initializationPromise = null;
|
|
441
582
|
}
|
|
442
583
|
|
|
443
584
|
// src/parser/detector.ts
|
|
@@ -1041,7 +1182,6 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
|
1041
1182
|
import { dirname as dirname3 } from "path";
|
|
1042
1183
|
var VectorStore = class {
|
|
1043
1184
|
db = null;
|
|
1044
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1045
1185
|
embedder = null;
|
|
1046
1186
|
dbPath;
|
|
1047
1187
|
constructor(dbPath) {
|
|
@@ -1078,13 +1218,16 @@ var VectorStore = class {
|
|
|
1078
1218
|
}
|
|
1079
1219
|
/**
|
|
1080
1220
|
* Initialize the embedding model
|
|
1221
|
+
* Fixed: Better error handling for dynamic import failures
|
|
1081
1222
|
*/
|
|
1082
1223
|
async initializeEmbedder() {
|
|
1083
1224
|
try {
|
|
1084
1225
|
const { pipeline } = await import("@xenova/transformers");
|
|
1085
1226
|
this.embedder = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
1086
1227
|
} catch (error) {
|
|
1087
|
-
|
|
1228
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1229
|
+
console.warn(`Failed to initialize embedding model: ${errorMessage}`);
|
|
1230
|
+
console.warn("Vector search will use text-based fallback (less accurate).");
|
|
1088
1231
|
this.embedder = null;
|
|
1089
1232
|
}
|
|
1090
1233
|
}
|
|
@@ -1156,54 +1299,76 @@ var VectorStore = class {
|
|
|
1156
1299
|
}
|
|
1157
1300
|
/**
|
|
1158
1301
|
* Vector-based similarity search
|
|
1302
|
+
* Fixed: Uses pagination to prevent unbounded memory growth
|
|
1159
1303
|
*/
|
|
1160
1304
|
vectorSearch(queryEmbedding, limit) {
|
|
1161
1305
|
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
|
-
|
|
1306
|
+
const pageSize = 1e3;
|
|
1307
|
+
let offset = 0;
|
|
1308
|
+
const allResults = [];
|
|
1309
|
+
while (allResults.length < limit * 2) {
|
|
1310
|
+
const chunks = this.db.prepare(`
|
|
1311
|
+
SELECT id, file_path, content, start_line, end_line, embedding
|
|
1312
|
+
FROM chunks
|
|
1313
|
+
WHERE embedding IS NOT NULL
|
|
1314
|
+
LIMIT ? OFFSET ?
|
|
1315
|
+
`).all(pageSize, offset);
|
|
1316
|
+
if (chunks.length === 0) break;
|
|
1317
|
+
const batchResults = chunks.map((chunk) => {
|
|
1318
|
+
const chunkEmbedding = new Float32Array(chunk.embedding.buffer);
|
|
1319
|
+
const score = cosineSimilarity(queryEmbedding, chunkEmbedding);
|
|
1320
|
+
return {
|
|
1321
|
+
chunkId: chunk.id,
|
|
1322
|
+
filePath: chunk.file_path,
|
|
1323
|
+
content: chunk.content,
|
|
1324
|
+
score,
|
|
1325
|
+
lines: [chunk.start_line, chunk.end_line]
|
|
1326
|
+
};
|
|
1327
|
+
});
|
|
1328
|
+
allResults.push(...batchResults);
|
|
1329
|
+
offset += pageSize;
|
|
1330
|
+
if (chunks.length < pageSize) break;
|
|
1331
|
+
}
|
|
1332
|
+
return allResults.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1179
1333
|
}
|
|
1180
1334
|
/**
|
|
1181
1335
|
* Text-based fallback search
|
|
1336
|
+
* Fixed: Uses pagination to prevent unbounded memory growth
|
|
1182
1337
|
*/
|
|
1183
1338
|
textSearch(query, limit) {
|
|
1184
1339
|
if (!this.db) throw new Error("VectorStore not initialized");
|
|
1185
1340
|
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
|
-
|
|
1341
|
+
const pageSize = 1e3;
|
|
1342
|
+
let offset = 0;
|
|
1343
|
+
const allResults = [];
|
|
1344
|
+
while (allResults.length < limit * 2) {
|
|
1345
|
+
const chunks = this.db.prepare(`
|
|
1346
|
+
SELECT id, file_path, content, start_line, end_line
|
|
1347
|
+
FROM chunks
|
|
1348
|
+
LIMIT ? OFFSET ?
|
|
1349
|
+
`).all(pageSize, offset);
|
|
1350
|
+
if (chunks.length === 0) break;
|
|
1351
|
+
const batchResults = chunks.map((chunk) => {
|
|
1352
|
+
const contentLower = chunk.content.toLowerCase();
|
|
1353
|
+
let score = 0;
|
|
1354
|
+
for (const term of terms) {
|
|
1355
|
+
const matches = (contentLower.match(new RegExp(term, "g")) || []).length;
|
|
1356
|
+
score += matches;
|
|
1357
|
+
}
|
|
1358
|
+
score = Math.min(1, score / (terms.length * 2));
|
|
1359
|
+
return {
|
|
1360
|
+
chunkId: chunk.id,
|
|
1361
|
+
filePath: chunk.file_path,
|
|
1362
|
+
content: chunk.content,
|
|
1363
|
+
score,
|
|
1364
|
+
lines: [chunk.start_line, chunk.end_line]
|
|
1365
|
+
};
|
|
1366
|
+
});
|
|
1367
|
+
allResults.push(...batchResults);
|
|
1368
|
+
offset += pageSize;
|
|
1369
|
+
if (chunks.length < pageSize) break;
|
|
1370
|
+
}
|
|
1371
|
+
return allResults.filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1207
1372
|
}
|
|
1208
1373
|
/**
|
|
1209
1374
|
* Remove chunks for a file
|
|
@@ -1251,6 +1416,39 @@ var VectorStore = class {
|
|
|
1251
1416
|
embeddedCount: stats.embedded_count
|
|
1252
1417
|
};
|
|
1253
1418
|
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Get paginated chunks
|
|
1421
|
+
* Fixed: Added pagination to prevent loading all chunks at once
|
|
1422
|
+
*/
|
|
1423
|
+
getChunksPaginated(page = 0, pageSize = 100) {
|
|
1424
|
+
if (!this.db) throw new Error("VectorStore not initialized");
|
|
1425
|
+
const totalResult = this.db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
1426
|
+
const total = totalResult.count;
|
|
1427
|
+
const rows = this.db.prepare(`
|
|
1428
|
+
SELECT id, file_path, content, start_line, end_line, hash, type
|
|
1429
|
+
FROM chunks
|
|
1430
|
+
ORDER BY file_path, start_line
|
|
1431
|
+
LIMIT ? OFFSET ?
|
|
1432
|
+
`).all(pageSize, page * pageSize);
|
|
1433
|
+
const chunks = rows.map((row) => ({
|
|
1434
|
+
id: row.id,
|
|
1435
|
+
filePath: row.file_path,
|
|
1436
|
+
content: row.content,
|
|
1437
|
+
startLine: row.start_line,
|
|
1438
|
+
endLine: row.end_line,
|
|
1439
|
+
hash: row.hash,
|
|
1440
|
+
type: row.type
|
|
1441
|
+
}));
|
|
1442
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
1443
|
+
return {
|
|
1444
|
+
chunks,
|
|
1445
|
+
total,
|
|
1446
|
+
page,
|
|
1447
|
+
pageSize,
|
|
1448
|
+
totalPages,
|
|
1449
|
+
hasMore: page * pageSize + chunks.length < total
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1254
1452
|
/**
|
|
1255
1453
|
* Clear all data
|
|
1256
1454
|
*/
|
|
@@ -1714,11 +1912,110 @@ ${content}`;
|
|
|
1714
1912
|
};
|
|
1715
1913
|
|
|
1716
1914
|
// src/context/builder.ts
|
|
1717
|
-
import {
|
|
1718
|
-
import {
|
|
1915
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1916
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
1917
|
+
import { join as join4, relative, normalize } from "path";
|
|
1719
1918
|
import { glob } from "glob";
|
|
1720
1919
|
import { stringify } from "yaml";
|
|
1721
1920
|
|
|
1921
|
+
// src/llm/rate-limiter.ts
|
|
1922
|
+
var RateLimiter = class {
|
|
1923
|
+
requests = [];
|
|
1924
|
+
// Timestamps of requests
|
|
1925
|
+
maxRequests;
|
|
1926
|
+
windowMs;
|
|
1927
|
+
constructor(options) {
|
|
1928
|
+
this.maxRequests = options.requestsPerMinute;
|
|
1929
|
+
this.windowMs = options.windowMs || 6e4;
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Check if a request is allowed and record it
|
|
1933
|
+
* @returns Rate limit info
|
|
1934
|
+
*/
|
|
1935
|
+
async checkLimit() {
|
|
1936
|
+
const now = Date.now();
|
|
1937
|
+
this.requests = this.requests.filter((timestamp) => now - timestamp < this.windowMs);
|
|
1938
|
+
if (this.requests.length >= this.maxRequests) {
|
|
1939
|
+
const oldestRequest = this.requests[0];
|
|
1940
|
+
const waitTime = this.windowMs - (now - oldestRequest);
|
|
1941
|
+
return {
|
|
1942
|
+
allowed: false,
|
|
1943
|
+
waitTime,
|
|
1944
|
+
remaining: 0,
|
|
1945
|
+
resetAt: oldestRequest + this.windowMs
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
this.requests.push(now);
|
|
1949
|
+
return {
|
|
1950
|
+
allowed: true,
|
|
1951
|
+
waitTime: 0,
|
|
1952
|
+
remaining: this.maxRequests - this.requests.length,
|
|
1953
|
+
resetAt: this.requests[0] ? this.requests[0] + this.windowMs : now + this.windowMs
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Wait until a request is allowed (blocking)
|
|
1958
|
+
* Use this for automatic retry with backoff
|
|
1959
|
+
*/
|
|
1960
|
+
async waitForSlot() {
|
|
1961
|
+
const result = await this.checkLimit();
|
|
1962
|
+
if (!result.allowed) {
|
|
1963
|
+
await new Promise((resolve2) => setTimeout(resolve2, result.waitTime + 100));
|
|
1964
|
+
return this.waitForSlot();
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Reset the rate limiter (clear all history)
|
|
1969
|
+
*/
|
|
1970
|
+
reset() {
|
|
1971
|
+
this.requests = [];
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Get current statistics
|
|
1975
|
+
*/
|
|
1976
|
+
getStats() {
|
|
1977
|
+
const now = Date.now();
|
|
1978
|
+
const currentRequests = this.requests.filter((t) => now - t < this.windowMs).length;
|
|
1979
|
+
return {
|
|
1980
|
+
currentRequests,
|
|
1981
|
+
maxRequests: this.maxRequests,
|
|
1982
|
+
windowMs: this.windowMs
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
var DEFAULT_RATE_LIMITS = {
|
|
1987
|
+
// OpenAI (GPT-4, GPT-3.5)
|
|
1988
|
+
"openai": 50,
|
|
1989
|
+
// Free tier: 3 RPM, Paid: 50-5000 RPM depending on model
|
|
1990
|
+
"gpt-4": 10,
|
|
1991
|
+
"gpt-4-turbo": 50,
|
|
1992
|
+
"gpt-3.5-turbo": 200,
|
|
1993
|
+
// Anthropic (Claude)
|
|
1994
|
+
"anthropic": 50,
|
|
1995
|
+
// Claude 3 Sonnet: 50 RPM default
|
|
1996
|
+
"claude-3-opus": 5,
|
|
1997
|
+
"claude-3-sonnet": 50,
|
|
1998
|
+
"claude-3-haiku": 200,
|
|
1999
|
+
// Google (Gemini)
|
|
2000
|
+
"gemini": 60,
|
|
2001
|
+
// Gemini Pro: 60 RPM
|
|
2002
|
+
"gemini-pro": 60,
|
|
2003
|
+
"gemini-flash": 150,
|
|
2004
|
+
// Local models (no rate limit)
|
|
2005
|
+
"local": 999999,
|
|
2006
|
+
"ollama": 999999,
|
|
2007
|
+
"lm-studio": 999999
|
|
2008
|
+
};
|
|
2009
|
+
function getDefaultRateLimit(provider, model) {
|
|
2010
|
+
if (model && model in DEFAULT_RATE_LIMITS) {
|
|
2011
|
+
return DEFAULT_RATE_LIMITS[model];
|
|
2012
|
+
}
|
|
2013
|
+
if (provider in DEFAULT_RATE_LIMITS) {
|
|
2014
|
+
return DEFAULT_RATE_LIMITS[provider];
|
|
2015
|
+
}
|
|
2016
|
+
return 60;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1722
2019
|
// src/llm/gemini-client.ts
|
|
1723
2020
|
var DEFAULT_MODEL = "gemini-3-pro-preview";
|
|
1724
2021
|
var GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
@@ -1728,12 +2025,15 @@ var GeminiClient = class {
|
|
|
1728
2025
|
maxOutputTokens;
|
|
1729
2026
|
temperature;
|
|
1730
2027
|
thinkingLevel;
|
|
2028
|
+
rateLimiter;
|
|
1731
2029
|
constructor(config) {
|
|
1732
2030
|
this.apiKey = config.apiKey;
|
|
1733
2031
|
this.model = config.model || DEFAULT_MODEL;
|
|
1734
2032
|
this.maxOutputTokens = config.maxOutputTokens || 2048;
|
|
1735
2033
|
this.temperature = 1;
|
|
1736
2034
|
this.thinkingLevel = "high";
|
|
2035
|
+
const rateLimit = getDefaultRateLimit("gemini", this.model);
|
|
2036
|
+
this.rateLimiter = new RateLimiter({ requestsPerMinute: rateLimit });
|
|
1737
2037
|
}
|
|
1738
2038
|
/**
|
|
1739
2039
|
* Check if API key is configured
|
|
@@ -1743,11 +2043,13 @@ var GeminiClient = class {
|
|
|
1743
2043
|
}
|
|
1744
2044
|
/**
|
|
1745
2045
|
* Make a request to Gemini API
|
|
2046
|
+
* Fixed: Added rate limiting and retry logic for 429 responses
|
|
1746
2047
|
*/
|
|
1747
|
-
async request(prompt, systemPrompt) {
|
|
2048
|
+
async request(prompt, systemPrompt, retryCount = 0) {
|
|
1748
2049
|
if (!this.apiKey) {
|
|
1749
2050
|
throw new Error("Gemini API key not configured. Set GEMINI_API_KEY environment variable.");
|
|
1750
2051
|
}
|
|
2052
|
+
await this.rateLimiter.waitForSlot();
|
|
1751
2053
|
const url = `${GEMINI_API_URL}/${this.model}:generateContent?key=${this.apiKey}`;
|
|
1752
2054
|
const contents = [];
|
|
1753
2055
|
if (systemPrompt) {
|
|
@@ -1781,6 +2083,16 @@ var GeminiClient = class {
|
|
|
1781
2083
|
}
|
|
1782
2084
|
})
|
|
1783
2085
|
});
|
|
2086
|
+
if (response.status === 429) {
|
|
2087
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
2088
|
+
const waitTime = retryAfter ? parseInt(retryAfter) * 1e3 : 6e4;
|
|
2089
|
+
if (retryCount < 3) {
|
|
2090
|
+
console.warn(`Rate limited. Waiting ${waitTime}ms before retry (${retryCount + 1}/3)...`);
|
|
2091
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitTime));
|
|
2092
|
+
return this.request(prompt, systemPrompt, retryCount + 1);
|
|
2093
|
+
}
|
|
2094
|
+
throw new Error(`Rate limit exceeded after ${retryCount} retries. Please try again later.`);
|
|
2095
|
+
}
|
|
1784
2096
|
if (!response.ok) {
|
|
1785
2097
|
const error = await response.text();
|
|
1786
2098
|
throw new Error(`Gemini API error: ${response.status} - ${error}`);
|
|
@@ -1991,6 +2303,43 @@ var ContextBuilder = class {
|
|
|
1991
2303
|
this.graph = new DependencyGraph();
|
|
1992
2304
|
this.budget = new TokenBudget();
|
|
1993
2305
|
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Sanitize git directory path to prevent command injection
|
|
2308
|
+
*/
|
|
2309
|
+
sanitizeGitPath(cwd) {
|
|
2310
|
+
if (!cwd) return process.cwd();
|
|
2311
|
+
const normalized = normalize(cwd);
|
|
2312
|
+
if (normalized.includes("\0") || normalized.includes("\n") || normalized.includes("\r")) {
|
|
2313
|
+
throw new Error("Invalid git directory path: contains forbidden characters");
|
|
2314
|
+
}
|
|
2315
|
+
return normalized;
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Execute git diff with proper sanitization and error handling
|
|
2319
|
+
*/
|
|
2320
|
+
async getGitDiff(type) {
|
|
2321
|
+
try {
|
|
2322
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
2323
|
+
const cwd = this.sanitizeGitPath(this.config?.rootDir);
|
|
2324
|
+
const flag = type === "cached" ? "--cached" : "";
|
|
2325
|
+
return execSync2(
|
|
2326
|
+
`git diff ${flag} --name-only`,
|
|
2327
|
+
{
|
|
2328
|
+
cwd,
|
|
2329
|
+
encoding: "utf-8",
|
|
2330
|
+
maxBuffer: 1024 * 1024,
|
|
2331
|
+
// 1MB
|
|
2332
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2333
|
+
timeout: 5e3
|
|
2334
|
+
// 5 second timeout
|
|
2335
|
+
}
|
|
2336
|
+
).trim();
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2339
|
+
console.warn(`Git command failed: ${errorMessage}`);
|
|
2340
|
+
return "";
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
1994
2343
|
/**
|
|
1995
2344
|
* Initialize the context builder for a project
|
|
1996
2345
|
*/
|
|
@@ -2002,8 +2351,13 @@ var ContextBuilder = class {
|
|
|
2002
2351
|
await this.vectorStore.initialize();
|
|
2003
2352
|
const graphPath = join4(this.config.rootDir, CONTEXTOS_DIR2, GRAPH_FILE);
|
|
2004
2353
|
if (existsSync4(graphPath)) {
|
|
2005
|
-
|
|
2006
|
-
|
|
2354
|
+
try {
|
|
2355
|
+
const graphContent = await readFile(graphPath, "utf-8");
|
|
2356
|
+
const graphData = JSON.parse(graphContent);
|
|
2357
|
+
this.graph.fromJSON(graphData);
|
|
2358
|
+
} catch (error) {
|
|
2359
|
+
console.warn(`Failed to load dependency graph: ${error instanceof Error ? error.message : String(error)}`);
|
|
2360
|
+
}
|
|
2007
2361
|
}
|
|
2008
2362
|
this.ranker = new HybridRanker(
|
|
2009
2363
|
this.vectorStore,
|
|
@@ -2040,7 +2394,7 @@ var ContextBuilder = class {
|
|
|
2040
2394
|
const parser = await getParser();
|
|
2041
2395
|
for (const filePath of files) {
|
|
2042
2396
|
try {
|
|
2043
|
-
const content =
|
|
2397
|
+
const content = await readFile(filePath, "utf-8");
|
|
2044
2398
|
const relativePath = relative(rootDir, filePath);
|
|
2045
2399
|
if (!force && !this.graph.hasChanged(relativePath, content)) {
|
|
2046
2400
|
continue;
|
|
@@ -2063,11 +2417,9 @@ var ContextBuilder = class {
|
|
|
2063
2417
|
const graphPath = join4(rootDir, CONTEXTOS_DIR2, GRAPH_FILE);
|
|
2064
2418
|
const graphDir = join4(rootDir, CONTEXTOS_DIR2, "db");
|
|
2065
2419
|
if (!existsSync4(graphDir)) {
|
|
2066
|
-
|
|
2067
|
-
mkdirSync9(graphDir, { recursive: true });
|
|
2420
|
+
await mkdir(graphDir, { recursive: true });
|
|
2068
2421
|
}
|
|
2069
|
-
|
|
2070
|
-
writeFileSync9(graphPath, JSON.stringify(this.graph.toJSON(), null, 2));
|
|
2422
|
+
await writeFile(graphPath, JSON.stringify(this.graph.toJSON(), null, 2), "utf-8");
|
|
2071
2423
|
return {
|
|
2072
2424
|
filesIndexed,
|
|
2073
2425
|
chunksCreated,
|
|
@@ -2103,37 +2455,38 @@ var ContextBuilder = class {
|
|
|
2103
2455
|
}
|
|
2104
2456
|
/**
|
|
2105
2457
|
* Infer goal from git diff, enhanced with Gemini when available
|
|
2458
|
+
* Fixed: Uses sanitized git commands to prevent command injection
|
|
2106
2459
|
*/
|
|
2107
2460
|
async inferGoal() {
|
|
2108
2461
|
let gitDiff = "";
|
|
2109
2462
|
let recentFiles = [];
|
|
2110
2463
|
try {
|
|
2111
2464
|
const { execSync: execSync2 } = await import("child_process");
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
});
|
|
2116
|
-
recentFiles = staged.trim().split("\n").filter(Boolean);
|
|
2465
|
+
const cwd = this.sanitizeGitPath(this.config?.rootDir);
|
|
2466
|
+
const staged = await this.getGitDiff("cached");
|
|
2467
|
+
recentFiles = staged.split("\n").filter(Boolean);
|
|
2117
2468
|
if (recentFiles.length === 0) {
|
|
2118
|
-
const uncommitted =
|
|
2119
|
-
|
|
2120
|
-
encoding: "utf-8"
|
|
2121
|
-
});
|
|
2122
|
-
recentFiles = uncommitted.trim().split("\n").filter(Boolean);
|
|
2469
|
+
const uncommitted = await this.getGitDiff("working");
|
|
2470
|
+
recentFiles = uncommitted.split("\n").filter(Boolean);
|
|
2123
2471
|
}
|
|
2124
2472
|
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,
|
|
2473
|
+
try {
|
|
2474
|
+
gitDiff = execSync2("git diff --cached", {
|
|
2475
|
+
cwd,
|
|
2134
2476
|
encoding: "utf-8",
|
|
2135
|
-
maxBuffer: 1024 * 1024
|
|
2477
|
+
maxBuffer: 1024 * 1024,
|
|
2478
|
+
// 1MB max
|
|
2479
|
+
timeout: 5e3
|
|
2136
2480
|
});
|
|
2481
|
+
if (!gitDiff) {
|
|
2482
|
+
gitDiff = execSync2("git diff", {
|
|
2483
|
+
cwd,
|
|
2484
|
+
encoding: "utf-8",
|
|
2485
|
+
maxBuffer: 1024 * 1024,
|
|
2486
|
+
timeout: 5e3
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
} catch {
|
|
2137
2490
|
}
|
|
2138
2491
|
}
|
|
2139
2492
|
} catch {
|
|
@@ -2220,16 +2573,34 @@ var ContextBuilder = class {
|
|
|
2220
2573
|
}
|
|
2221
2574
|
};
|
|
2222
2575
|
var builderInstance = null;
|
|
2576
|
+
var initializationPromise2 = null;
|
|
2223
2577
|
async function getContextBuilder(projectDir) {
|
|
2224
|
-
if (
|
|
2225
|
-
builderInstance
|
|
2226
|
-
await builderInstance.initialize(projectDir);
|
|
2578
|
+
if (builderInstance) {
|
|
2579
|
+
return builderInstance;
|
|
2227
2580
|
}
|
|
2228
|
-
|
|
2581
|
+
if (!initializationPromise2) {
|
|
2582
|
+
initializationPromise2 = (async () => {
|
|
2583
|
+
try {
|
|
2584
|
+
const instance = new ContextBuilder();
|
|
2585
|
+
await instance.initialize(projectDir);
|
|
2586
|
+
builderInstance = instance;
|
|
2587
|
+
return instance;
|
|
2588
|
+
} catch (error) {
|
|
2589
|
+
initializationPromise2 = null;
|
|
2590
|
+
throw error;
|
|
2591
|
+
}
|
|
2592
|
+
})();
|
|
2593
|
+
}
|
|
2594
|
+
return initializationPromise2;
|
|
2595
|
+
}
|
|
2596
|
+
function resetContextBuilder() {
|
|
2597
|
+
builderInstance?.close();
|
|
2598
|
+
builderInstance = null;
|
|
2599
|
+
initializationPromise2 = null;
|
|
2229
2600
|
}
|
|
2230
2601
|
|
|
2231
2602
|
// src/doctor/drift-detector.ts
|
|
2232
|
-
import {
|
|
2603
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2233
2604
|
import { glob as glob2 } from "glob";
|
|
2234
2605
|
var TECH_PATTERNS = {
|
|
2235
2606
|
// Databases
|
|
@@ -2408,33 +2779,40 @@ var DriftDetector = class {
|
|
|
2408
2779
|
}
|
|
2409
2780
|
/**
|
|
2410
2781
|
* Check a specific constraint
|
|
2782
|
+
* Fix N9: Convert to async file operations
|
|
2411
2783
|
*/
|
|
2412
2784
|
async checkConstraint(rule) {
|
|
2413
2785
|
const violations = [];
|
|
2414
2786
|
if (rule.toLowerCase().includes("no direct database access in controllers")) {
|
|
2415
2787
|
for (const file of this.sourceFiles) {
|
|
2416
2788
|
if (file.includes("controller")) {
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2789
|
+
try {
|
|
2790
|
+
const content = await readFile2(file, "utf-8");
|
|
2791
|
+
if (/import.*from.*(prisma|typeorm|sequelize|mongoose)/i.test(content)) {
|
|
2792
|
+
const lines = content.split("\n");
|
|
2793
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2794
|
+
if (/import.*from.*(prisma|typeorm|sequelize|mongoose)/i.test(lines[i])) {
|
|
2795
|
+
violations.push({ file, line: i + 1 });
|
|
2796
|
+
break;
|
|
2797
|
+
}
|
|
2424
2798
|
}
|
|
2425
2799
|
}
|
|
2800
|
+
} catch {
|
|
2426
2801
|
}
|
|
2427
2802
|
}
|
|
2428
2803
|
}
|
|
2429
2804
|
}
|
|
2430
2805
|
if (rule.toLowerCase().includes("no console.log")) {
|
|
2431
2806
|
for (const file of this.sourceFiles) {
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2807
|
+
try {
|
|
2808
|
+
const content = await readFile2(file, "utf-8");
|
|
2809
|
+
const lines = content.split("\n");
|
|
2810
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2811
|
+
if (/console\.log\(/.test(lines[i]) && !lines[i].includes("//")) {
|
|
2812
|
+
violations.push({ file, line: i + 1 });
|
|
2813
|
+
}
|
|
2437
2814
|
}
|
|
2815
|
+
} catch {
|
|
2438
2816
|
}
|
|
2439
2817
|
}
|
|
2440
2818
|
}
|
|
@@ -2458,12 +2836,13 @@ var DriftDetector = class {
|
|
|
2458
2836
|
}
|
|
2459
2837
|
/**
|
|
2460
2838
|
* Find patterns in files
|
|
2839
|
+
* Fix N9: Convert to async file operations
|
|
2461
2840
|
*/
|
|
2462
2841
|
async findPatternInFiles(patterns) {
|
|
2463
2842
|
const results = [];
|
|
2464
2843
|
for (const file of this.sourceFiles) {
|
|
2465
2844
|
try {
|
|
2466
|
-
const content =
|
|
2845
|
+
const content = await readFile2(file, "utf-8");
|
|
2467
2846
|
const lines = content.split("\n");
|
|
2468
2847
|
for (let i = 0; i < lines.length; i++) {
|
|
2469
2848
|
for (const pattern of patterns) {
|
|
@@ -2639,16 +3018,19 @@ var OpenAIAdapter = class {
|
|
|
2639
3018
|
apiKey;
|
|
2640
3019
|
model;
|
|
2641
3020
|
baseUrl;
|
|
3021
|
+
rateLimiter;
|
|
2642
3022
|
constructor(options = {}) {
|
|
2643
3023
|
this.apiKey = options.apiKey || process.env.OPENAI_API_KEY || "";
|
|
2644
3024
|
this.model = options.model || "gpt-5.2";
|
|
2645
3025
|
this.baseUrl = options.baseUrl || "https://api.openai.com/v1";
|
|
2646
3026
|
this.maxContextTokens = getModelContextSize("openai", this.model);
|
|
3027
|
+
const rateLimit = options.requestsPerMinute || getDefaultRateLimit("openai", this.model);
|
|
3028
|
+
this.rateLimiter = new RateLimiter({ requestsPerMinute: rateLimit });
|
|
2647
3029
|
if (!this.apiKey) {
|
|
2648
3030
|
console.warn("OpenAI API key not set. Set OPENAI_API_KEY environment variable.");
|
|
2649
3031
|
}
|
|
2650
3032
|
}
|
|
2651
|
-
async complete(request) {
|
|
3033
|
+
async complete(request, retryCount = 0) {
|
|
2652
3034
|
if (!this.apiKey) {
|
|
2653
3035
|
return {
|
|
2654
3036
|
content: "",
|
|
@@ -2657,6 +3039,7 @@ var OpenAIAdapter = class {
|
|
|
2657
3039
|
error: "OpenAI API key not configured"
|
|
2658
3040
|
};
|
|
2659
3041
|
}
|
|
3042
|
+
await this.rateLimiter.waitForSlot();
|
|
2660
3043
|
try {
|
|
2661
3044
|
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
2662
3045
|
method: "POST",
|
|
@@ -2675,11 +3058,29 @@ var OpenAIAdapter = class {
|
|
|
2675
3058
|
stop: request.stopSequences
|
|
2676
3059
|
})
|
|
2677
3060
|
});
|
|
3061
|
+
if (response.status === 429) {
|
|
3062
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
3063
|
+
const waitTime = retryAfter ? parseInt(retryAfter) * 1e3 : 6e4;
|
|
3064
|
+
if (retryCount < 3) {
|
|
3065
|
+
console.warn(`OpenAI rate limited. Waiting ${waitTime}ms before retry (${retryCount + 1}/3)...`);
|
|
3066
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitTime));
|
|
3067
|
+
return this.complete(request, retryCount + 1);
|
|
3068
|
+
}
|
|
3069
|
+
return {
|
|
3070
|
+
content: "",
|
|
3071
|
+
tokensUsed: { prompt: 0, completion: 0, total: 0 },
|
|
3072
|
+
finishReason: "error",
|
|
3073
|
+
error: `Rate limit exceeded after ${retryCount} retries`
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
2678
3076
|
if (!response.ok) {
|
|
2679
3077
|
const errorData = await response.json().catch(() => ({}));
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
3078
|
+
return {
|
|
3079
|
+
content: "",
|
|
3080
|
+
tokensUsed: { prompt: 0, completion: 0, total: 0 },
|
|
3081
|
+
finishReason: "error",
|
|
3082
|
+
error: `OpenAI API error: ${response.status} - ${JSON.stringify(errorData)}`
|
|
3083
|
+
};
|
|
2683
3084
|
}
|
|
2684
3085
|
const data = await response.json();
|
|
2685
3086
|
const choice = data.choices?.[0];
|
|
@@ -2719,13 +3120,16 @@ var AnthropicAdapter = class {
|
|
|
2719
3120
|
apiKey;
|
|
2720
3121
|
model;
|
|
2721
3122
|
baseUrl;
|
|
3123
|
+
rateLimiter;
|
|
2722
3124
|
constructor(options = {}) {
|
|
2723
3125
|
this.apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY || "";
|
|
2724
3126
|
this.model = options.model || "claude-4.5-opus-20260115";
|
|
2725
3127
|
this.baseUrl = options.baseUrl || "https://api.anthropic.com";
|
|
2726
3128
|
this.maxContextTokens = getModelContextSize("anthropic", this.model);
|
|
3129
|
+
const rateLimit = options.requestsPerMinute || getDefaultRateLimit("anthropic", this.model);
|
|
3130
|
+
this.rateLimiter = new RateLimiter({ requestsPerMinute: rateLimit });
|
|
2727
3131
|
}
|
|
2728
|
-
async complete(request) {
|
|
3132
|
+
async complete(request, retryCount = 0) {
|
|
2729
3133
|
if (!this.apiKey) {
|
|
2730
3134
|
return {
|
|
2731
3135
|
content: "",
|
|
@@ -2734,6 +3138,7 @@ var AnthropicAdapter = class {
|
|
|
2734
3138
|
error: "Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable."
|
|
2735
3139
|
};
|
|
2736
3140
|
}
|
|
3141
|
+
await this.rateLimiter.waitForSlot();
|
|
2737
3142
|
try {
|
|
2738
3143
|
const response = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
2739
3144
|
method: "POST",
|
|
@@ -2753,11 +3158,29 @@ var AnthropicAdapter = class {
|
|
|
2753
3158
|
stop_sequences: request.stopSequences
|
|
2754
3159
|
})
|
|
2755
3160
|
});
|
|
3161
|
+
if (response.status === 429) {
|
|
3162
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
3163
|
+
const waitTime = retryAfter ? parseInt(retryAfter) * 1e3 : 6e4;
|
|
3164
|
+
if (retryCount < 3) {
|
|
3165
|
+
console.warn(`Anthropic rate limited. Waiting ${waitTime}ms before retry (${retryCount + 1}/3)...`);
|
|
3166
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitTime));
|
|
3167
|
+
return this.complete(request, retryCount + 1);
|
|
3168
|
+
}
|
|
3169
|
+
return {
|
|
3170
|
+
content: "",
|
|
3171
|
+
tokensUsed: { prompt: 0, completion: 0, total: 0 },
|
|
3172
|
+
finishReason: "error",
|
|
3173
|
+
error: `Rate limit exceeded after ${retryCount} retries`
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
2756
3176
|
if (!response.ok) {
|
|
2757
3177
|
const errorData = await response.json().catch(() => ({}));
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
3178
|
+
return {
|
|
3179
|
+
content: "",
|
|
3180
|
+
tokensUsed: { prompt: 0, completion: 0, total: 0 },
|
|
3181
|
+
finishReason: "error",
|
|
3182
|
+
error: `Anthropic API error: ${response.status} - ${JSON.stringify(errorData)}`
|
|
3183
|
+
};
|
|
2761
3184
|
}
|
|
2762
3185
|
const data = await response.json();
|
|
2763
3186
|
const content = data.content?.filter((c) => c.type === "text")?.map((c) => c.text || "")?.join("") || "";
|
|
@@ -3040,6 +3463,13 @@ function createContextAPI(rawContext) {
|
|
|
3040
3463
|
}).filter(Boolean);
|
|
3041
3464
|
},
|
|
3042
3465
|
getFile: (path) => {
|
|
3466
|
+
const MAX_PATH_LENGTH = 1e3;
|
|
3467
|
+
if (path.length > MAX_PATH_LENGTH) {
|
|
3468
|
+
return null;
|
|
3469
|
+
}
|
|
3470
|
+
if (!/^[\w\-./\\]+$/.test(path)) {
|
|
3471
|
+
return null;
|
|
3472
|
+
}
|
|
3043
3473
|
const escapedPath = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3044
3474
|
const pattern = new RegExp(
|
|
3045
3475
|
`^={3,}\\s*FILE:\\s*${escapedPath}\\s*={3,}$([\\s\\S]*?)(?=^={3,}\\s*FILE:|$)`,
|
|
@@ -3478,7 +3908,8 @@ var RLMEngine = class {
|
|
|
3478
3908
|
const state = {
|
|
3479
3909
|
depth,
|
|
3480
3910
|
consumedTokens: 0,
|
|
3481
|
-
visitedPaths: /* @__PURE__ */ new
|
|
3911
|
+
visitedPaths: /* @__PURE__ */ new Map(),
|
|
3912
|
+
// Changed to Map with timestamps for memory leak fix
|
|
3482
3913
|
executionLog: [],
|
|
3483
3914
|
iteration: 0,
|
|
3484
3915
|
startTime
|
|
@@ -3588,7 +4019,9 @@ Depth: ${depth}/${this.config.maxDepth}`
|
|
|
3588
4019
|
entry.output = result.success ? result.output || result.stdout : result.error || "Unknown error";
|
|
3589
4020
|
state.executionLog.push(entry);
|
|
3590
4021
|
const pathKey = action.code.slice(0, 100);
|
|
3591
|
-
|
|
4022
|
+
const now = Date.now();
|
|
4023
|
+
const lastSeen = state.visitedPaths.get(pathKey);
|
|
4024
|
+
if (lastSeen !== void 0) {
|
|
3592
4025
|
messages.push({ role: "assistant", content: response.content });
|
|
3593
4026
|
messages.push({
|
|
3594
4027
|
role: "user",
|
|
@@ -3596,7 +4029,14 @@ Depth: ${depth}/${this.config.maxDepth}`
|
|
|
3596
4029
|
});
|
|
3597
4030
|
continue;
|
|
3598
4031
|
}
|
|
3599
|
-
state.visitedPaths.
|
|
4032
|
+
state.visitedPaths.set(pathKey, now);
|
|
4033
|
+
if (state.visitedPaths.size > 50) {
|
|
4034
|
+
const entries = Array.from(state.visitedPaths.entries());
|
|
4035
|
+
entries.sort((a, b) => a[1] - b[1]);
|
|
4036
|
+
for (let i = 0; i < 10; i++) {
|
|
4037
|
+
state.visitedPaths.delete(entries[i][0]);
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
3600
4040
|
messages.push({ role: "assistant", content: response.content });
|
|
3601
4041
|
messages.push({
|
|
3602
4042
|
role: "user",
|
|
@@ -3784,7 +4224,7 @@ function createRLMEngine(options = {}) {
|
|
|
3784
4224
|
}
|
|
3785
4225
|
|
|
3786
4226
|
// src/rlm/proposal.ts
|
|
3787
|
-
import { createHash as createHash3 } from "crypto";
|
|
4227
|
+
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
3788
4228
|
var ProposalManager = class {
|
|
3789
4229
|
proposals = /* @__PURE__ */ new Map();
|
|
3790
4230
|
fileSnapshots = /* @__PURE__ */ new Map();
|
|
@@ -3960,7 +4400,10 @@ Rejection reason: ${reason}`;
|
|
|
3960
4400
|
}
|
|
3961
4401
|
// Private helpers
|
|
3962
4402
|
generateId() {
|
|
3963
|
-
|
|
4403
|
+
const bytes = randomBytes2(16);
|
|
4404
|
+
const hex = bytes.toString("hex");
|
|
4405
|
+
const timestamp = Date.now().toString(36);
|
|
4406
|
+
return `prop_${timestamp}_${hex.substring(0, 16)}`;
|
|
3964
4407
|
}
|
|
3965
4408
|
hashContent(content) {
|
|
3966
4409
|
return createHash3("sha256").update(content).digest("hex").substring(0, 16);
|
|
@@ -4598,9 +5041,21 @@ function createWatchdog(config) {
|
|
|
4598
5041
|
|
|
4599
5042
|
// src/sync/team-sync.ts
|
|
4600
5043
|
import { execSync } from "child_process";
|
|
4601
|
-
import { readFileSync as
|
|
5044
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
4602
5045
|
import { join as join5 } from "path";
|
|
4603
5046
|
import { parse, stringify as stringify2 } from "yaml";
|
|
5047
|
+
function validateGitBranchName(branch) {
|
|
5048
|
+
if (!/^[A-Za-z0-9/_-]+$/.test(branch)) {
|
|
5049
|
+
throw new Error(`Invalid git branch name: "${branch}". Only alphanumeric, _, -, and / are allowed.`);
|
|
5050
|
+
}
|
|
5051
|
+
return branch;
|
|
5052
|
+
}
|
|
5053
|
+
function validateGitRemoteName(remote) {
|
|
5054
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(remote)) {
|
|
5055
|
+
throw new Error(`Invalid git remote name: "${remote}". Only alphanumeric, _, -, and . are allowed.`);
|
|
5056
|
+
}
|
|
5057
|
+
return remote;
|
|
5058
|
+
}
|
|
4604
5059
|
var DEFAULT_SYNC_CONFIG = {
|
|
4605
5060
|
enabled: false,
|
|
4606
5061
|
remote: "origin",
|
|
@@ -4616,11 +5071,17 @@ var TeamSync = class {
|
|
|
4616
5071
|
this.rootDir = rootDir;
|
|
4617
5072
|
this.contextosDir = join5(rootDir, ".contextos");
|
|
4618
5073
|
this.syncConfig = this.loadSyncConfig();
|
|
5074
|
+
if (this.syncConfig.remote) {
|
|
5075
|
+
this.syncConfig.remote = validateGitRemoteName(this.syncConfig.remote);
|
|
5076
|
+
}
|
|
5077
|
+
if (this.syncConfig.branch) {
|
|
5078
|
+
this.syncConfig.branch = validateGitBranchName(this.syncConfig.branch);
|
|
5079
|
+
}
|
|
4619
5080
|
}
|
|
4620
5081
|
loadSyncConfig() {
|
|
4621
5082
|
const configPath = join5(this.contextosDir, "sync.yaml");
|
|
4622
|
-
if (
|
|
4623
|
-
const content =
|
|
5083
|
+
if (existsSync5(configPath)) {
|
|
5084
|
+
const content = readFileSync5(configPath, "utf-8");
|
|
4624
5085
|
return { ...DEFAULT_SYNC_CONFIG, ...parse(content) };
|
|
4625
5086
|
}
|
|
4626
5087
|
return DEFAULT_SYNC_CONFIG;
|
|
@@ -4634,7 +5095,8 @@ var TeamSync = class {
|
|
|
4634
5095
|
*/
|
|
4635
5096
|
async initialize(remote = "origin") {
|
|
4636
5097
|
this.syncConfig.enabled = true;
|
|
4637
|
-
this.syncConfig.remote = remote;
|
|
5098
|
+
this.syncConfig.remote = validateGitRemoteName(remote);
|
|
5099
|
+
this.syncConfig.branch = validateGitBranchName(this.syncConfig.branch);
|
|
4638
5100
|
this.saveSyncConfig();
|
|
4639
5101
|
try {
|
|
4640
5102
|
execSync(`git checkout -b ${this.syncConfig.branch}`, {
|
|
@@ -4740,7 +5202,7 @@ var TemplateManager = class {
|
|
|
4740
5202
|
templatesDir;
|
|
4741
5203
|
constructor(rootDir) {
|
|
4742
5204
|
this.templatesDir = join5(rootDir, ".contextos", "templates");
|
|
4743
|
-
if (!
|
|
5205
|
+
if (!existsSync5(this.templatesDir)) {
|
|
4744
5206
|
mkdirSync3(this.templatesDir, { recursive: true });
|
|
4745
5207
|
}
|
|
4746
5208
|
}
|
|
@@ -4753,7 +5215,7 @@ var TemplateManager = class {
|
|
|
4753
5215
|
const files = __require("fs").readdirSync(this.templatesDir);
|
|
4754
5216
|
for (const file of files) {
|
|
4755
5217
|
if (file.endsWith(".yaml")) {
|
|
4756
|
-
const content =
|
|
5218
|
+
const content = readFileSync5(join5(this.templatesDir, file), "utf-8");
|
|
4757
5219
|
templates.push(parse(content));
|
|
4758
5220
|
}
|
|
4759
5221
|
}
|
|
@@ -4772,8 +5234,8 @@ var TemplateManager = class {
|
|
|
4772
5234
|
description,
|
|
4773
5235
|
author,
|
|
4774
5236
|
version: "1.0.0",
|
|
4775
|
-
context:
|
|
4776
|
-
config:
|
|
5237
|
+
context: existsSync5(contextPath) ? parse(readFileSync5(contextPath, "utf-8")) : {},
|
|
5238
|
+
config: existsSync5(configPath) ? parse(readFileSync5(configPath, "utf-8")) : {}
|
|
4777
5239
|
};
|
|
4778
5240
|
const templatePath = join5(this.templatesDir, `${name}.yaml`);
|
|
4779
5241
|
writeFileSync2(templatePath, stringify2(template, { indent: 2 }), "utf-8");
|
|
@@ -4783,10 +5245,10 @@ var TemplateManager = class {
|
|
|
4783
5245
|
*/
|
|
4784
5246
|
apply(name) {
|
|
4785
5247
|
const templatePath = join5(this.templatesDir, `${name}.yaml`);
|
|
4786
|
-
if (!
|
|
5248
|
+
if (!existsSync5(templatePath)) {
|
|
4787
5249
|
throw new Error(`Template '${name}' not found`);
|
|
4788
5250
|
}
|
|
4789
|
-
const template = parse(
|
|
5251
|
+
const template = parse(readFileSync5(templatePath, "utf-8"));
|
|
4790
5252
|
const contextPath = join5(this.templatesDir, "..", "context.yaml");
|
|
4791
5253
|
const configPath = join5(this.templatesDir, "..", "config.yaml");
|
|
4792
5254
|
if (template.context && Object.keys(template.context).length > 0) {
|
|
@@ -4800,8 +5262,10 @@ var TemplateManager = class {
|
|
|
4800
5262
|
|
|
4801
5263
|
// src/sync/cloud-sync.ts
|
|
4802
5264
|
import crypto from "crypto";
|
|
4803
|
-
import {
|
|
5265
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5266
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
4804
5267
|
import { join as join6 } from "path";
|
|
5268
|
+
var { AbortController } = globalThis;
|
|
4805
5269
|
var E2EEncryption = class {
|
|
4806
5270
|
algorithm = "aes-256-gcm";
|
|
4807
5271
|
keyLength = 32;
|
|
@@ -4877,7 +5341,7 @@ var CloudSync = class {
|
|
|
4877
5341
|
this.config.encryptionKey = key;
|
|
4878
5342
|
this.config.enabled = true;
|
|
4879
5343
|
const saltPath = join6(this.rootDir, ".contextos", ".salt");
|
|
4880
|
-
|
|
5344
|
+
await writeFile2(saltPath, salt, "utf-8");
|
|
4881
5345
|
return key;
|
|
4882
5346
|
}
|
|
4883
5347
|
/**
|
|
@@ -4890,9 +5354,11 @@ var CloudSync = class {
|
|
|
4890
5354
|
try {
|
|
4891
5355
|
const contextPath = join6(this.rootDir, ".contextos", "context.yaml");
|
|
4892
5356
|
const configPath = join6(this.rootDir, ".contextos", "config.yaml");
|
|
5357
|
+
const contextContent = existsSync6(contextPath) ? await readFile3(contextPath, "utf-8") : "";
|
|
5358
|
+
const configContent = existsSync6(configPath) ? await readFile3(configPath, "utf-8") : "";
|
|
4893
5359
|
const data = JSON.stringify({
|
|
4894
|
-
context:
|
|
4895
|
-
config:
|
|
5360
|
+
context: contextContent,
|
|
5361
|
+
config: configContent,
|
|
4896
5362
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4897
5363
|
});
|
|
4898
5364
|
const encryptedData = this.encryption.encrypt(data, this.config.encryptionKey);
|
|
@@ -4904,20 +5370,32 @@ var CloudSync = class {
|
|
|
4904
5370
|
data: encryptedData,
|
|
4905
5371
|
checksum
|
|
4906
5372
|
};
|
|
4907
|
-
const
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
5373
|
+
const controller = new AbortController();
|
|
5374
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
5375
|
+
try {
|
|
5376
|
+
const response = await fetch(`${this.config.apiEndpoint}/sync/upload`, {
|
|
5377
|
+
method: "POST",
|
|
5378
|
+
headers: { "Content-Type": "application/json" },
|
|
5379
|
+
body: JSON.stringify(payload),
|
|
5380
|
+
signal: controller.signal
|
|
5381
|
+
});
|
|
5382
|
+
clearTimeout(timeoutId);
|
|
5383
|
+
if (!response.ok) {
|
|
5384
|
+
throw new Error(`Upload failed: ${response.status}`);
|
|
5385
|
+
}
|
|
5386
|
+
return {
|
|
5387
|
+
success: true,
|
|
5388
|
+
action: "upload",
|
|
5389
|
+
message: "Context uploaded and encrypted",
|
|
5390
|
+
timestamp: payload.timestamp
|
|
5391
|
+
};
|
|
5392
|
+
} catch (fetchError) {
|
|
5393
|
+
clearTimeout(timeoutId);
|
|
5394
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
5395
|
+
return { success: false, action: "upload", message: "Upload timed out after 30 seconds" };
|
|
5396
|
+
}
|
|
5397
|
+
throw fetchError;
|
|
4914
5398
|
}
|
|
4915
|
-
return {
|
|
4916
|
-
success: true,
|
|
4917
|
-
action: "upload",
|
|
4918
|
-
message: "Context uploaded and encrypted",
|
|
4919
|
-
timestamp: payload.timestamp
|
|
4920
|
-
};
|
|
4921
5399
|
} catch (error) {
|
|
4922
5400
|
return {
|
|
4923
5401
|
success: false,
|
|
@@ -4934,30 +5412,41 @@ var CloudSync = class {
|
|
|
4934
5412
|
return { success: false, action: "download", message: "Cloud sync not initialized" };
|
|
4935
5413
|
}
|
|
4936
5414
|
try {
|
|
4937
|
-
const
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
5415
|
+
const controller = new AbortController();
|
|
5416
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
5417
|
+
try {
|
|
5418
|
+
const response = await fetch(
|
|
5419
|
+
`${this.config.apiEndpoint}/sync/download?teamId=${this.config.teamId}&userId=${this.config.userId}`,
|
|
5420
|
+
{ method: "GET", signal: controller.signal }
|
|
5421
|
+
);
|
|
5422
|
+
clearTimeout(timeoutId);
|
|
5423
|
+
if (!response.ok) {
|
|
5424
|
+
throw new Error(`Download failed: ${response.status}`);
|
|
5425
|
+
}
|
|
5426
|
+
const payload = await response.json();
|
|
5427
|
+
const decryptedData = this.encryption.decrypt(payload.data, this.config.encryptionKey);
|
|
5428
|
+
const data = JSON.parse(decryptedData);
|
|
5429
|
+
const expectedChecksum = this.encryption.checksum(decryptedData);
|
|
5430
|
+
if (expectedChecksum !== payload.checksum) {
|
|
5431
|
+
return { success: false, action: "download", message: "Checksum mismatch - data corrupted" };
|
|
5432
|
+
}
|
|
5433
|
+
const contextPath = join6(this.rootDir, ".contextos", "context.yaml");
|
|
5434
|
+
const configPath = join6(this.rootDir, ".contextos", "config.yaml");
|
|
5435
|
+
if (data.context) await writeFile2(contextPath, data.context, "utf-8");
|
|
5436
|
+
if (data.config) await writeFile2(configPath, data.config, "utf-8");
|
|
5437
|
+
return {
|
|
5438
|
+
success: true,
|
|
5439
|
+
action: "download",
|
|
5440
|
+
message: "Context downloaded and decrypted",
|
|
5441
|
+
timestamp: payload.timestamp
|
|
5442
|
+
};
|
|
5443
|
+
} catch (fetchError) {
|
|
5444
|
+
clearTimeout(timeoutId);
|
|
5445
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
5446
|
+
return { success: false, action: "download", message: "Download timed out after 30 seconds" };
|
|
5447
|
+
}
|
|
5448
|
+
throw fetchError;
|
|
4950
5449
|
}
|
|
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
5450
|
} catch (error) {
|
|
4962
5451
|
return {
|
|
4963
5452
|
success: false,
|
|
@@ -4967,19 +5456,19 @@ var CloudSync = class {
|
|
|
4967
5456
|
}
|
|
4968
5457
|
}
|
|
4969
5458
|
/**
|
|
4970
|
-
* Check if encryption key is valid
|
|
5459
|
+
* Check if encryption key is valid - Fix R9: Use async readFile
|
|
4971
5460
|
*/
|
|
4972
|
-
validateKey(password) {
|
|
5461
|
+
async validateKey(password) {
|
|
4973
5462
|
const saltPath = join6(this.rootDir, ".contextos", ".salt");
|
|
4974
|
-
if (!
|
|
4975
|
-
const salt =
|
|
5463
|
+
if (!existsSync6(saltPath)) return false;
|
|
5464
|
+
const salt = await readFile3(saltPath, "utf-8");
|
|
4976
5465
|
const derivedKey = this.encryption.deriveKey(password, salt);
|
|
4977
5466
|
return derivedKey === this.config.encryptionKey;
|
|
4978
5467
|
}
|
|
4979
5468
|
};
|
|
4980
5469
|
|
|
4981
5470
|
// src/analytics/analytics.ts
|
|
4982
|
-
import { existsSync as
|
|
5471
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
4983
5472
|
import { join as join7 } from "path";
|
|
4984
5473
|
var AnalyticsCollector = class {
|
|
4985
5474
|
analyticsDir;
|
|
@@ -4989,7 +5478,7 @@ var AnalyticsCollector = class {
|
|
|
4989
5478
|
this.analyticsDir = join7(rootDir, ".contextos", "analytics");
|
|
4990
5479
|
this.eventsFile = join7(this.analyticsDir, "events.json");
|
|
4991
5480
|
this.statsFile = join7(this.analyticsDir, "stats.json");
|
|
4992
|
-
if (!
|
|
5481
|
+
if (!existsSync7(this.analyticsDir)) {
|
|
4993
5482
|
mkdirSync4(this.analyticsDir, { recursive: true });
|
|
4994
5483
|
}
|
|
4995
5484
|
}
|
|
@@ -5028,9 +5517,9 @@ var AnalyticsCollector = class {
|
|
|
5028
5517
|
});
|
|
5029
5518
|
}
|
|
5030
5519
|
loadEvents() {
|
|
5031
|
-
if (
|
|
5520
|
+
if (existsSync7(this.eventsFile)) {
|
|
5032
5521
|
try {
|
|
5033
|
-
return JSON.parse(
|
|
5522
|
+
return JSON.parse(readFileSync7(this.eventsFile, "utf-8"));
|
|
5034
5523
|
} catch {
|
|
5035
5524
|
return [];
|
|
5036
5525
|
}
|
|
@@ -5056,9 +5545,9 @@ var AnalyticsCollector = class {
|
|
|
5056
5545
|
writeFileSync4(this.statsFile, JSON.stringify(updatedStats, null, 2), "utf-8");
|
|
5057
5546
|
}
|
|
5058
5547
|
loadDailyStats() {
|
|
5059
|
-
if (
|
|
5548
|
+
if (existsSync7(this.statsFile)) {
|
|
5060
5549
|
try {
|
|
5061
|
-
return JSON.parse(
|
|
5550
|
+
return JSON.parse(readFileSync7(this.statsFile, "utf-8"));
|
|
5062
5551
|
} catch {
|
|
5063
5552
|
return [];
|
|
5064
5553
|
}
|
|
@@ -5124,7 +5613,7 @@ var AnalyticsCollector = class {
|
|
|
5124
5613
|
};
|
|
5125
5614
|
|
|
5126
5615
|
// src/compliance/rbac.ts
|
|
5127
|
-
import { readFileSync as
|
|
5616
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
5128
5617
|
import { join as join8 } from "path";
|
|
5129
5618
|
import crypto2 from "crypto";
|
|
5130
5619
|
var BUILT_IN_ROLES = [
|
|
@@ -5189,7 +5678,7 @@ var RBACManager = class {
|
|
|
5189
5678
|
this.rolesFile = join8(this.rbacDir, "roles.json");
|
|
5190
5679
|
this.usersFile = join8(this.rbacDir, "users.json");
|
|
5191
5680
|
this.policiesFile = join8(this.rbacDir, "policies.json");
|
|
5192
|
-
if (!
|
|
5681
|
+
if (!existsSync8(this.rbacDir)) {
|
|
5193
5682
|
mkdirSync5(this.rbacDir, { recursive: true });
|
|
5194
5683
|
}
|
|
5195
5684
|
this.loadData();
|
|
@@ -5198,27 +5687,27 @@ var RBACManager = class {
|
|
|
5198
5687
|
for (const role of BUILT_IN_ROLES) {
|
|
5199
5688
|
this.roles.set(role.id, role);
|
|
5200
5689
|
}
|
|
5201
|
-
if (
|
|
5690
|
+
if (existsSync8(this.rolesFile)) {
|
|
5202
5691
|
try {
|
|
5203
|
-
const customRoles = JSON.parse(
|
|
5692
|
+
const customRoles = JSON.parse(readFileSync8(this.rolesFile, "utf-8"));
|
|
5204
5693
|
for (const role of customRoles) {
|
|
5205
5694
|
this.roles.set(role.id, role);
|
|
5206
5695
|
}
|
|
5207
5696
|
} catch {
|
|
5208
5697
|
}
|
|
5209
5698
|
}
|
|
5210
|
-
if (
|
|
5699
|
+
if (existsSync8(this.usersFile)) {
|
|
5211
5700
|
try {
|
|
5212
|
-
const users = JSON.parse(
|
|
5701
|
+
const users = JSON.parse(readFileSync8(this.usersFile, "utf-8"));
|
|
5213
5702
|
for (const user of users) {
|
|
5214
5703
|
this.users.set(user.id, user);
|
|
5215
5704
|
}
|
|
5216
5705
|
} catch {
|
|
5217
5706
|
}
|
|
5218
5707
|
}
|
|
5219
|
-
if (
|
|
5708
|
+
if (existsSync8(this.policiesFile)) {
|
|
5220
5709
|
try {
|
|
5221
|
-
this.policies = JSON.parse(
|
|
5710
|
+
this.policies = JSON.parse(readFileSync8(this.policiesFile, "utf-8"));
|
|
5222
5711
|
} catch {
|
|
5223
5712
|
}
|
|
5224
5713
|
}
|
|
@@ -5379,7 +5868,7 @@ var RBACManager = class {
|
|
|
5379
5868
|
};
|
|
5380
5869
|
|
|
5381
5870
|
// src/compliance/audit.ts
|
|
5382
|
-
import { existsSync as
|
|
5871
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6, appendFileSync, mkdirSync as mkdirSync6 } from "fs";
|
|
5383
5872
|
import { join as join9 } from "path";
|
|
5384
5873
|
import crypto3 from "crypto";
|
|
5385
5874
|
var AuditLogger = class {
|
|
@@ -5389,7 +5878,7 @@ var AuditLogger = class {
|
|
|
5389
5878
|
constructor(rootDir) {
|
|
5390
5879
|
this.auditDir = join9(rootDir, ".contextos", "audit");
|
|
5391
5880
|
this.indexFile = join9(this.auditDir, "index.json");
|
|
5392
|
-
if (!
|
|
5881
|
+
if (!existsSync9(this.auditDir)) {
|
|
5393
5882
|
mkdirSync6(this.auditDir, { recursive: true });
|
|
5394
5883
|
}
|
|
5395
5884
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -5439,9 +5928,9 @@ var AuditLogger = class {
|
|
|
5439
5928
|
writeFileSync6(this.indexFile, JSON.stringify(index, null, 2), "utf-8");
|
|
5440
5929
|
}
|
|
5441
5930
|
loadIndex() {
|
|
5442
|
-
if (
|
|
5931
|
+
if (existsSync9(this.indexFile)) {
|
|
5443
5932
|
try {
|
|
5444
|
-
return JSON.parse(
|
|
5933
|
+
return JSON.parse(readFileSync9(this.indexFile, "utf-8"));
|
|
5445
5934
|
} catch {
|
|
5446
5935
|
return {};
|
|
5447
5936
|
}
|
|
@@ -5458,7 +5947,7 @@ var AuditLogger = class {
|
|
|
5458
5947
|
for (const logFile of logFiles) {
|
|
5459
5948
|
if (entries.length >= limit) break;
|
|
5460
5949
|
const filePath = join9(this.auditDir, logFile);
|
|
5461
|
-
const content =
|
|
5950
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
5462
5951
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
5463
5952
|
for (const line of lines.reverse()) {
|
|
5464
5953
|
if (entries.length >= limit) break;
|
|
@@ -5495,7 +5984,7 @@ var AuditLogger = class {
|
|
|
5495
5984
|
*/
|
|
5496
5985
|
verifyLogFile(filename) {
|
|
5497
5986
|
const filePath = join9(this.auditDir, filename);
|
|
5498
|
-
const content =
|
|
5987
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
5499
5988
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
5500
5989
|
const result = { valid: 0, invalid: 0, entries: [] };
|
|
5501
5990
|
for (const line of lines) {
|
|
@@ -5549,8 +6038,8 @@ var AuditLogger = class {
|
|
|
5549
6038
|
};
|
|
5550
6039
|
|
|
5551
6040
|
// src/plugins/manager.ts
|
|
5552
|
-
import { existsSync as
|
|
5553
|
-
import { join as join10, resolve } from "path";
|
|
6041
|
+
import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync10, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
|
|
6042
|
+
import { join as join10, resolve, normalize as normalize2 } from "path";
|
|
5554
6043
|
var PluginManager = class {
|
|
5555
6044
|
plugins = /* @__PURE__ */ new Map();
|
|
5556
6045
|
pluginsDir;
|
|
@@ -5559,7 +6048,7 @@ var PluginManager = class {
|
|
|
5559
6048
|
constructor(projectRoot) {
|
|
5560
6049
|
this.projectRoot = projectRoot;
|
|
5561
6050
|
this.pluginsDir = join10(projectRoot, ".contextos", "plugins");
|
|
5562
|
-
if (!
|
|
6051
|
+
if (!existsSync10(this.pluginsDir)) {
|
|
5563
6052
|
mkdirSync7(this.pluginsDir, { recursive: true });
|
|
5564
6053
|
}
|
|
5565
6054
|
}
|
|
@@ -5569,7 +6058,7 @@ var PluginManager = class {
|
|
|
5569
6058
|
async loadAll() {
|
|
5570
6059
|
const loaded = [];
|
|
5571
6060
|
const errors = [];
|
|
5572
|
-
if (!
|
|
6061
|
+
if (!existsSync10(this.pluginsDir)) {
|
|
5573
6062
|
return { loaded, errors };
|
|
5574
6063
|
}
|
|
5575
6064
|
const entries = readdirSync(this.pluginsDir, { withFileTypes: true });
|
|
@@ -5592,17 +6081,32 @@ var PluginManager = class {
|
|
|
5592
6081
|
* Load a single plugin from path
|
|
5593
6082
|
*/
|
|
5594
6083
|
async loadPlugin(pluginPath) {
|
|
6084
|
+
const absolutePluginPath = resolve(pluginPath);
|
|
6085
|
+
const absolutePluginsDir = resolve(this.pluginsDir);
|
|
6086
|
+
const normalizedPlugin = normalize2(absolutePluginPath);
|
|
6087
|
+
const normalizedPluginsDir = normalize2(absolutePluginsDir);
|
|
6088
|
+
if (!normalizedPlugin.startsWith(normalizedPluginsDir)) {
|
|
6089
|
+
throw new Error(`Plugin path "${pluginPath}" is outside plugins directory`);
|
|
6090
|
+
}
|
|
5595
6091
|
const manifestPath = join10(pluginPath, "package.json");
|
|
5596
|
-
if (!
|
|
6092
|
+
if (!existsSync10(manifestPath)) {
|
|
5597
6093
|
throw new Error(`Plugin manifest not found: ${manifestPath}`);
|
|
5598
6094
|
}
|
|
5599
|
-
const manifestContent =
|
|
5600
|
-
|
|
6095
|
+
const manifestContent = readFileSync10(manifestPath, "utf-8");
|
|
6096
|
+
let manifest;
|
|
6097
|
+
try {
|
|
6098
|
+
manifest = JSON.parse(manifestContent);
|
|
6099
|
+
} catch (error) {
|
|
6100
|
+
throw new Error(
|
|
6101
|
+
`Failed to parse plugin manifest at ${manifestPath}: ${error instanceof Error ? error.message : String(error)}
|
|
6102
|
+
The file may contain invalid JSON.`
|
|
6103
|
+
);
|
|
6104
|
+
}
|
|
5601
6105
|
if (!manifest.name || !manifest.version || !manifest.main) {
|
|
5602
6106
|
throw new Error(`Invalid plugin manifest: missing name, version, or main`);
|
|
5603
6107
|
}
|
|
5604
6108
|
const mainPath = join10(pluginPath, manifest.main);
|
|
5605
|
-
if (!
|
|
6109
|
+
if (!existsSync10(mainPath)) {
|
|
5606
6110
|
throw new Error(`Plugin main file not found: ${mainPath}`);
|
|
5607
6111
|
}
|
|
5608
6112
|
const pluginModule = await import(`file://${resolve(mainPath)}`);
|
|
@@ -5669,21 +6173,29 @@ var PluginManager = class {
|
|
|
5669
6173
|
*/
|
|
5670
6174
|
async install(source, options = {}) {
|
|
5671
6175
|
const targetDir = join10(this.pluginsDir, source.split("/").pop() || source);
|
|
5672
|
-
if (
|
|
6176
|
+
if (existsSync10(targetDir) && !options.force) {
|
|
5673
6177
|
throw new Error(`Plugin already installed: ${source}`);
|
|
5674
6178
|
}
|
|
5675
6179
|
if (options.local) {
|
|
5676
6180
|
const sourcePath = resolve(source);
|
|
5677
|
-
if (!
|
|
6181
|
+
if (!existsSync10(sourcePath)) {
|
|
5678
6182
|
throw new Error(`Source path not found: ${sourcePath}`);
|
|
5679
6183
|
}
|
|
5680
|
-
if (options.force &&
|
|
6184
|
+
if (options.force && existsSync10(targetDir)) {
|
|
5681
6185
|
rmSync(targetDir, { recursive: true });
|
|
5682
6186
|
}
|
|
5683
6187
|
mkdirSync7(targetDir, { recursive: true });
|
|
5684
|
-
|
|
6188
|
+
let manifest;
|
|
6189
|
+
try {
|
|
6190
|
+
const manifestContent = readFileSync10(join10(sourcePath, "package.json"), "utf-8");
|
|
6191
|
+
manifest = JSON.parse(manifestContent);
|
|
6192
|
+
} catch (error) {
|
|
6193
|
+
throw new Error(
|
|
6194
|
+
`Failed to parse plugin manifest at ${sourcePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
6195
|
+
);
|
|
6196
|
+
}
|
|
5685
6197
|
writeFileSync7(join10(targetDir, "package.json"), JSON.stringify(manifest, null, 2));
|
|
5686
|
-
const mainContent =
|
|
6198
|
+
const mainContent = readFileSync10(join10(sourcePath, manifest.main), "utf-8");
|
|
5687
6199
|
writeFileSync7(join10(targetDir, manifest.main), mainContent);
|
|
5688
6200
|
} else {
|
|
5689
6201
|
throw new Error("Remote plugin installation not yet implemented");
|
|
@@ -5701,7 +6213,7 @@ var PluginManager = class {
|
|
|
5701
6213
|
const state = this.plugins.get(name);
|
|
5702
6214
|
if (!state) return false;
|
|
5703
6215
|
await this.unloadPlugin(name);
|
|
5704
|
-
if (
|
|
6216
|
+
if (existsSync10(state.path)) {
|
|
5705
6217
|
rmSync(state.path, { recursive: true });
|
|
5706
6218
|
}
|
|
5707
6219
|
return true;
|
|
@@ -5755,7 +6267,7 @@ var PluginManager = class {
|
|
|
5755
6267
|
*/
|
|
5756
6268
|
createPluginScaffold(template) {
|
|
5757
6269
|
const pluginDir = join10(this.pluginsDir, template.name);
|
|
5758
|
-
if (
|
|
6270
|
+
if (existsSync10(pluginDir)) {
|
|
5759
6271
|
throw new Error(`Plugin directory already exists: ${template.name}`);
|
|
5760
6272
|
}
|
|
5761
6273
|
mkdirSync7(pluginDir, { recursive: true });
|
|
@@ -5821,6 +6333,9 @@ ${commandsCode}
|
|
|
5821
6333
|
this.storage.set(pluginName, /* @__PURE__ */ new Map());
|
|
5822
6334
|
}
|
|
5823
6335
|
const pluginStorage = this.storage.get(pluginName);
|
|
6336
|
+
if (!pluginStorage) {
|
|
6337
|
+
throw new Error(`Failed to get plugin storage for ${pluginName}`);
|
|
6338
|
+
}
|
|
5824
6339
|
return {
|
|
5825
6340
|
projectRoot: this.projectRoot,
|
|
5826
6341
|
configDir: join10(this.projectRoot, ".contextos"),
|
|
@@ -5834,8 +6349,13 @@ ${commandsCode}
|
|
|
5834
6349
|
return { files: [], context: "" };
|
|
5835
6350
|
},
|
|
5836
6351
|
readFile: async (path) => {
|
|
5837
|
-
const fullPath =
|
|
5838
|
-
|
|
6352
|
+
const fullPath = resolve(this.projectRoot, path);
|
|
6353
|
+
const normalized = normalize2(fullPath);
|
|
6354
|
+
const rootNormalized = normalize2(this.projectRoot);
|
|
6355
|
+
if (!normalized.startsWith(rootNormalized)) {
|
|
6356
|
+
throw new Error(`Path traversal detected: "${path}" escapes project boundaries`);
|
|
6357
|
+
}
|
|
6358
|
+
return readFileSync10(normalized, "utf-8");
|
|
5839
6359
|
},
|
|
5840
6360
|
getDependencies: async (_path, _depth = 2) => {
|
|
5841
6361
|
return [];
|
|
@@ -5855,7 +6375,7 @@ function createPluginManager(projectRoot) {
|
|
|
5855
6375
|
}
|
|
5856
6376
|
|
|
5857
6377
|
// src/plugins/registry.ts
|
|
5858
|
-
import { existsSync as
|
|
6378
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
|
|
5859
6379
|
import { join as join11 } from "path";
|
|
5860
6380
|
var PluginRegistry = class {
|
|
5861
6381
|
config;
|
|
@@ -5868,7 +6388,7 @@ var PluginRegistry = class {
|
|
|
5868
6388
|
*/
|
|
5869
6389
|
listLocal() {
|
|
5870
6390
|
const plugins = [];
|
|
5871
|
-
if (!
|
|
6391
|
+
if (!existsSync11(this.config.localDir)) {
|
|
5872
6392
|
return plugins;
|
|
5873
6393
|
}
|
|
5874
6394
|
const entries = readdirSync2(this.config.localDir, { withFileTypes: true });
|
|
@@ -5876,13 +6396,13 @@ var PluginRegistry = class {
|
|
|
5876
6396
|
if (!entry.isDirectory()) continue;
|
|
5877
6397
|
const pluginPath = join11(this.config.localDir, entry.name);
|
|
5878
6398
|
const manifestPath = join11(pluginPath, "package.json");
|
|
5879
|
-
if (!
|
|
6399
|
+
if (!existsSync11(manifestPath)) continue;
|
|
5880
6400
|
try {
|
|
5881
6401
|
const manifest = JSON.parse(
|
|
5882
|
-
|
|
6402
|
+
readFileSync11(manifestPath, "utf-8")
|
|
5883
6403
|
);
|
|
5884
6404
|
const disabledPath = join11(pluginPath, ".disabled");
|
|
5885
|
-
const enabled = !
|
|
6405
|
+
const enabled = !existsSync11(disabledPath);
|
|
5886
6406
|
plugins.push({
|
|
5887
6407
|
name: manifest.name,
|
|
5888
6408
|
version: manifest.version,
|
|
@@ -6020,7 +6540,7 @@ function createPluginRegistry(projectRoot) {
|
|
|
6020
6540
|
}
|
|
6021
6541
|
|
|
6022
6542
|
// src/finetuning/collector.ts
|
|
6023
|
-
import { existsSync as
|
|
6543
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "fs";
|
|
6024
6544
|
import { join as join12 } from "path";
|
|
6025
6545
|
import { createHash as createHash4 } from "crypto";
|
|
6026
6546
|
var TrainingDataCollector = class {
|
|
@@ -6029,7 +6549,7 @@ var TrainingDataCollector = class {
|
|
|
6029
6549
|
loaded = false;
|
|
6030
6550
|
constructor(projectRoot) {
|
|
6031
6551
|
this.dataDir = join12(projectRoot, ".contextos", "training");
|
|
6032
|
-
if (!
|
|
6552
|
+
if (!existsSync12(this.dataDir)) {
|
|
6033
6553
|
mkdirSync8(this.dataDir, { recursive: true });
|
|
6034
6554
|
}
|
|
6035
6555
|
}
|
|
@@ -6039,9 +6559,9 @@ var TrainingDataCollector = class {
|
|
|
6039
6559
|
load() {
|
|
6040
6560
|
if (this.loaded) return;
|
|
6041
6561
|
const dataFile = join12(this.dataDir, "examples.json");
|
|
6042
|
-
if (
|
|
6562
|
+
if (existsSync12(dataFile)) {
|
|
6043
6563
|
try {
|
|
6044
|
-
const data = JSON.parse(
|
|
6564
|
+
const data = JSON.parse(readFileSync12(dataFile, "utf-8"));
|
|
6045
6565
|
this.examples = data.examples || [];
|
|
6046
6566
|
} catch {
|
|
6047
6567
|
this.examples = [];
|
|
@@ -6197,7 +6717,7 @@ function createTrainingDataCollector(projectRoot) {
|
|
|
6197
6717
|
}
|
|
6198
6718
|
|
|
6199
6719
|
// src/finetuning/formatter.ts
|
|
6200
|
-
import { createReadStream, createWriteStream, existsSync as
|
|
6720
|
+
import { createReadStream, createWriteStream, existsSync as existsSync13 } from "fs";
|
|
6201
6721
|
import { createInterface } from "readline";
|
|
6202
6722
|
var DatasetFormatter = class {
|
|
6203
6723
|
/**
|
|
@@ -6255,7 +6775,7 @@ var DatasetFormatter = class {
|
|
|
6255
6775
|
dateRange: { earliest: /* @__PURE__ */ new Date(), latest: /* @__PURE__ */ new Date(0) }
|
|
6256
6776
|
}
|
|
6257
6777
|
};
|
|
6258
|
-
if (!
|
|
6778
|
+
if (!existsSync13(filePath)) {
|
|
6259
6779
|
result.valid = false;
|
|
6260
6780
|
result.errors.push({
|
|
6261
6781
|
line: 0,
|
|
@@ -7162,7 +7682,9 @@ export {
|
|
|
7162
7682
|
mergeSmallChunks,
|
|
7163
7683
|
parseWithRegex,
|
|
7164
7684
|
prepareSandboxVariables,
|
|
7685
|
+
resetContextBuilder,
|
|
7165
7686
|
resetGlobalBlackboard,
|
|
7687
|
+
resetParser,
|
|
7166
7688
|
saveConfigYaml,
|
|
7167
7689
|
saveContextYaml,
|
|
7168
7690
|
setGlobalLogger,
|