@contextos/core 0.2.0 → 0.2.2

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