@contextos/core 0.2.1 → 0.2.2

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