cto-ai-cli 5.0.0 → 5.2.0

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.
@@ -592,8 +592,8 @@ async function analyzeProject(projectPath, config) {
592
592
  maxDepth: mergedConfig.analysis.maxDepth
593
593
  });
594
594
  const tokenMethod = mergedConfig.tokens.method;
595
- const files = [];
596
- for (const entry of walkEntries) {
595
+ const BATCH_SIZE = 50;
596
+ async function estimateFileTokens(entry) {
597
597
  let tokens;
598
598
  if (tokenMethod === "tiktoken") {
599
599
  try {
@@ -605,7 +605,7 @@ async function analyzeProject(projectPath, config) {
605
605
  } else {
606
606
  tokens = countTokensChars4(entry.size);
607
607
  }
608
- files.push({
608
+ return {
609
609
  path: entry.path,
610
610
  relativePath: entry.relativePath,
611
611
  extension: entry.extension,
@@ -614,16 +614,20 @@ async function analyzeProject(projectPath, config) {
614
614
  lines: entry.lines,
615
615
  lastModified: entry.lastModified,
616
616
  kind: classifyFileKind(entry.relativePath),
617
- // Graph data — populated by graph analysis
618
617
  imports: [],
619
618
  importedBy: [],
620
619
  isHub: false,
621
620
  complexity: 0,
622
- // Risk data — populated by risk analysis
623
621
  riskScore: 0,
624
622
  riskFactors: [],
625
623
  exclusionImpact: "none"
626
- });
624
+ };
625
+ }
626
+ const files = [];
627
+ for (let i = 0; i < walkEntries.length; i += BATCH_SIZE) {
628
+ const batch = walkEntries.slice(i, i + BATCH_SIZE);
629
+ const results = await Promise.all(batch.map(estimateFileTokens));
630
+ files.push(...results);
627
631
  }
628
632
  const graph = buildProjectGraph(absPath, files);
629
633
  for (const file of files) {
@@ -3226,6 +3230,52 @@ async function planInteraction(input) {
3226
3230
  // src/api/server.ts
3227
3231
  var API_VERSION = "1.0.0";
3228
3232
  var DEFAULT_PORT = 3141;
3233
+ var FORBIDDEN_PREFIXES = [
3234
+ "/etc",
3235
+ "/usr",
3236
+ "/var",
3237
+ "/sys",
3238
+ "/proc",
3239
+ "/dev",
3240
+ "/boot",
3241
+ "/sbin",
3242
+ "/bin",
3243
+ "/tmp",
3244
+ "/root",
3245
+ "/lib",
3246
+ "/opt"
3247
+ ];
3248
+ var FORBIDDEN_PATTERNS = [".ssh", ".gnupg", ".aws", ".env", "passwd", "shadow"];
3249
+ function getAllowedRoots() {
3250
+ const raw = process.env.CTO_ALLOWED_ROOTS;
3251
+ if (!raw) return null;
3252
+ return raw.split(",").map((r) => resolve7(r.trim()));
3253
+ }
3254
+ function validateProjectPath(projectPath) {
3255
+ if (typeof projectPath !== "string" || !projectPath.trim()) {
3256
+ return { valid: false, reason: "Missing or invalid path field" };
3257
+ }
3258
+ const absPath = resolve7(projectPath);
3259
+ const lower = absPath.toLowerCase();
3260
+ for (const prefix of FORBIDDEN_PREFIXES) {
3261
+ if (lower === prefix || lower.startsWith(prefix + "/")) {
3262
+ return { valid: false, reason: `Access denied: system path ${prefix}` };
3263
+ }
3264
+ }
3265
+ for (const pattern of FORBIDDEN_PATTERNS) {
3266
+ if (lower.includes(pattern)) {
3267
+ return { valid: false, reason: `Access denied: path contains forbidden segment '${pattern}'` };
3268
+ }
3269
+ }
3270
+ const roots = getAllowedRoots();
3271
+ if (roots && roots.length > 0) {
3272
+ const allowed = roots.some((root) => absPath === root || absPath.startsWith(root + "/"));
3273
+ if (!allowed) {
3274
+ return { valid: false, reason: `Access denied: path not under allowed roots. Set CTO_ALLOWED_ROOTS to allow.` };
3275
+ }
3276
+ }
3277
+ return { valid: true, absPath };
3278
+ }
3229
3279
  function validateApiKey(req) {
3230
3280
  const apiKey = process.env.CTO_API_KEY;
3231
3281
  if (!apiKey) return true;
@@ -3287,9 +3337,9 @@ function error(res, status, message) {
3287
3337
  json(res, status, { error: message, status });
3288
3338
  }
3289
3339
  async function handleAnalyze(body, res) {
3290
- const { path: projectPath } = body;
3291
- if (!projectPath) return error(res, 400, "Missing required field: path");
3292
- const absPath = resolve7(projectPath);
3340
+ const check = validateProjectPath(body.path);
3341
+ if (!check.valid) return error(res, 403, check.reason);
3342
+ const absPath = check.absPath;
3293
3343
  const analysis = await getCachedAnalysis(absPath);
3294
3344
  json(res, 200, {
3295
3345
  project: analysis.projectName,
@@ -3307,10 +3357,11 @@ async function handleAnalyze(body, res) {
3307
3357
  });
3308
3358
  }
3309
3359
  async function handleSelect(body, res) {
3310
- const { path: projectPath, task, budget } = body;
3311
- if (!projectPath) return error(res, 400, "Missing required field: path");
3360
+ const { task, budget } = body;
3361
+ const check = validateProjectPath(body.path);
3362
+ if (!check.valid) return error(res, 403, check.reason);
3312
3363
  if (!task) return error(res, 400, "Missing required field: task");
3313
- const absPath = resolve7(projectPath);
3364
+ const absPath = check.absPath;
3314
3365
  const analysis = await getCachedAnalysis(absPath);
3315
3366
  const selection = await selectContext({
3316
3367
  task,
@@ -3334,9 +3385,10 @@ async function handleSelect(body, res) {
3334
3385
  });
3335
3386
  }
3336
3387
  async function handleScore(body, res) {
3337
- const { path: projectPath, task, budget } = body;
3338
- if (!projectPath) return error(res, 400, "Missing required field: path");
3339
- const absPath = resolve7(projectPath);
3388
+ const { task, budget } = body;
3389
+ const check = validateProjectPath(body.path);
3390
+ if (!check.valid) return error(res, 403, check.reason);
3391
+ const absPath = check.absPath;
3340
3392
  const analysis = await getCachedAnalysis(absPath);
3341
3393
  const score = await computeContextScore(
3342
3394
  analysis,
@@ -3359,9 +3411,10 @@ async function handleScore(body, res) {
3359
3411
  });
3360
3412
  }
3361
3413
  async function handleBenchmark(body, res) {
3362
- const { path: projectPath, task, budget } = body;
3363
- if (!projectPath) return error(res, 400, "Missing required field: path");
3364
- const absPath = resolve7(projectPath);
3414
+ const { task, budget } = body;
3415
+ const check = validateProjectPath(body.path);
3416
+ if (!check.valid) return error(res, 403, check.reason);
3417
+ const absPath = check.absPath;
3365
3418
  const analysis = await getCachedAnalysis(absPath);
3366
3419
  const result = await runBenchmark(
3367
3420
  analysis,
@@ -3371,10 +3424,11 @@ async function handleBenchmark(body, res) {
3371
3424
  json(res, 200, result);
3372
3425
  }
3373
3426
  async function handleQuality(body, res) {
3374
- const { path: projectPath, task, budget } = body;
3375
- if (!projectPath) return error(res, 400, "Missing required field: path");
3427
+ const { task, budget } = body;
3428
+ const check = validateProjectPath(body.path);
3429
+ if (!check.valid) return error(res, 403, check.reason);
3376
3430
  if (!task) return error(res, 400, "Missing required field: task");
3377
- const absPath = resolve7(projectPath);
3431
+ const absPath = check.absPath;
3378
3432
  const analysis = await getCachedAnalysis(absPath);
3379
3433
  const result = await runQualityBenchmark(analysis, task, budget ?? 5e4);
3380
3434
  json(res, 200, {
@@ -3388,9 +3442,10 @@ async function handleQuality(body, res) {
3388
3442
  });
3389
3443
  }
3390
3444
  async function handlePRContext(body, res) {
3391
- const { path: projectPath, baseBranch, depth, includeTests } = body;
3392
- if (!projectPath) return error(res, 400, "Missing required field: path");
3393
- const absPath = resolve7(projectPath);
3445
+ const { baseBranch, depth, includeTests } = body;
3446
+ const check = validateProjectPath(body.path);
3447
+ if (!check.valid) return error(res, 403, check.reason);
3448
+ const absPath = check.absPath;
3394
3449
  const analysis = await getCachedAnalysis(absPath);
3395
3450
  const pr = await generatePRContext(analysis, {
3396
3451
  baseBranch: baseBranch ?? "main",
@@ -3409,10 +3464,11 @@ async function handlePRContext(body, res) {
3409
3464
  });
3410
3465
  }
3411
3466
  async function handleInteract(body, res) {
3412
- const { path: projectPath, task, budget, model } = body;
3413
- if (!projectPath) return error(res, 400, "Missing required field: path");
3467
+ const { task, budget, model } = body;
3468
+ const check = validateProjectPath(body.path);
3469
+ if (!check.valid) return error(res, 403, check.reason);
3414
3470
  if (!task) return error(res, 400, "Missing required field: task");
3415
- const absPath = resolve7(projectPath);
3471
+ const absPath = check.absPath;
3416
3472
  const analysis = await getCachedAnalysis(absPath);
3417
3473
  const plan = await planInteraction({
3418
3474
  task,
@@ -3568,8 +3624,9 @@ function createAPIServer() {
3568
3624
  const body = req.method === "GET" ? {} : await readBody(req);
3569
3625
  await handler(body, res);
3570
3626
  } catch (err) {
3571
- console.error(`[CTO API] Error on ${routeKey}:`, err.message);
3572
- error(res, 500, err.message ?? "Internal server error");
3627
+ const message = err instanceof Error ? err.message : String(err);
3628
+ console.error(`[CTO API] Error on ${routeKey}:`, message);
3629
+ error(res, 500, message || "Internal server error");
3573
3630
  }
3574
3631
  });
3575
3632
  return server2;