@corbat-tech/coco 1.1.0 → 1.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.
package/dist/index.js CHANGED
@@ -5973,17 +5973,17 @@ var QualityEvaluator = class {
5973
5973
  maintainabilityResult
5974
5974
  ] = await Promise.all([
5975
5975
  this.coverageAnalyzer.analyze().catch(() => null),
5976
- this.securityScanner.scan(fileContents),
5977
- this.complexityAnalyzer.analyze(targetFiles),
5978
- this.duplicationAnalyzer.analyze(targetFiles),
5976
+ this.securityScanner.scan(fileContents).catch(() => ({ score: 0, vulnerabilities: [] })),
5977
+ this.complexityAnalyzer.analyze(targetFiles).catch(() => ({ score: 0, files: [] })),
5978
+ this.duplicationAnalyzer.analyze(targetFiles).catch(() => ({ score: 0, percentage: 0 })),
5979
5979
  this.correctnessAnalyzer.analyze().catch(() => ({ score: 0 })),
5980
5980
  this.completenessAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 })),
5981
5981
  this.robustnessAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 })),
5982
5982
  this.testQualityAnalyzer.analyze().catch(() => ({ score: 0 })),
5983
5983
  this.documentationAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 })),
5984
- this.styleAnalyzer.analyze().catch(() => ({ score: 50 })),
5985
- this.readabilityAnalyzer.analyze(targetFiles).catch(() => ({ score: 50 })),
5986
- this.maintainabilityAnalyzer.analyze(targetFiles).catch(() => ({ score: 50 }))
5984
+ this.styleAnalyzer.analyze().catch(() => ({ score: 0 })),
5985
+ this.readabilityAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 })),
5986
+ this.maintainabilityAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 }))
5987
5987
  ]);
