@contextos/core 0.2.0 → 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 +749 -213
- 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
|
-
|
|
561
|
+
if (parserInstance) {
|
|
562
|
+
return parserInstance;
|
|
563
|
+
}
|
|
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
|
+
})();
|
|
439
576
|
}
|
|
440
|
-
return
|
|
577
|
+
return initializationPromise;
|
|
578
|
+
}
|
|
579
|
+
function resetParser() {
|
|
580
|
+
parserInstance = null;
|
|
581
|
+
initializationPromise = null;
|
|
441
582
|
}
|
|
442
583
|
|
|
443
584
|
// src/parser/detector.ts
|
|
@@ -993,9 +1134,23 @@ var DependencyGraph = class {
|
|
|
993
1134
|
* Deserialize graph from JSON
|
|
994
1135
|
*/
|
|
995
1136
|
fromJSON(data) {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1137
|
+
if (!data) {
|
|
1138
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
1139
|
+
this.edges = [];
|
|
1140
|
+
this.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (data.nodes instanceof Map) {
|
|
1144
|
+
this.nodes = data.nodes;
|
|
1145
|
+
} else if (Array.isArray(data.nodes)) {
|
|
1146
|
+
this.nodes = new Map(data.nodes);
|
|
1147
|
+
} else if (data.nodes && typeof data.nodes === "object") {
|
|
1148
|
+
this.nodes = new Map(Object.entries(data.nodes));
|
|
1149
|
+
} else {
|
|
1150
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
1151
|
+
}
|
|
1152
|
+
this.edges = Array.isArray(data.edges) ? data.edges : [];
|
|
1153
|
+
this.lastUpdated = data.lastUpdated || (/* @__PURE__ */ new Date()).toISOString();
|
|
999
1154
|
}
|
|
1000
1155
|
/**
|
|
1001
1156
|
* Get graph statistics
|
|
@@ -1027,7 +1182,6 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
|
1027
1182
|
import { dirname as dirname3 } from "path";
|
|
1028
1183
|
var VectorStore = class {
|
|
1029
1184
|
db = null;
|
|
1030
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1031
1185
|
embedder = null;
|
|
1032
1186
|
dbPath;
|
|
1033
1187
|
constructor(dbPath) {
|
|
@@ -1064,13 +1218,16 @@ var VectorStore = class {
|
|
|
1064
1218
|
}
|
|
1065
1219
|
/**
|
|
1066
1220
|
* Initialize the embedding model
|
|
1221
|
+
* Fixed: Better error handling for dynamic import failures
|
|
1067
1222
|
*/
|
|
1068
1223
|
async initializeEmbedder() {
|
|
1069
1224
|
try {
|
|
1070
1225
|
const { pipeline } = await import("@xenova/transformers");
|
|
1071
1226
|
this.embedder = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
1072
1227
|
} catch (error) {
|
|
1073
|
-
|
|
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).");
|
|
1074
1231
|
this.embedder = null;
|
|
1075
1232
|
}
|
|
1076
1233
|
}
|
|
@@ -1142,54 +1299,76 @@ var VectorStore = class {
|
|
|
1142
1299
|
}
|
|
1143
1300
|
/**
|
|
1144
1301
|
* Vector-based similarity search
|
|
1302
|
+
* Fixed: Uses pagination to prevent unbounded memory growth
|
|
1145
1303
|
*/
|
|
1146
1304
|
vectorSearch(queryEmbedding, limit) {
|
|
1147
1305
|
if (!this.db) throw new Error("VectorStore not initialized");
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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);
|
|
1165
1333
|
}
|
|
1166
1334
|
/**
|
|
1167
1335
|
* Text-based fallback search
|
|
1336
|
+
* Fixed: Uses pagination to prevent unbounded memory growth
|
|
1168
1337
|
*/
|
|
1169
1338
|
textSearch(query, limit) {
|
|
1170
1339
|
if (!this.db) throw new Error("VectorStore not initialized");
|
|
1171
1340
|
const terms = query.toLowerCase().split(/\s+/);
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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);
|
|
1193
1372
|
}
|
|
1194
1373
|
/**
|
|
1195
1374
|
* Remove chunks for a file
|
|
@@ -1237,6 +1416,39 @@ var VectorStore = class {
|
|
|
1237
1416
|
embeddedCount: stats.embedded_count
|
|
1238
1417
|
};
|
|
1239
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
|
+
}
|
|
1240
1452
|
/**
|
|
1241
1453
|
* Clear all data
|
|
1242
1454
|
*/
|
|
@@ -1700,11 +1912,110 @@ ${content}`;
|
|
|
1700
1912
|
};
|
|
1701
1913
|
|
|
1702
1914
|
// src/context/builder.ts
|
|
1703
|
-
import {
|
|
1704
|
-
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";
|
|
1705
1918
|
import { glob } from "glob";
|
|
1706
1919
|
import { stringify } from "yaml";
|
|
1707
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
|
+
|
|
1708
2019
|
// src/llm/gemini-client.ts
|
|
1709
2020
|
var DEFAULT_MODEL = "gemini-3-pro-preview";
|
|
1710
2021
|
var GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
@@ -1714,12 +2025,15 @@ var GeminiClient = class {
|
|
|
1714
2025
|
maxOutputTokens;
|
|
1715
2026
|
temperature;
|
|
1716
2027
|
thinkingLevel;
|
|
2028
|
+
rateLimiter;
|
|
1717
2029
|
constructor(config) {
|
|
1718
2030
|
this.apiKey = config.apiKey;
|
|
1719
2031
|
this.model = config.model || DEFAULT_MODEL;
|
|
1720
2032
|
this.maxOutputTokens = config.maxOutputTokens || 2048;
|
|
1721
2033
|
this.temperature = 1;
|
|
1722
2034
|
this.thinkingLevel = "high";
|
|
2035
|
+
const rateLimit = getDefaultRateLimit("gemini", this.model);
|
|
2036
|
+
this.rateLimiter = new RateLimiter({ requestsPerMinute: rateLimit });
|
|
1723
2037
|
}
|
|
1724
2038
|
/**
|
|
1725
2039
|
* Check if API key is configured
|
|
@@ -1729,11 +2043,13 @@ var GeminiClient = class {
|
|
|
1729
2043
|
}
|
|
1730
2044
|
/**
|
|
1731
2045
|
* Make a request to Gemini API
|
|
2046
|
+
* Fixed: Added rate limiting and retry logic for 429 responses
|
|
1732
2047
|
*/
|
|
1733
|
-
async request(prompt, systemPrompt) {
|
|
2048
|
+
async request(prompt, systemPrompt, retryCount = 0) {
|
|
1734
2049
|
if (!this.apiKey) {
|
|
1735
2050
|
throw new Error("Gemini API key not configured. Set GEMINI_API_KEY environment variable.");
|
|
1736
2051
|
}
|
|
2052
|
+
await this.rateLimiter.waitForSlot();
|
|
1737
2053
|
const url = `${GEMINI_API_URL}/${this.model}:generateContent?key=${this.apiKey}`;
|
|
1738
2054
|
const contents = [];
|
|
1739
2055
|
if (systemPrompt) {
|
|
@@ -1767,6 +2083,16 @@ var GeminiClient = class {
|
|
|
1767
2083
|
}
|
|
1768
2084
|
})
|
|
1769
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
|
+
}
|
|
1770
2096
|
if (!response.ok) {
|
|
1771
2097
|
const error = await response.text();
|
|
1772
2098
|
throw new Error(`Gemini API error: ${response.status} - ${error}`);
|
|
@@ -1977,6 +2303,43 @@ var ContextBuilder = class {
|
|
|
1977
2303
|
this.graph = new DependencyGraph();
|
|
1978
2304
|
this.budget = new TokenBudget();
|
|
1979
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
|
+
}
|
|
1980
2343
|
/**
|
|
1981
2344
|
* Initialize the context builder for a project
|
|
1982
2345
|
*/
|
|
@@ -1988,8 +2351,13 @@ var ContextBuilder = class {
|
|
|
1988
2351
|
await this.vectorStore.initialize();
|
|
1989
2352
|
const graphPath = join4(this.config.rootDir, CONTEXTOS_DIR2, GRAPH_FILE);
|
|
1990
2353
|
if (existsSync4(graphPath)) {
|
|
1991
|
-
|
|
1992
|
-
|
|
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
|
+
}
|
|
1993
2361
|
}
|
|
1994
2362
|
this.ranker = new HybridRanker(
|
|
1995
2363
|
this.vectorStore,
|
|
@@ -2026,7 +2394,7 @@ var ContextBuilder = class {
|
|
|
2026
2394
|
const parser = await getParser();
|
|
2027
2395
|
for (const filePath of files) {
|
|
2028
2396
|
try {
|
|
2029
|
-
const content =
|
|
2397
|
+
const content = await readFile(filePath, "utf-8");
|
|
2030
2398
|
const relativePath = relative(rootDir, filePath);
|
|
2031
2399
|
if (!force && !this.graph.hasChanged(relativePath, content)) {
|
|
2032
2400
|
continue;
|
|
@@ -2049,11 +2417,9 @@ var ContextBuilder = class {
|
|
|
2049
2417
|
const graphPath = join4(rootDir, CONTEXTOS_DIR2, GRAPH_FILE);
|
|
2050
2418
|
const graphDir = join4(rootDir, CONTEXTOS_DIR2, "db");
|
|
2051
2419
|
if (!existsSync4(graphDir)) {
|
|
2052
|
-
|
|
2053
|
-
mkdirSync9(graphDir, { recursive: true });
|
|
2420
|
+
await mkdir(graphDir, { recursive: true });
|
|
2054
2421
|
}
|
|
2055
|
-
|
|
2056
|
-
writeFileSync9(graphPath, JSON.stringify(this.graph.toJSON(), null, 2));
|
|
2422
|
+
await writeFile(graphPath, JSON.stringify(this.graph.toJSON(), null, 2), "utf-8");
|
|
2057
2423
|
return {
|
|
2058
2424
|
filesIndexed,
|
|
2059
2425
|
chunksCreated,
|
|
@@ -2089,37 +2455,38 @@ var ContextBuilder = class {
|
|
|
2089
2455
|
}
|
|
2090
2456
|
/**
|
|
2091
2457
|
* Infer goal from git diff, enhanced with Gemini when available
|
|
2458
|
+
* Fixed: Uses sanitized git commands to prevent command injection
|
|
2092
2459
|
*/
|
|
2093
2460
|
async inferGoal() {
|
|
2094
2461
|
let gitDiff = "";
|
|
2095
2462
|
let recentFiles = [];
|
|
2096
2463
|
try {
|
|
2097
2464
|
const { execSync: execSync2 } = await import("child_process");
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
});
|
|
2102
|
-
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);
|
|
2103
2468
|
if (recentFiles.length === 0) {
|
|
2104
|
-
const uncommitted =
|
|
2105
|
-
|
|
2106
|
-
encoding: "utf-8"
|
|
2107
|
-
});
|
|
2108
|
-
recentFiles = uncommitted.trim().split("\n").filter(Boolean);
|
|
2469
|
+
const uncommitted = await this.getGitDiff("working");
|
|
2470
|
+
recentFiles = uncommitted.split("\n").filter(Boolean);
|
|
2109
2471
|
}
|
|
2110
2472
|
if (recentFiles.length > 0) {
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
maxBuffer: 1024 * 1024
|
|
2115
|
-
// 1MB max
|
|
2116
|
-
});
|
|
2117
|
-
if (!gitDiff) {
|
|
2118
|
-
gitDiff = execSync2("git diff", {
|
|
2119
|
-
cwd: this.config?.rootDir,
|
|
2473
|
+
try {
|
|
2474
|
+
gitDiff = execSync2("git diff --cached", {
|
|
2475
|
+
cwd,
|
|
2120
2476
|
encoding: "utf-8",
|
|
2121
|
-
maxBuffer: 1024 * 1024
|
|
2477
|
+
maxBuffer: 1024 * 1024,
|
|
2478
|
+
// 1MB max
|
|
2479
|
+
timeout: 5e3
|
|
2122
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 {
|
|
2123
2490
|
}
|
|
2124
2491
|
}
|
|
2125
2492
|
} catch {
|
|
@@ -2206,16 +2573,34 @@ var ContextBuilder = class {
|
|
|
2206
2573
|
}
|
|
2207
2574
|
};
|
|
2208
2575
|
var builderInstance = null;
|
|
2576
|
+
var initializationPromise2 = null;
|
|
2209
2577
|
async function getContextBuilder(projectDir) {
|
|
2210
|
-
if (
|
|
2211
|
-
builderInstance
|
|
2212
|
-
await builderInstance.initialize(projectDir);
|
|
2578
|
+
if (builderInstance) {
|
|
2579
|
+
return builderInstance;
|
|
2213
2580
|
}
|
|
2214
|
-
|
|
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;
|
|
2215
2600
|
}
|
|
2216
2601
|
|
|
2217
2602
|
// src/doctor/drift-detector.ts
|
|
2218
|
-
import {
|
|
2603
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2219
2604
|
import { glob as glob2 } from "glob";
|
|
2220
2605
|
var TECH_PATTERNS = {
|
|
2221
2606
|
// Databases
|
|
@@ -2394,33 +2779,40 @@ var DriftDetector = class {
|
|
|
2394
2779
|
}
|
|
2395
2780
|
/**
|
|
2396
2781
|
* Check a specific constraint
|
|
2782
|
+
* Fix N9: Convert to async file operations
|
|
2397
2783
|
*/
|
|
2398
2784
|
async checkConstraint(rule) {
|
|
2399
2785
|
const violations = [];
|
|
2400
2786
|
if (rule.toLowerCase().includes("no direct database access in controllers")) {
|
|
2401
2787
|
for (const file of this.sourceFiles) {
|
|
2402
2788
|
if (file.includes("controller")) {
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
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
|
+
}
|
|
2410
2798
|
}
|
|
2411
2799
|
}
|
|
2800
|
+
} catch {
|
|
2412
2801
|
}
|
|
2413
2802
|
}
|
|
2414
2803
|
}
|
|
2415
2804
|
}
|
|
2416
2805
|
if (rule.toLowerCase().includes("no console.log")) {
|
|
2417
2806
|
for (const file of this.sourceFiles) {
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
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
|
+
}
|
|
2423
2814
|
}
|
|
2815
|
+
} catch {
|
|
2424
2816
|
}
|
|
2425
2817
|
}
|
|
2426
2818
|
}
|
|
@@ -2444,12 +2836,13 @@ var DriftDetector = class {
|
|
|
2444
2836
|
}
|
|
2445
2837
|
/**
|
|
2446
2838
|
* Find patterns in files
|
|
2839
|
+
* Fix N9: Convert to async file operations
|
|
2447
2840
|
*/
|
|
2448
2841
|
async findPatternInFiles(patterns) {
|
|
2449
2842
|
const results = [];
|
|
2450
2843
|
for (const file of this.sourceFiles) {
|
|
2451
2844
|
try {
|
|
2452
|
-
const content =
|
|
2845
|
+
const content = await readFile2(file, "utf-8");
|
|
2453
2846
|
const lines = content.split("\n");
|
|
2454
2847
|
for (let i = 0; i < lines.length; i++) {
|
|
2455
2848
|
for (const pattern of patterns) {
|
|
@@ -2625,16 +3018,19 @@ var OpenAIAdapter = class {
|
|
|
2625
3018
|
apiKey;
|
|
2626
3019
|
model;
|
|
2627
3020
|
baseUrl;
|
|
3021
|
+
rateLimiter;
|
|
2628
3022
|
constructor(options = {}) {
|
|
2629
3023
|
this.apiKey = options.apiKey || process.env.OPENAI_API_KEY || "";
|
|
2630
3024
|
this.model = options.model || "gpt-5.2";
|
|
2631
3025
|
this.baseUrl = options.baseUrl || "https://api.openai.com/v1";
|
|
2632
3026
|
this.maxContextTokens = getModelContextSize("openai", this.model);
|
|
3027
|
+
const rateLimit = options.requestsPerMinute || getDefaultRateLimit("openai", this.model);
|
|
3028
|
+
this.rateLimiter = new RateLimiter({ requestsPerMinute: rateLimit });
|
|
2633
3029
|
if (!this.apiKey) {
|
|
2634
3030
|
console.warn("OpenAI API key not set. Set OPENAI_API_KEY environment variable.");
|
|
2635
3031
|
}
|
|
2636
3032
|
}
|
|
2637
|
-
async complete(request) {
|
|
3033
|
+
async complete(request, retryCount = 0) {
|
|
2638
3034
|
if (!this.apiKey) {
|
|
2639
3035
|
return {
|
|
2640
3036
|
content: "",
|
|
@@ -2643,6 +3039,7 @@ var OpenAIAdapter = class {
|
|
|
2643
3039
|
error: "OpenAI API key not configured"
|
|
2644
3040
|
};
|
|
2645
3041
|
}
|
|
3042
|
+
await this.rateLimiter.waitForSlot();
|
|
2646
3043
|
try {
|
|
2647
3044
|
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
2648
3045
|
method: "POST",
|
|
@@ -2661,11 +3058,29 @@ var OpenAIAdapter = class {
|
|
|
2661
3058
|
stop: request.stopSequences
|
|
2662
3059
|
})
|
|
2663
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
|
+
}
|
|
2664
3076
|
if (!response.ok) {
|
|
2665
3077
|
const errorData = await response.json().catch(() => ({}));
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
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
|
+
};
|
|
2669
3084
|
}
|
|
2670
3085
|
const data = await response.json();
|
|
2671
3086
|
const choice = data.choices?.[0];
|
|
@@ -2705,13 +3120,16 @@ var AnthropicAdapter = class {
|
|
|
2705
3120
|
apiKey;
|
|
2706
3121
|
model;
|
|
2707
3122
|
baseUrl;
|
|
3123
|
+
rateLimiter;
|
|
2708
3124
|
constructor(options = {}) {
|
|
2709
3125
|
this.apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY || "";
|
|
2710
3126
|
this.model = options.model || "claude-4.5-opus-20260115";
|
|
2711
3127
|
this.baseUrl = options.baseUrl || "https://api.anthropic.com";
|
|
2712
3128
|
this.maxContextTokens = getModelContextSize("anthropic", this.model);
|
|
3129
|
+
const rateLimit = options.requestsPerMinute || getDefaultRateLimit("anthropic", this.model);
|
|
3130
|
+
this.rateLimiter = new RateLimiter({ requestsPerMinute: rateLimit });
|
|
2713
3131
|
}
|
|
2714
|
-
async complete(request) {
|
|
3132
|
+
async complete(request, retryCount = 0) {
|
|
2715
3133
|
if (!this.apiKey) {
|
|
2716
3134
|
return {
|
|
2717
3135
|
content: "",
|
|
@@ -2720,6 +3138,7 @@ var AnthropicAdapter = class {
|
|
|
2720
3138
|
error: "Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable."
|
|
2721
3139
|
};
|
|
2722
3140
|
}
|
|
3141
|
+
await this.rateLimiter.waitForSlot();
|
|
2723
3142
|
try {
|
|
2724
3143
|
const response = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
2725
3144
|
method: "POST",
|
|
@@ -2739,11 +3158,29 @@ var AnthropicAdapter = class {
|
|
|
2739
3158
|
stop_sequences: request.stopSequences
|
|
2740
3159
|
})
|
|
2741
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
|
+
}
|
|
2742
3176
|
if (!response.ok) {
|
|
2743
3177
|
const errorData = await response.json().catch(() => ({}));
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
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
|
+
};
|
|
2747
3184
|
}
|
|
2748
3185
|
const data = await response.json();
|
|
2749
3186
|
const content = data.content?.filter((c) => c.type === "text")?.map((c) => c.text || "")?.join("") || "";
|
|
@@ -3026,6 +3463,13 @@ function createContextAPI(rawContext) {
|
|
|
3026
3463
|
}).filter(Boolean);
|
|
3027
3464
|
},
|
|
3028
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
|
+
}
|
|
3029
3473
|
const escapedPath = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3030
3474
|
const pattern = new RegExp(
|
|
3031
3475
|
`^={3,}\\s*FILE:\\s*${escapedPath}\\s*={3,}$([\\s\\S]*?)(?=^={3,}\\s*FILE:|$)`,
|
|
@@ -3464,7 +3908,8 @@ var RLMEngine = class {
|
|
|
3464
3908
|
const state = {
|
|
3465
3909
|
depth,
|
|
3466
3910
|
consumedTokens: 0,
|
|
3467
|
-
visitedPaths: /* @__PURE__ */ new
|
|
3911
|
+
visitedPaths: /* @__PURE__ */ new Map(),
|
|
3912
|
+
// Changed to Map with timestamps for memory leak fix
|
|
3468
3913
|
executionLog: [],
|
|
3469
3914
|
iteration: 0,
|
|
3470
3915
|
startTime
|
|
@@ -3574,7 +4019,9 @@ Depth: ${depth}/${this.config.maxDepth}`
|
|
|
3574
4019
|
entry.output = result.success ? result.output || result.stdout : result.error || "Unknown error";
|
|
3575
4020
|
state.executionLog.push(entry);
|
|
3576
4021
|
const pathKey = action.code.slice(0, 100);
|
|
3577
|
-
|
|
4022
|
+
const now = Date.now();
|
|
4023
|
+
const lastSeen = state.visitedPaths.get(pathKey);
|
|
4024
|
+
if (lastSeen !== void 0) {
|
|
3578
4025
|
messages.push({ role: "assistant", content: response.content });
|
|
3579
4026
|
messages.push({
|
|
3580
4027
|
role: "user",
|
|
@@ -3582,7 +4029,14 @@ Depth: ${depth}/${this.config.maxDepth}`
|
|
|
3582
4029
|
});
|
|
3583
4030
|
continue;
|
|
3584
4031
|
}
|
|
3585
|
-
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
|
+
}
|
|
3586
4040
|
messages.push({ role: "assistant", content: response.content });
|
|
3587
4041
|
messages.push({
|
|
3588
4042
|
role: "user",
|
|
@@ -3770,7 +4224,7 @@ function createRLMEngine(options = {}) {
|
|
|
3770
4224
|
}
|
|
3771
4225
|
|
|
3772
4226
|
// src/rlm/proposal.ts
|
|
3773
|
-
import { createHash as createHash3 } from "crypto";
|
|
4227
|
+
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
3774
4228
|
var ProposalManager = class {
|
|
3775
4229
|
proposals = /* @__PURE__ */ new Map();
|
|
3776
4230
|
fileSnapshots = /* @__PURE__ */ new Map();
|
|
@@ -3946,7 +4400,10 @@ Rejection reason: ${reason}`;
|
|
|
3946
4400
|
}
|
|
3947
4401
|
// Private helpers
|
|
3948
4402
|
generateId() {
|
|
3949
|
-
|
|
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)}`;
|
|
3950
4407
|
}
|
|
3951
4408
|
hashContent(content) {
|
|
3952
4409
|
return createHash3("sha256").update(content).digest("hex").substring(0, 16);
|
|
@@ -4584,9 +5041,21 @@ function createWatchdog(config) {
|
|
|
4584
5041
|
|
|
4585
5042
|
// src/sync/team-sync.ts
|
|
4586
5043
|
import { execSync } from "child_process";
|
|
4587
|
-
import { readFileSync as
|
|
5044
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
4588
5045
|
import { join as join5 } from "path";
|
|
4589
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
|
+
}
|
|
4590
5059
|
var DEFAULT_SYNC_CONFIG = {
|
|
4591
5060
|
enabled: false,
|
|
4592
5061
|
remote: "origin",
|
|
@@ -4602,11 +5071,17 @@ var TeamSync = class {
|
|
|
4602
5071
|
this.rootDir = rootDir;
|
|
4603
5072
|
this.contextosDir = join5(rootDir, ".contextos");
|
|
4604
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
|
+
}
|
|
4605
5080
|
}
|
|
4606
5081
|
loadSyncConfig() {
|
|
4607
5082
|
const configPath = join5(this.contextosDir, "sync.yaml");
|
|
4608
|
-
if (
|
|
4609
|
-
const content =
|
|
5083
|
+
if (existsSync5(configPath)) {
|
|
5084
|
+
const content = readFileSync5(configPath, "utf-8");
|
|
4610
5085
|
return { ...DEFAULT_SYNC_CONFIG, ...parse(content) };
|
|
4611
5086
|
}
|
|
4612
5087
|
return DEFAULT_SYNC_CONFIG;
|
|
@@ -4620,7 +5095,8 @@ var TeamSync = class {
|
|
|
4620
5095
|
*/
|
|
4621
5096
|
async initialize(remote = "origin") {
|
|
4622
5097
|
this.syncConfig.enabled = true;
|
|
4623
|
-
this.syncConfig.remote = remote;
|
|
5098
|
+
this.syncConfig.remote = validateGitRemoteName(remote);
|
|
5099
|
+
this.syncConfig.branch = validateGitBranchName(this.syncConfig.branch);
|
|
4624
5100
|
this.saveSyncConfig();
|
|
4625
5101
|
try {
|
|
4626
5102
|
execSync(`git checkout -b ${this.syncConfig.branch}`, {
|
|
@@ -4726,7 +5202,7 @@ var TemplateManager = class {
|
|
|
4726
5202
|
templatesDir;
|
|
4727
5203
|
constructor(rootDir) {
|
|
4728
5204
|
this.templatesDir = join5(rootDir, ".contextos", "templates");
|
|
4729
|
-
if (!
|
|
5205
|
+
if (!existsSync5(this.templatesDir)) {
|
|
4730
5206
|
mkdirSync3(this.templatesDir, { recursive: true });
|
|
4731
5207
|
}
|
|
4732
5208
|
}
|
|
@@ -4739,7 +5215,7 @@ var TemplateManager = class {
|
|
|
4739
5215
|
const files = __require("fs").readdirSync(this.templatesDir);
|
|
4740
5216
|
for (const file of files) {
|
|
4741
5217
|
if (file.endsWith(".yaml")) {
|
|
4742
|
-
const content =
|
|
5218
|
+
const content = readFileSync5(join5(this.templatesDir, file), "utf-8");
|
|
4743
5219
|
templates.push(parse(content));
|
|
4744
5220
|
}
|
|
4745
5221
|
}
|
|
@@ -4758,8 +5234,8 @@ var TemplateManager = class {
|
|
|
4758
5234
|
description,
|
|
4759
5235
|
author,
|
|
4760
5236
|
version: "1.0.0",
|
|
4761
|
-
context:
|
|
4762
|
-
config:
|
|
5237
|
+
context: existsSync5(contextPath) ? parse(readFileSync5(contextPath, "utf-8")) : {},
|
|
5238
|
+
config: existsSync5(configPath) ? parse(readFileSync5(configPath, "utf-8")) : {}
|
|
4763
5239
|
};
|
|
4764
5240
|
const templatePath = join5(this.templatesDir, `${name}.yaml`);
|
|
4765
5241
|
writeFileSync2(templatePath, stringify2(template, { indent: 2 }), "utf-8");
|
|
@@ -4769,10 +5245,10 @@ var TemplateManager = class {
|
|
|
4769
5245
|
*/
|
|
4770
5246
|
apply(name) {
|
|
4771
5247
|
const templatePath = join5(this.templatesDir, `${name}.yaml`);
|
|
4772
|
-
if (!
|
|
5248
|
+
if (!existsSync5(templatePath)) {
|
|
4773
5249
|
throw new Error(`Template '${name}' not found`);
|
|
4774
5250
|
}
|
|
4775
|
-
const template = parse(
|
|
5251
|
+
const template = parse(readFileSync5(templatePath, "utf-8"));
|
|
4776
5252
|
const contextPath = join5(this.templatesDir, "..", "context.yaml");
|
|
4777
5253
|
const configPath = join5(this.templatesDir, "..", "config.yaml");
|
|
4778
5254
|
if (template.context && Object.keys(template.context).length > 0) {
|
|
@@ -4786,8 +5262,10 @@ var TemplateManager = class {
|
|
|
4786
5262
|
|
|
4787
5263
|
// src/sync/cloud-sync.ts
|
|
4788
5264
|
import crypto from "crypto";
|
|
4789
|
-
import {
|
|
5265
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5266
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
4790
5267
|
import { join as join6 } from "path";
|
|
5268
|
+
var { AbortController } = globalThis;
|
|
4791
5269
|
var E2EEncryption = class {
|
|
4792
5270
|
algorithm = "aes-256-gcm";
|
|
4793
5271
|
keyLength = 32;
|
|
@@ -4863,7 +5341,7 @@ var CloudSync = class {
|
|
|
4863
5341
|
this.config.encryptionKey = key;
|
|
4864
5342
|
this.config.enabled = true;
|
|
4865
5343
|
const saltPath = join6(this.rootDir, ".contextos", ".salt");
|
|
4866
|
-
|
|
5344
|
+
await writeFile2(saltPath, salt, "utf-8");
|
|
4867
5345
|
return key;
|
|
4868
5346
|
}
|
|
4869
5347
|
/**
|
|
@@ -4876,9 +5354,11 @@ var CloudSync = class {
|
|
|
4876
5354
|
try {
|
|
4877
5355
|
const contextPath = join6(this.rootDir, ".contextos", "context.yaml");
|
|
4878
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") : "";
|
|
4879
5359
|
const data = JSON.stringify({
|
|
4880
|
-
context:
|
|
4881
|
-
config:
|
|
5360
|
+
context: contextContent,
|
|
5361
|
+
config: configContent,
|
|
4882
5362
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4883
5363
|
});
|
|
4884
5364
|
const encryptedData = this.encryption.encrypt(data, this.config.encryptionKey);
|
|
@@ -4890,20 +5370,32 @@ var CloudSync = class {
|
|
|
4890
5370
|
data: encryptedData,
|
|
4891
5371
|
checksum
|
|
4892
5372
|
};
|
|
4893
|
-
const
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
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;
|
|
4900
5398
|
}
|
|
4901
|
-
return {
|
|
4902
|
-
success: true,
|
|
4903
|
-
action: "upload",
|
|
4904
|
-
message: "Context uploaded and encrypted",
|
|
4905
|
-
timestamp: payload.timestamp
|
|
4906
|
-
};
|
|
4907
5399
|
} catch (error) {
|
|
4908
5400
|
return {
|
|
4909
5401
|
success: false,
|
|
@@ -4920,30 +5412,41 @@ var CloudSync = class {
|
|
|
4920
5412
|
return { success: false, action: "download", message: "Cloud sync not initialized" };
|
|
4921
5413
|
}
|
|
4922
5414
|
try {
|
|
4923
|
-
const
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
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;
|
|
4936
5449
|
}
|
|
4937
|
-
const contextPath = join6(this.rootDir, ".contextos", "context.yaml");
|
|
4938
|
-
const configPath = join6(this.rootDir, ".contextos", "config.yaml");
|
|
4939
|
-
if (data.context) writeFileSync3(contextPath, data.context, "utf-8");
|
|
4940
|
-
if (data.config) writeFileSync3(configPath, data.config, "utf-8");
|
|
4941
|
-
return {
|
|
4942
|
-
success: true,
|
|
4943
|
-
action: "download",
|
|
4944
|
-
message: "Context downloaded and decrypted",
|
|
4945
|
-
timestamp: payload.timestamp
|
|
4946
|
-
};
|
|
4947
5450
|
} catch (error) {
|
|
4948
5451
|
return {
|
|
4949
5452
|
success: false,
|
|
@@ -4953,19 +5456,19 @@ var CloudSync = class {
|
|
|
4953
5456
|
}
|
|
4954
5457
|
}
|
|
4955
5458
|
/**
|
|
4956
|
-
* Check if encryption key is valid
|
|
5459
|
+
* Check if encryption key is valid - Fix R9: Use async readFile
|
|
4957
5460
|
*/
|
|
4958
|
-
validateKey(password) {
|
|
5461
|
+
async validateKey(password) {
|
|
4959
5462
|
const saltPath = join6(this.rootDir, ".contextos", ".salt");
|
|
4960
|
-
if (!
|
|
4961
|
-
const salt =
|
|
5463
|
+
if (!existsSync6(saltPath)) return false;
|
|
5464
|
+
const salt = await readFile3(saltPath, "utf-8");
|
|
4962
5465
|
const derivedKey = this.encryption.deriveKey(password, salt);
|
|
4963
5466
|
return derivedKey === this.config.encryptionKey;
|
|
4964
5467
|
}
|
|
4965
5468
|
};
|
|
4966
5469
|
|
|
4967
5470
|
// src/analytics/analytics.ts
|
|
4968
|
-
import { existsSync as
|
|
5471
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
4969
5472
|
import { join as join7 } from "path";
|
|
4970
5473
|
var AnalyticsCollector = class {
|
|
4971
5474
|
analyticsDir;
|
|
@@ -4975,7 +5478,7 @@ var AnalyticsCollector = class {
|
|
|
4975
5478
|
this.analyticsDir = join7(rootDir, ".contextos", "analytics");
|
|
4976
5479
|
this.eventsFile = join7(this.analyticsDir, "events.json");
|
|
4977
5480
|
this.statsFile = join7(this.analyticsDir, "stats.json");
|
|
4978
|
-
if (!
|
|
5481
|
+
if (!existsSync7(this.analyticsDir)) {
|
|
4979
5482
|
mkdirSync4(this.analyticsDir, { recursive: true });
|
|
4980
5483
|
}
|
|
4981
5484
|
}
|
|
@@ -5014,9 +5517,9 @@ var AnalyticsCollector = class {
|
|
|
5014
5517
|
});
|
|
5015
5518
|
}
|
|
5016
5519
|
loadEvents() {
|
|
5017
|
-
if (
|
|
5520
|
+
if (existsSync7(this.eventsFile)) {
|
|
5018
5521
|
try {
|
|
5019
|
-
return JSON.parse(
|
|
5522
|
+
return JSON.parse(readFileSync7(this.eventsFile, "utf-8"));
|
|
5020
5523
|
} catch {
|
|
5021
5524
|
return [];
|
|
5022
5525
|
}
|
|
@@ -5042,9 +5545,9 @@ var AnalyticsCollector = class {
|
|
|
5042
5545
|
writeFileSync4(this.statsFile, JSON.stringify(updatedStats, null, 2), "utf-8");
|
|
5043
5546
|
}
|
|
5044
5547
|
loadDailyStats() {
|
|
5045
|
-
if (
|
|
5548
|
+
if (existsSync7(this.statsFile)) {
|
|
5046
5549
|
try {
|
|
5047
|
-
return JSON.parse(
|
|
5550
|
+
return JSON.parse(readFileSync7(this.statsFile, "utf-8"));
|
|
5048
5551
|
} catch {
|
|
5049
5552
|
return [];
|
|
5050
5553
|
}
|
|
@@ -5110,7 +5613,7 @@ var AnalyticsCollector = class {
|
|
|
5110
5613
|
};
|
|
5111
5614
|
|
|
5112
5615
|
// src/compliance/rbac.ts
|
|
5113
|
-
import { readFileSync as
|
|
5616
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
5114
5617
|
import { join as join8 } from "path";
|
|
5115
5618
|
import crypto2 from "crypto";
|
|
5116
5619
|
var BUILT_IN_ROLES = [
|
|
@@ -5175,7 +5678,7 @@ var RBACManager = class {
|
|
|
5175
5678
|
this.rolesFile = join8(this.rbacDir, "roles.json");
|
|
5176
5679
|
this.usersFile = join8(this.rbacDir, "users.json");
|
|
5177
5680
|
this.policiesFile = join8(this.rbacDir, "policies.json");
|
|
5178
|
-
if (!
|
|
5681
|
+
if (!existsSync8(this.rbacDir)) {
|
|
5179
5682
|
mkdirSync5(this.rbacDir, { recursive: true });
|
|
5180
5683
|
}
|
|
5181
5684
|
this.loadData();
|
|
@@ -5184,27 +5687,27 @@ var RBACManager = class {
|
|
|
5184
5687
|
for (const role of BUILT_IN_ROLES) {
|
|
5185
5688
|
this.roles.set(role.id, role);
|
|
5186
5689
|
}
|
|
5187
|
-
if (
|
|
5690
|
+
if (existsSync8(this.rolesFile)) {
|
|
5188
5691
|
try {
|
|
5189
|
-
const customRoles = JSON.parse(
|
|
5692
|
+
const customRoles = JSON.parse(readFileSync8(this.rolesFile, "utf-8"));
|
|
5190
5693
|
for (const role of customRoles) {
|
|
5191
5694
|
this.roles.set(role.id, role);
|
|
5192
5695
|
}
|
|
5193
5696
|
} catch {
|
|
5194
5697
|
}
|
|
5195
5698
|
}
|
|
5196
|
-
if (
|
|
5699
|
+
if (existsSync8(this.usersFile)) {
|
|
5197
5700
|
try {
|
|
5198
|
-
const users = JSON.parse(
|
|
5701
|
+
const users = JSON.parse(readFileSync8(this.usersFile, "utf-8"));
|
|
5199
5702
|
for (const user of users) {
|
|
5200
5703
|
this.users.set(user.id, user);
|
|
5201
5704
|
}
|
|
5202
5705
|
} catch {
|
|
5203
5706
|
}
|
|
5204
5707
|
}
|
|
5205
|
-
if (
|
|
5708
|
+
if (existsSync8(this.policiesFile)) {
|
|
5206
5709
|
try {
|
|
5207
|
-
this.policies = JSON.parse(
|
|
5710
|
+
this.policies = JSON.parse(readFileSync8(this.policiesFile, "utf-8"));
|
|
5208
5711
|
} catch {
|
|
5209
5712
|
}
|
|
5210
5713
|
}
|
|
@@ -5365,7 +5868,7 @@ var RBACManager = class {
|
|
|
5365
5868
|
};
|
|
5366
5869
|
|
|
5367
5870
|
// src/compliance/audit.ts
|
|
5368
|
-
import { existsSync as
|
|
5871
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6, appendFileSync, mkdirSync as mkdirSync6 } from "fs";
|
|
5369
5872
|
import { join as join9 } from "path";
|
|
5370
5873
|
import crypto3 from "crypto";
|
|
5371
5874
|
var AuditLogger = class {
|
|
@@ -5375,7 +5878,7 @@ var AuditLogger = class {
|
|
|
5375
5878
|
constructor(rootDir) {
|
|
5376
5879
|
this.auditDir = join9(rootDir, ".contextos", "audit");
|
|
5377
5880
|
this.indexFile = join9(this.auditDir, "index.json");
|
|
5378
|
-
if (!
|
|
5881
|
+
if (!existsSync9(this.auditDir)) {
|
|
5379
5882
|
mkdirSync6(this.auditDir, { recursive: true });
|
|
5380
5883
|
}
|
|
5381
5884
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -5425,9 +5928,9 @@ var AuditLogger = class {
|
|
|
5425
5928
|
writeFileSync6(this.indexFile, JSON.stringify(index, null, 2), "utf-8");
|
|
5426
5929
|
}
|
|
5427
5930
|
loadIndex() {
|
|
5428
|
-
if (
|
|
5931
|
+
if (existsSync9(this.indexFile)) {
|
|
5429
5932
|
try {
|
|
5430
|
-
return JSON.parse(
|
|
5933
|
+
return JSON.parse(readFileSync9(this.indexFile, "utf-8"));
|
|
5431
5934
|
} catch {
|
|
5432
5935
|
return {};
|
|
5433
5936
|
}
|
|
@@ -5444,7 +5947,7 @@ var AuditLogger = class {
|
|
|
5444
5947
|
for (const logFile of logFiles) {
|
|
5445
5948
|
if (entries.length >= limit) break;
|
|
5446
5949
|
const filePath = join9(this.auditDir, logFile);
|
|
5447
|
-
const content =
|
|
5950
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
5448
5951
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
5449
5952
|
for (const line of lines.reverse()) {
|
|
5450
5953
|
if (entries.length >= limit) break;
|
|
@@ -5481,7 +5984,7 @@ var AuditLogger = class {
|
|
|
5481
5984
|
*/
|
|
5482
5985
|
verifyLogFile(filename) {
|
|
5483
5986
|
const filePath = join9(this.auditDir, filename);
|
|
5484
|
-
const content =
|
|
5987
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
5485
5988
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
5486
5989
|
const result = { valid: 0, invalid: 0, entries: [] };
|
|
5487
5990
|
for (const line of lines) {
|
|
@@ -5535,8 +6038,8 @@ var AuditLogger = class {
|
|
|
5535
6038
|
};
|
|
5536
6039
|
|
|
5537
6040
|
// src/plugins/manager.ts
|
|
5538
|
-
import { existsSync as
|
|
5539
|
-
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";
|
|
5540
6043
|
var PluginManager = class {
|
|
5541
6044
|
plugins = /* @__PURE__ */ new Map();
|
|
5542
6045
|
pluginsDir;
|
|
@@ -5545,7 +6048,7 @@ var PluginManager = class {
|
|
|
5545
6048
|
constructor(projectRoot) {
|
|
5546
6049
|
this.projectRoot = projectRoot;
|
|
5547
6050
|
this.pluginsDir = join10(projectRoot, ".contextos", "plugins");
|
|
5548
|
-
if (!
|
|
6051
|
+
if (!existsSync10(this.pluginsDir)) {
|
|
5549
6052
|
mkdirSync7(this.pluginsDir, { recursive: true });
|
|
5550
6053
|
}
|
|
5551
6054
|
}
|
|
@@ -5555,7 +6058,7 @@ var PluginManager = class {
|
|
|
5555
6058
|
async loadAll() {
|
|
5556
6059
|
const loaded = [];
|
|
5557
6060
|
const errors = [];
|
|
5558
|
-
if (!
|
|
6061
|
+
if (!existsSync10(this.pluginsDir)) {
|
|
5559
6062
|
return { loaded, errors };
|
|
5560
6063
|
}
|
|
5561
6064
|
const entries = readdirSync(this.pluginsDir, { withFileTypes: true });
|
|
@@ -5578,17 +6081,32 @@ var PluginManager = class {
|
|
|
5578
6081
|
* Load a single plugin from path
|
|
5579
6082
|
*/
|
|
5580
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
|
+
}
|
|
5581
6091
|
const manifestPath = join10(pluginPath, "package.json");
|
|
5582
|
-
if (!
|
|
6092
|
+
if (!existsSync10(manifestPath)) {
|
|
5583
6093
|
throw new Error(`Plugin manifest not found: ${manifestPath}`);
|
|
5584
6094
|
}
|
|
5585
|
-
const manifestContent =
|
|
5586
|
-
|
|
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
|
+
}
|
|
5587
6105
|
if (!manifest.name || !manifest.version || !manifest.main) {
|
|
5588
6106
|
throw new Error(`Invalid plugin manifest: missing name, version, or main`);
|
|
5589
6107
|
}
|
|
5590
6108
|
const mainPath = join10(pluginPath, manifest.main);
|
|
5591
|
-
if (!
|
|
6109
|
+
if (!existsSync10(mainPath)) {
|
|
5592
6110
|
throw new Error(`Plugin main file not found: ${mainPath}`);
|
|
5593
6111
|
}
|
|
5594
6112
|
const pluginModule = await import(`file://${resolve(mainPath)}`);
|
|
@@ -5655,21 +6173,29 @@ var PluginManager = class {
|
|
|
5655
6173
|
*/
|
|
5656
6174
|
async install(source, options = {}) {
|
|
5657
6175
|
const targetDir = join10(this.pluginsDir, source.split("/").pop() || source);
|
|
5658
|
-
if (
|
|
6176
|
+
if (existsSync10(targetDir) && !options.force) {
|
|
5659
6177
|
throw new Error(`Plugin already installed: ${source}`);
|
|
5660
6178
|
}
|
|
5661
6179
|
if (options.local) {
|
|
5662
6180
|
const sourcePath = resolve(source);
|
|
5663
|
-
if (!
|
|
6181
|
+
if (!existsSync10(sourcePath)) {
|
|
5664
6182
|
throw new Error(`Source path not found: ${sourcePath}`);
|
|
5665
6183
|
}
|
|
5666
|
-
if (options.force &&
|
|
6184
|
+
if (options.force && existsSync10(targetDir)) {
|
|
5667
6185
|
rmSync(targetDir, { recursive: true });
|
|
5668
6186
|
}
|
|
5669
6187
|
mkdirSync7(targetDir, { recursive: true });
|
|
5670
|
-
|
|
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
|
+
}
|
|
5671
6197
|
writeFileSync7(join10(targetDir, "package.json"), JSON.stringify(manifest, null, 2));
|
|
5672
|
-
const mainContent =
|
|
6198
|
+
const mainContent = readFileSync10(join10(sourcePath, manifest.main), "utf-8");
|
|
5673
6199
|
writeFileSync7(join10(targetDir, manifest.main), mainContent);
|
|
5674
6200
|
} else {
|
|
5675
6201
|
throw new Error("Remote plugin installation not yet implemented");
|
|
@@ -5687,7 +6213,7 @@ var PluginManager = class {
|
|
|
5687
6213
|
const state = this.plugins.get(name);
|
|
5688
6214
|
if (!state) return false;
|
|
5689
6215
|
await this.unloadPlugin(name);
|
|
5690
|
-
if (
|
|
6216
|
+
if (existsSync10(state.path)) {
|
|
5691
6217
|
rmSync(state.path, { recursive: true });
|
|
5692
6218
|
}
|
|
5693
6219
|
return true;
|
|
@@ -5741,7 +6267,7 @@ var PluginManager = class {
|
|
|
5741
6267
|
*/
|
|
5742
6268
|
createPluginScaffold(template) {
|
|
5743
6269
|
const pluginDir = join10(this.pluginsDir, template.name);
|
|
5744
|
-
if (
|
|
6270
|
+
if (existsSync10(pluginDir)) {
|
|
5745
6271
|
throw new Error(`Plugin directory already exists: ${template.name}`);
|
|
5746
6272
|
}
|
|
5747
6273
|
mkdirSync7(pluginDir, { recursive: true });
|
|
@@ -5807,6 +6333,9 @@ ${commandsCode}
|
|
|
5807
6333
|
this.storage.set(pluginName, /* @__PURE__ */ new Map());
|
|
5808
6334
|
}
|
|
5809
6335
|
const pluginStorage = this.storage.get(pluginName);
|
|
6336
|
+
if (!pluginStorage) {
|
|
6337
|
+
throw new Error(`Failed to get plugin storage for ${pluginName}`);
|
|
6338
|
+
}
|
|
5810
6339
|
return {
|
|
5811
6340
|
projectRoot: this.projectRoot,
|
|
5812
6341
|
configDir: join10(this.projectRoot, ".contextos"),
|
|
@@ -5820,8 +6349,13 @@ ${commandsCode}
|
|
|
5820
6349
|
return { files: [], context: "" };
|
|
5821
6350
|
},
|
|
5822
6351
|
readFile: async (path) => {
|
|
5823
|
-
const fullPath =
|
|
5824
|
-
|
|
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");
|
|
5825
6359
|
},
|
|
5826
6360
|
getDependencies: async (_path, _depth = 2) => {
|
|
5827
6361
|
return [];
|
|
@@ -5841,7 +6375,7 @@ function createPluginManager(projectRoot) {
|
|
|
5841
6375
|
}
|
|
5842
6376
|
|
|
5843
6377
|
// src/plugins/registry.ts
|
|
5844
|
-
import { existsSync as
|
|
6378
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
|
|
5845
6379
|
import { join as join11 } from "path";
|
|
5846
6380
|
var PluginRegistry = class {
|
|
5847
6381
|
config;
|
|
@@ -5854,7 +6388,7 @@ var PluginRegistry = class {
|
|
|
5854
6388
|
*/
|
|
5855
6389
|
listLocal() {
|
|
5856
6390
|
const plugins = [];
|
|
5857
|
-
if (!
|
|
6391
|
+
if (!existsSync11(this.config.localDir)) {
|
|
5858
6392
|
return plugins;
|
|
5859
6393
|
}
|
|
5860
6394
|
const entries = readdirSync2(this.config.localDir, { withFileTypes: true });
|
|
@@ -5862,13 +6396,13 @@ var PluginRegistry = class {
|
|
|
5862
6396
|
if (!entry.isDirectory()) continue;
|
|
5863
6397
|
const pluginPath = join11(this.config.localDir, entry.name);
|
|
5864
6398
|
const manifestPath = join11(pluginPath, "package.json");
|
|
5865
|
-
if (!
|
|
6399
|
+
if (!existsSync11(manifestPath)) continue;
|
|
5866
6400
|
try {
|
|
5867
6401
|
const manifest = JSON.parse(
|
|
5868
|
-
|
|
6402
|
+
readFileSync11(manifestPath, "utf-8")
|
|
5869
6403
|
);
|
|
5870
6404
|
const disabledPath = join11(pluginPath, ".disabled");
|
|
5871
|
-
const enabled = !
|
|
6405
|
+
const enabled = !existsSync11(disabledPath);
|
|
5872
6406
|
plugins.push({
|
|
5873
6407
|
name: manifest.name,
|
|
5874
6408
|
version: manifest.version,
|
|
@@ -6006,7 +6540,7 @@ function createPluginRegistry(projectRoot) {
|
|
|
6006
6540
|
}
|
|
6007
6541
|
|
|
6008
6542
|
// src/finetuning/collector.ts
|
|
6009
|
-
import { existsSync as
|
|
6543
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "fs";
|
|
6010
6544
|
import { join as join12 } from "path";
|
|
6011
6545
|
import { createHash as createHash4 } from "crypto";
|
|
6012
6546
|
var TrainingDataCollector = class {
|
|
@@ -6015,7 +6549,7 @@ var TrainingDataCollector = class {
|
|
|
6015
6549
|
loaded = false;
|
|
6016
6550
|
constructor(projectRoot) {
|
|
6017
6551
|
this.dataDir = join12(projectRoot, ".contextos", "training");
|
|
6018
|
-
if (!
|
|
6552
|
+
if (!existsSync12(this.dataDir)) {
|
|
6019
6553
|
mkdirSync8(this.dataDir, { recursive: true });
|
|
6020
6554
|
}
|
|
6021
6555
|
}
|
|
@@ -6025,9 +6559,9 @@ var TrainingDataCollector = class {
|
|
|
6025
6559
|
load() {
|
|
6026
6560
|
if (this.loaded) return;
|
|
6027
6561
|
const dataFile = join12(this.dataDir, "examples.json");
|
|
6028
|
-
if (
|
|
6562
|
+
if (existsSync12(dataFile)) {
|
|
6029
6563
|
try {
|
|
6030
|
-
const data = JSON.parse(
|
|
6564
|
+
const data = JSON.parse(readFileSync12(dataFile, "utf-8"));
|
|
6031
6565
|
this.examples = data.examples || [];
|
|
6032
6566
|
} catch {
|
|
6033
6567
|
this.examples = [];
|
|
@@ -6183,7 +6717,7 @@ function createTrainingDataCollector(projectRoot) {
|
|
|
6183
6717
|
}
|
|
6184
6718
|
|
|
6185
6719
|
// src/finetuning/formatter.ts
|
|
6186
|
-
import { createReadStream, createWriteStream, existsSync as
|
|
6720
|
+
import { createReadStream, createWriteStream, existsSync as existsSync13 } from "fs";
|
|
6187
6721
|
import { createInterface } from "readline";
|
|
6188
6722
|
var DatasetFormatter = class {
|
|
6189
6723
|
/**
|
|
@@ -6241,7 +6775,7 @@ var DatasetFormatter = class {
|
|
|
6241
6775
|
dateRange: { earliest: /* @__PURE__ */ new Date(), latest: /* @__PURE__ */ new Date(0) }
|
|
6242
6776
|
}
|
|
6243
6777
|
};
|
|
6244
|
-
if (!
|
|
6778
|
+
if (!existsSync13(filePath)) {
|
|
6245
6779
|
result.valid = false;
|
|
6246
6780
|
result.errors.push({
|
|
6247
6781
|
line: 0,
|
|
@@ -7148,7 +7682,9 @@ export {
|
|
|
7148
7682
|
mergeSmallChunks,
|
|
7149
7683
|
parseWithRegex,
|
|
7150
7684
|
prepareSandboxVariables,
|
|
7685
|
+
resetContextBuilder,
|
|
7151
7686
|
resetGlobalBlackboard,
|
|
7687
|
+
resetParser,
|
|
7152
7688
|
saveConfigYaml,
|
|
7153
7689
|
saveContextYaml,
|
|
7154
7690
|
setGlobalLogger,
|