@contextos/core 0.2.1 → 0.2.3

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