5988
5988
  const dimensions = {
5989
5989
  testCoverage: coverageResult?.lines.percentage ?? 0,
@@ -9992,6 +9992,9 @@ var AnthropicProvider = class {
9992
9992
  try {
9993
9993
  currentToolCall.input = currentToolInputJson ? JSON.parse(currentToolInputJson) : {};
9994
9994
  } catch {
9995
+ console.warn(
9996
+ `[Anthropic] Failed to parse tool call arguments: ${currentToolInputJson?.slice(0, 100)}`
9997
+ );
9995
9998
  currentToolCall.input = {};
9996
9999
  }
9997
10000
  yield {
@@ -10532,6 +10535,9 @@ var OpenAIProvider = class {
10532
10535
  try {
10533
10536
  input = builder.arguments ? JSON.parse(builder.arguments) : {};
10534
10537
  } catch {
10538
+ console.warn(
10539
+ `[OpenAI] Failed to parse tool call arguments: ${builder.arguments?.slice(0, 100)}`
10540
+ );
10535
10541
  }
10536
10542
  yield {
10537
10543
  type: "tool_use_end",
@@ -10820,7 +10826,16 @@ var OpenAIProvider = class {
10820
10826
  ).map((tc) => ({
10821
10827
  id: tc.id,
10822
10828
  name: tc.function.name,
10823
- input: JSON.parse(tc.function.arguments || "{}")
10829
+ input: (() => {
10830
+ try {
10831
+ return JSON.parse(tc.function.arguments || "{}");
10832
+ } catch {
10833
+ console.warn(
10834
+ `[OpenAI] Failed to parse tool call arguments: ${tc.function.arguments?.slice(0, 100)}`
10835
+ );
10836
+ return {};
10837
+ }
10838
+ })()
10824
10839
  }));
10825
10840
  }
10826
10841
  /**
@@ -11475,7 +11490,10 @@ var GeminiProvider = class {
11475
11490
  for (const part of candidate.content.parts) {
11476
11491
  if ("functionCall" in part && part.functionCall) {
11477
11492
  const funcCall = part.functionCall;
11478
- const callKey = `${funcCall.name}-${JSON.stringify(funcCall.args)}`;
11493
+ const sortedArgs = funcCall.args ? Object.keys(funcCall.args).sort().map(
11494
+ (k) => `${k}:${JSON.stringify(funcCall.args[k])}`
11495
+ ).join(",") : "";
11496
+ const callKey = `${funcCall.name}-${sortedArgs}`;
11479
11497
  if (!emittedToolCalls.has(callKey)) {
11480
11498
  emittedToolCalls.add(callKey);
11481
11499
  const toolCall = {
@@ -11507,9 +11525,18 @@ var GeminiProvider = class {
11507
11525
  }
11508
11526
  /**
11509
11527
  * Count tokens (approximate)
11528
+ *
11529
+ * Gemini uses a SentencePiece tokenizer. The average ratio varies:
11530
+ * - English text: ~4 characters per token
11531
+ * - Code: ~3.2 characters per token
11532
+ * - Mixed content: ~3.5 characters per token
11533
+ *
11534
+ * Using 3.5 as the default provides a better estimate for typical
11535
+ * coding agent workloads which mix code and natural language.
11510
11536
  */
11511
11537
  countTokens(text) {
11512
- return Math.ceil(text.length / 4);
11538
+ if (!text) return 0;
11539
+ return Math.ceil(text.length / 3.5);
11513
11540
  }
11514
11541
  /**
11515
11542
  * Get context window size
@@ -11800,6 +11827,19 @@ function createOrchestrator(config) {
11800
11827
  },
11801
11828
  async transitionTo(phase) {
11802
11829
  const previousPhase = state.currentPhase;
11830
+ const validTransitions = {
11831
+ idle: ["converge"],
11832
+ converge: ["orchestrate"],
11833
+ orchestrate: ["complete"],
11834
+ complete: ["output"],
11835
+ output: []
11836
+ };
11837
+ const allowed = validTransitions[previousPhase];
11838
+ if (allowed && !allowed.includes(phase)) {
11839
+ throw new Error(
11840
+ `Invalid phase transition: ${previousPhase} \u2192 ${phase}. Allowed: ${allowed.join(", ") || "none"}`
11841
+ );
11842
+ }
11803
11843
  state.phaseHistory.push({
11804
11844
  from: previousPhase,
11805
11845
  to: phase,
@@ -11868,7 +11908,10 @@ async function saveState(state) {
11868
11908
  const fs33 = await import('fs/promises');
11869
11909
  const statePath = `${state.path}/.coco/state`;
11870
11910
  await fs33.mkdir(statePath, { recursive: true });
11871
- await fs33.writeFile(`${statePath}/project.json`, JSON.stringify(state, null, 2), "utf-8");
11911
+ const filePath = `${statePath}/project.json`;
11912
+ const tmpPath = `${filePath}.tmp.${Date.now()}`;
11913
+ await fs33.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
11914
+ await fs33.rename(tmpPath, filePath);
11872
11915
  }
11873
11916
  function getPhaseExecutor(phase) {
11874
11917
  switch (phase) {
@@ -12145,7 +12188,10 @@ async function saveSnapshot(state, snapshotId) {
12145
12188
  }
12146
12189
  }
12147
12190
  function restoreFromSnapshot(target, snapshot) {
12148
- Object.assign(target, snapshot);
12191
+ const deep = JSON.parse(JSON.stringify(snapshot));
12192
+ deep.createdAt = new Date(deep.createdAt);
12193
+ deep.updatedAt = new Date(deep.updatedAt);
12194
+ Object.assign(target, deep);
12149
12195
  }
12150
12196
  async function executePhase(phase, state, config) {
12151
12197
  const executor = getPhaseExecutor(phase);
@@ -12234,9 +12280,6 @@ var QualityConfigSchema = z.object({
12234
12280
  }).refine((data) => data.minIterations <= data.maxIterations, {
12235
12281
  message: "minIterations must be <= maxIterations",
12236
12282
  path: ["minIterations"]
12237
- }).refine((data) => data.convergenceThreshold < data.minScore, {
12238
- message: "convergenceThreshold must be < minScore",
12239
- path: ["convergenceThreshold"]
12240
12283
  });
12241
12284
  var PersistenceConfigSchema = z.object({
12242
12285
  checkpointInterval: z.number().min(6e4).default(3e5),
@@ -12429,7 +12472,17 @@ function deepMergeConfig(base, override) {
12429
12472
  project: { ...base.project, ...override.project },
12430
12473
  provider: { ...base.provider, ...override.provider },
12431
12474
  quality: { ...base.quality, ...override.quality },
12432
- persistence: { ...base.persistence, ...override.persistence }
12475
+ persistence: { ...base.persistence, ...override.persistence },
12476
+ // Merge optional sections only if present in either base or override
12477
+ ...base.stack || override.stack ? { stack: { ...base.stack, ...override.stack } } : {},
12478
+ ...base.integrations || override.integrations ? {
12479
+ integrations: {
12480
+ ...base.integrations,
12481
+ ...override.integrations
12482
+ }
12483
+ } : {},
12484
+ ...base.mcp || override.mcp ? { mcp: { ...base.mcp, ...override.mcp } } : {},
12485
+ ...base.tools || override.tools ? { tools: { ...base.tools, ...override.tools } } : {}
12433
12486
  };
12434
12487
  }
12435
12488
  function getProjectConfigPath() {
@@ -14536,7 +14589,17 @@ Examples:
14536
14589
  regexPattern = `\\b${pattern}\\b`;
14537
14590
  }
14538
14591
  const flags = caseSensitive ? "g" : "gi";
14539
- const regex = new RegExp(regexPattern, flags);
14592
+ let regex;
14593
+ try {
14594
+ regex = new RegExp(regexPattern, flags);
14595
+ } catch {
14596
+ throw new ToolError(`Invalid regex pattern: ${pattern}`, { tool: "grep" });
14597
+ }
14598
+ if (/(\.\*|\.\+|\[.*\][*+])\s*(\.\*|\.\+|\[.*\][*+])/.test(regexPattern)) {
14599
+ throw new ToolError(`Regex pattern rejected: nested quantifiers may cause slow matching`, {
14600
+ tool: "grep"
14601
+ });
14602
+ }
14540
14603
  const stats = await fs14__default.stat(targetPath);
14541
14604
  let filesToSearch;
14542
14605
  if (stats.isFile()) {
@@ -16116,9 +16179,11 @@ function parseDiff(raw) {
16116
16179
  function parseFileBlock(lines, start) {
16117
16180
  const diffLine = lines[start];
16118
16181
  let i = start + 1;
16119
- const pathMatch = diffLine.match(/^diff --git a\/(.+?) b\/(.+)$/);
16120
- const oldPath = pathMatch?.[1] ?? "";
16121
- const newPath = pathMatch?.[2] ?? oldPath;
16182
+ const gitPrefix = "diff --git a/";
16183
+ const pathPart = diffLine.slice(gitPrefix.length);
16184
+ const lastBSlash = pathPart.lastIndexOf(" b/");
16185
+ const oldPath = lastBSlash >= 0 ? pathPart.slice(0, lastBSlash) : pathPart;
16186
+ const newPath = lastBSlash >= 0 ? pathPart.slice(lastBSlash + 3) : oldPath;
16122
16187
  let fileType = "modified";
16123
16188
  while (i < lines.length && !lines[i].startsWith("diff --git ")) {
16124
16189
  const current = lines[i];
@@ -16264,7 +16329,7 @@ function renderDiff(diff, options) {
16264
16329
  function renderFileBlock(file, opts) {
16265
16330
  const { maxWidth, showLineNumbers, compact } = opts;
16266
16331
  const lang = detectLanguage(file.path);
16267
- const contentWidth = maxWidth - 4;
16332
+ const contentWidth = Math.max(1, maxWidth - 4);
16268
16333
  const typeLabel = file.type === "modified" ? "modified" : file.type === "added" ? "new file" : file.type === "deleted" ? "deleted" : `renamed from ${file.oldPath}`;
16269
16334
  const statsLabel = ` +${file.additions} -${file.deletions}`;
16270
16335
  const title = ` ${file.path} (${typeLabel}${statsLabel}) `;
@@ -16311,12 +16376,8 @@ function renderFileBlock(file, opts) {
16311
16376
  }
16312
16377
  function formatLineNo(line, show) {
16313
16378
  if (!show) return "";
16314
- if (line.type === "add") {
16315
- return chalk3.dim(`${String(line.newLineNo ?? "").padStart(4)} `);
16316
- } else if (line.type === "delete") {
16317
- return chalk3.dim(`${String(line.oldLineNo ?? "").padStart(4)} `);
16318
- }
16319
- return chalk3.dim(`${String(line.newLineNo ?? "").padStart(4)} `);
16379
+ const lineNo = line.type === "delete" ? line.oldLineNo : line.newLineNo;
16380
+ return chalk3.dim(`${String(lineNo ?? "").padStart(5)} `);
16320
16381
  }
16321
16382
  function getChangedLines(diff) {
16322
16383
  const result = /* @__PURE__ */ new Map();
@@ -18473,6 +18534,13 @@ Examples:
18473
18534
  const startTime = performance.now();
18474
18535
  const effectivePrompt = prompt ?? "Describe this image in detail. If it's code or a UI, identify the key elements.";
18475
18536
  const absPath = path27.resolve(filePath);
18537
+ const cwd = process.cwd();
18538
+ if (!absPath.startsWith(cwd + path27.sep) && absPath !== cwd) {
18539
+ throw new ToolError(
18540
+ `Path traversal denied: '${filePath}' resolves outside the project directory`,
18541
+ { tool: "read_image" }
18542
+ );
18543
+ }
18476
18544
  const ext = path27.extname(absPath).toLowerCase();
18477
18545
  if (!SUPPORTED_FORMATS.has(ext)) {
18478
18546
  throw new ToolError(