@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/cli/index.js CHANGED
@@ -724,9 +724,6 @@ var QualityConfigSchema = z.object({
724
724
  }).refine((data) => data.minIterations <= data.maxIterations, {
725
725
  message: "minIterations must be <= maxIterations",
726
726
  path: ["minIterations"]
727
- }).refine((data) => data.convergenceThreshold < data.minScore, {
728
- message: "convergenceThreshold must be < minScore",
729
- path: ["convergenceThreshold"]
730
727
  });
731
728
  var PersistenceConfigSchema = z.object({
732
729
  checkpointInterval: z.number().min(6e4).default(3e5),
@@ -1072,7 +1069,17 @@ function deepMergeConfig(base, override) {
1072
1069
  project: { ...base.project, ...override.project },
1073
1070
  provider: { ...base.provider, ...override.provider },
1074
1071
  quality: { ...base.quality, ...override.quality },
1075
- persistence: { ...base.persistence, ...override.persistence }
1072
+ persistence: { ...base.persistence, ...override.persistence },
1073
+ // Merge optional sections only if present in either base or override
1074
+ ...base.stack || override.stack ? { stack: { ...base.stack, ...override.stack } } : {},
1075
+ ...base.integrations || override.integrations ? {
1076
+ integrations: {
1077
+ ...base.integrations,
1078
+ ...override.integrations
1079
+ }
1080
+ } : {},
1081
+ ...base.mcp || override.mcp ? { mcp: { ...base.mcp, ...override.mcp } } : {},
1082
+ ...base.tools || override.tools ? { tools: { ...base.tools, ...override.tools } } : {}
1076
1083
  };
1077
1084
  }
1078
1085
  function getProjectConfigPath() {
@@ -4778,6 +4785,9 @@ var AnthropicProvider = class {
4778
4785
  try {
4779
4786
  currentToolCall.input = currentToolInputJson ? JSON.parse(currentToolInputJson) : {};
4780
4787
  } catch {
4788
+ console.warn(
4789
+ `[Anthropic] Failed to parse tool call arguments: ${currentToolInputJson?.slice(0, 100)}`
4790
+ );
4781
4791
  currentToolCall.input = {};
4782
4792
  }
4783
4793
  yield {
@@ -5310,6 +5320,9 @@ var OpenAIProvider = class {
5310
5320
  try {
5311
5321
  input = builder.arguments ? JSON.parse(builder.arguments) : {};
5312
5322
  } catch {
5323
+ console.warn(
5324
+ `[OpenAI] Failed to parse tool call arguments: ${builder.arguments?.slice(0, 100)}`
5325
+ );
5313
5326
  }
5314
5327
  yield {
5315
5328
  type: "tool_use_end",
@@ -5598,7 +5611,16 @@ var OpenAIProvider = class {
5598
5611
  ).map((tc) => ({
5599
5612
  id: tc.id,
5600
5613
  name: tc.function.name,
5601
- input: JSON.parse(tc.function.arguments || "{}")
5614
+ input: (() => {
5615
+ try {
5616
+ return JSON.parse(tc.function.arguments || "{}");
5617
+ } catch {
5618
+ console.warn(
5619
+ `[OpenAI] Failed to parse tool call arguments: ${tc.function.arguments?.slice(0, 100)}`
5620
+ );
5621
+ return {};
5622
+ }
5623
+ })()
5602
5624
  }));
5603
5625
  }
5604
5626
  /**
@@ -7246,7 +7268,10 @@ var GeminiProvider = class {
7246
7268
  for (const part of candidate.content.parts) {
7247
7269
  if ("functionCall" in part && part.functionCall) {
7248
7270
  const funcCall = part.functionCall;
7249
- const callKey = `${funcCall.name}-${JSON.stringify(funcCall.args)}`;
7271
+ const sortedArgs = funcCall.args ? Object.keys(funcCall.args).sort().map(
7272
+ (k) => `${k}:${JSON.stringify(funcCall.args[k])}`
7273
+ ).join(",") : "";
7274
+ const callKey = `${funcCall.name}-${sortedArgs}`;
7250
7275
  if (!emittedToolCalls.has(callKey)) {
7251
7276
  emittedToolCalls.add(callKey);
7252
7277
  const toolCall = {
@@ -7278,9 +7303,18 @@ var GeminiProvider = class {
7278
7303
  }
7279
7304
  /**
7280
7305
  * Count tokens (approximate)
7306
+ *
7307
+ * Gemini uses a SentencePiece tokenizer. The average ratio varies:
7308
+ * - English text: ~4 characters per token
7309
+ * - Code: ~3.2 characters per token
7310
+ * - Mixed content: ~3.5 characters per token
7311
+ *
7312
+ * Using 3.5 as the default provides a better estimate for typical
7313
+ * coding agent workloads which mix code and natural language.
7281
7314
  */
7282
7315
  countTokens(text9) {
7283
- return Math.ceil(text9.length / 4);
7316
+ if (!text9) return 0;
7317
+ return Math.ceil(text9.length / 3.5);
7284
7318
  }
7285
7319
  /**
7286
7320
  * Get context window size
@@ -9658,7 +9692,9 @@ async function saveTrustSettings(settings) {
9658
9692
  await fs22__default.mkdir(TRUST_SETTINGS_DIR, { recursive: true });
9659
9693
  settings.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9660
9694
  await fs22__default.writeFile(TRUST_SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf-8");
9661
- } catch {
9695
+ } catch (error) {
9696
+ const msg = error instanceof Error ? error.message : String(error);
9697
+ console.warn(`[Trust] Failed to save trust settings: ${msg}`);
9662
9698
  }
9663
9699
  }
9664
9700
  async function loadTrustedTools(projectPath) {
@@ -14369,7 +14405,11 @@ function flushLineBuffer() {
14369
14405
  }
14370
14406
  if (inCodeBlock && codeBlockLines.length > 0) {
14371
14407
  stopStreamingIndicator();
14372
- renderCodeBlock(codeBlockLang, codeBlockLines);
14408
+ try {
14409
+ renderCodeBlock(codeBlockLang, codeBlockLines);
14410
+ } finally {
14411
+ stopStreamingIndicator();
14412
+ }
14373
14413
  inCodeBlock = false;
14374
14414
  codeBlockLang = "";
14375
14415
  codeBlockLines = [];
@@ -14725,6 +14765,7 @@ function formatInlineMarkdown(text9) {
14725
14765
  return text9;
14726
14766
  }
14727
14767
  function wrapText(text9, maxWidth) {
14768
+ if (maxWidth <= 0) return [text9];
14728
14769
  const plainText = stripAnsi(text9);
14729
14770
  if (plainText.length <= maxWidth) {
14730
14771
  return [text9];
@@ -14732,14 +14773,37 @@ function wrapText(text9, maxWidth) {
14732
14773
  const lines = [];
14733
14774
  let remaining = text9;
14734
14775
  while (stripAnsi(remaining).length > maxWidth) {
14735
- let breakPoint = maxWidth;
14736
14776
  const plain = stripAnsi(remaining);
14777
+ let breakPoint = maxWidth;
14737
14778
  const lastSpace = plain.lastIndexOf(" ", maxWidth);
14738
14779
  if (lastSpace > maxWidth * 0.5) {
14739
14780
  breakPoint = lastSpace;
14740
14781
  }
14741
- lines.push(remaining.slice(0, breakPoint));
14742
- remaining = remaining.slice(breakPoint).trimStart();
14782
+ const ansiRegex = /\x1b\[[0-9;]*m/g;
14783
+ let match;
14784
+ const ansiPositions = [];
14785
+ ansiRegex.lastIndex = 0;
14786
+ while ((match = ansiRegex.exec(remaining)) !== null) {
14787
+ ansiPositions.push({ start: match.index, end: match.index + match[0].length });
14788
+ }
14789
+ let rawPos = 0;
14790
+ let visualPos = 0;
14791
+ let ansiIdx = 0;
14792
+ while (visualPos < breakPoint && rawPos < remaining.length) {
14793
+ while (ansiIdx < ansiPositions.length && ansiPositions[ansiIdx].start === rawPos) {
14794
+ rawPos = ansiPositions[ansiIdx].end;
14795
+ ansiIdx++;
14796
+ }
14797
+ if (rawPos >= remaining.length) break;
14798
+ rawPos++;
14799
+ visualPos++;
14800
+ }
14801
+ while (ansiIdx < ansiPositions.length && ansiPositions[ansiIdx].start === rawPos) {
14802
+ rawPos = ansiPositions[ansiIdx].end;
14803
+ ansiIdx++;
14804
+ }
14805
+ lines.push(remaining.slice(0, rawPos));
14806
+ remaining = remaining.slice(rawPos).trimStart();
14743
14807
  }
14744
14808
  if (remaining) {
14745
14809
  lines.push(remaining);
@@ -16188,6 +16252,8 @@ function createInputHandler(_session) {
16188
16252
  let historyIndex = -1;
16189
16253
  let tempLine = "";
16190
16254
  let lastMenuLines = 0;
16255
+ let lastCursorRow = 0;
16256
+ let isFirstRender = true;
16191
16257
  let isPasting = false;
16192
16258
  let pasteBuffer = "";
16193
16259
  let isReadingClipboard = false;
@@ -16219,11 +16285,13 @@ function createInputHandler(_session) {
16219
16285
  function render() {
16220
16286
  const termCols = process.stdout.columns || 80;
16221
16287
  const prompt = getPrompt();
16222
- const totalVisualLen = prompt.visualLen + currentLine.length;
16223
- const wrappedLines = Math.max(0, Math.ceil(totalVisualLen / termCols) - 1);
16224
- if (wrappedLines > 0) {
16225
- process.stdout.write(ansiEscapes3.cursorUp(wrappedLines));
16288
+ if (!isFirstRender) {
16289
+ const linesToGoUp = lastCursorRow + 1;
16290
+ if (linesToGoUp > 0) {
16291
+ process.stdout.write(ansiEscapes3.cursorUp(linesToGoUp));
16292
+ }
16226
16293
  }
16294
+ isFirstRender = false;
16227
16295
  process.stdout.write("\r" + ansiEscapes3.eraseDown);
16228
16296
  const separator = chalk12.dim("\u2500".repeat(termCols));
16229
16297
  let output = separator + "\n";
@@ -16299,6 +16367,7 @@ function createInputHandler(_session) {
16299
16367
  if (finalCol > 0) {
16300
16368
  output += ansiEscapes3.cursorForward(finalCol);
16301
16369
  }
16370
+ lastCursorRow = finalLine;
16302
16371
  process.stdout.write(output);
16303
16372
  }
16304
16373
  function clearMenu() {
@@ -16325,15 +16394,20 @@ function createInputHandler(_session) {
16325
16394
  historyIndex = -1;
16326
16395
  tempLine = "";
16327
16396
  lastMenuLines = 0;
16397
+ lastCursorRow = 0;
16398
+ isFirstRender = true;
16328
16399
  render();
16329
16400
  if (process.stdin.isTTY) {
16330
16401
  process.stdin.setRawMode(true);
16331
16402
  }
16332
16403
  process.stdin.resume();
16333
16404
  process.stdout.write("\x1B[?2004h");
16405
+ const onResize = () => render();
16406
+ process.stdout.on("resize", onResize);
16334
16407
  const cleanup = () => {
16335
16408
  process.stdout.write("\x1B[?2004l");
16336
16409
  process.stdin.removeListener("data", onData);
16410
+ process.stdout.removeListener("resize", onResize);
16337
16411
  if (process.stdin.isTTY) {
16338
16412
  process.stdin.setRawMode(false);
16339
16413
  }
@@ -17504,7 +17578,8 @@ async function promptEditCommand(originalCommand) {
17504
17578
  console.log();
17505
17579
  console.log(chalk12.dim(" Edit command (or press Enter to cancel):"));
17506
17580
  console.log(chalk12.cyan(` Current: ${originalCommand}`));
17507
- if (process.stdin.isTTY && process.stdin.isRaw) {
17581
+ const wasRaw = process.stdin.isTTY ? process.stdin.isRaw : false;
17582
+ if (process.stdin.isTTY && wasRaw) {
17508
17583
  process.stdin.setRawMode(false);
17509
17584
  }
17510
17585
  const rl = readline2.createInterface({
@@ -17517,6 +17592,9 @@ async function promptEditCommand(originalCommand) {
17517
17592
  return trimmed || null;
17518
17593
  } finally {
17519
17594
  rl.close();
17595
+ if (process.stdin.isTTY && wasRaw) {
17596
+ process.stdin.setRawMode(true);
17597
+ }
17520
17598
  }
17521
17599
  }
17522
17600
  async function confirmToolExecution(toolCall) {
@@ -22452,17 +22530,17 @@ var QualityEvaluator = class {
22452
22530
  maintainabilityResult
22453
22531
  ] = await Promise.all([
22454
22532
  this.coverageAnalyzer.analyze().catch(() => null),
22455
- this.securityScanner.scan(fileContents),
22456
- this.complexityAnalyzer.analyze(targetFiles),
22457
- this.duplicationAnalyzer.analyze(targetFiles),
22533
+ this.securityScanner.scan(fileContents).catch(() => ({ score: 0, vulnerabilities: [] })),
22534
+ this.complexityAnalyzer.analyze(targetFiles).catch(() => ({ score: 0, files: [] })),
22535
+ this.duplicationAnalyzer.analyze(targetFiles).catch(() => ({ score: 0, percentage: 0 })),
22458
22536
  this.correctnessAnalyzer.analyze().catch(() => ({ score: 0 })),
22459
22537
  this.completenessAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 })),
22460
22538
  this.robustnessAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 })),
22461
22539
  this.testQualityAnalyzer.analyze().catch(() => ({ score: 0 })),
22462
22540
  this.documentationAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 })),
22463
- this.styleAnalyzer.analyze().catch(() => ({ score: 50 })),
22464
- this.readabilityAnalyzer.analyze(targetFiles).catch(() => ({ score: 50 })),
22465
- this.maintainabilityAnalyzer.analyze(targetFiles).catch(() => ({ score: 50 }))
22541
+ this.styleAnalyzer.analyze().catch(() => ({ score: 0 })),
22542
+ this.readabilityAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 })),
22543
+ this.maintainabilityAnalyzer.analyze(targetFiles).catch(() => ({ score: 0 }))
22466
22544
  ]);
22467
22545
  const dimensions = {
22468
22546
  testCoverage: coverageResult?.lines.percentage ?? 0,
@@ -22976,7 +23054,17 @@ Examples:
22976
23054
  regexPattern = `\\b${pattern}\\b`;
22977
23055
  }
22978
23056
  const flags = caseSensitive ? "g" : "gi";
22979
- const regex = new RegExp(regexPattern, flags);
23057
+ let regex;
23058
+ try {
23059
+ regex = new RegExp(regexPattern, flags);
23060
+ } catch {
23061
+ throw new ToolError(`Invalid regex pattern: ${pattern}`, { tool: "grep" });
23062
+ }
23063
+ if (/(\.\*|\.\+|\[.*\][*+])\s*(\.\*|\.\+|\[.*\][*+])/.test(regexPattern)) {
23064
+ throw new ToolError(`Regex pattern rejected: nested quantifiers may cause slow matching`, {
23065
+ tool: "grep"
23066
+ });
23067
+ }
22980
23068
  const stats = await fs22__default.stat(targetPath);
22981
23069
  let filesToSearch;
22982
23070
  if (stats.isFile()) {
@@ -24114,9 +24202,11 @@ function parseDiff(raw) {
24114
24202
  function parseFileBlock(lines, start) {
24115
24203
  const diffLine = lines[start];
24116
24204
  let i = start + 1;
24117
- const pathMatch = diffLine.match(/^diff --git a\/(.+?) b\/(.+)$/);
24118
- const oldPath = pathMatch?.[1] ?? "";
24119
- const newPath = pathMatch?.[2] ?? oldPath;
24205
+ const gitPrefix = "diff --git a/";
24206
+ const pathPart = diffLine.slice(gitPrefix.length);
24207
+ const lastBSlash = pathPart.lastIndexOf(" b/");
24208
+ const oldPath = lastBSlash >= 0 ? pathPart.slice(0, lastBSlash) : pathPart;
24209
+ const newPath = lastBSlash >= 0 ? pathPart.slice(lastBSlash + 3) : oldPath;
24120
24210
  let fileType = "modified";
24121
24211
  while (i < lines.length && !lines[i].startsWith("diff --git ")) {
24122
24212
  const current = lines[i];
@@ -24262,7 +24352,7 @@ function renderDiff(diff, options) {
24262
24352
  function renderFileBlock(file, opts) {
24263
24353
  const { maxWidth, showLineNumbers, compact } = opts;
24264
24354
  const lang = detectLanguage(file.path);
24265
- const contentWidth = maxWidth - 4;
24355
+ const contentWidth = Math.max(1, maxWidth - 4);
24266
24356
  const typeLabel = file.type === "modified" ? "modified" : file.type === "added" ? "new file" : file.type === "deleted" ? "deleted" : `renamed from ${file.oldPath}`;
24267
24357
  const statsLabel = ` +${file.additions} -${file.deletions}`;
24268
24358
  const title = ` ${file.path} (${typeLabel}${statsLabel}) `;
@@ -24309,12 +24399,8 @@ function renderFileBlock(file, opts) {
24309
24399
  }
24310
24400
  function formatLineNo(line, show) {
24311
24401
  if (!show) return "";
24312
- if (line.type === "add") {
24313
- return chalk12.dim(`${String(line.newLineNo ?? "").padStart(4)} `);
24314
- } else if (line.type === "delete") {
24315
- return chalk12.dim(`${String(line.oldLineNo ?? "").padStart(4)} `);
24316
- }
24317
- return chalk12.dim(`${String(line.newLineNo ?? "").padStart(4)} `);
24402
+ const lineNo = line.type === "delete" ? line.oldLineNo : line.newLineNo;
24403
+ return chalk12.dim(`${String(lineNo ?? "").padStart(5)} `);
24318
24404
  }
24319
24405
  function getChangedLines(diff) {
24320
24406
  const result = /* @__PURE__ */ new Map();
@@ -26471,6 +26557,13 @@ Examples:
26471
26557
  const startTime = performance.now();
26472
26558
  const effectivePrompt = prompt ?? "Describe this image in detail. If it's code or a UI, identify the key elements.";
26473
26559
  const absPath = path31.resolve(filePath);
26560
+ const cwd = process.cwd();
26561
+ if (!absPath.startsWith(cwd + path31.sep) && absPath !== cwd) {
26562
+ throw new ToolError(
26563
+ `Path traversal denied: '${filePath}' resolves outside the project directory`,
26564
+ { tool: "read_image" }
26565
+ );
26566
+ }
26474
26567
  const ext = path31.extname(absPath).toLowerCase();
26475
26568
  if (!SUPPORTED_FORMATS.has(ext)) {
26476
26569
  throw new ToolError(
@@ -28598,6 +28691,17 @@ async function startRepl(options = {}) {
28598
28691
  const inputHandler = createInputHandler();
28599
28692
  const intentRecognizer = createIntentRecognizer();
28600
28693
  await printWelcome(session);
28694
+ const cleanupTerminal = () => {
28695
+ process.stdout.write("\x1B[?2004l");
28696
+ if (process.stdin.isTTY && process.stdin.isRaw) {
28697
+ process.stdin.setRawMode(false);
28698
+ }
28699
+ };
28700
+ process.on("exit", cleanupTerminal);
28701
+ process.on("SIGTERM", () => {
28702
+ cleanupTerminal();
28703
+ process.exit(0);
28704
+ });
28601
28705
  while (true) {
28602
28706
  const input = await inputHandler.prompt();
28603
28707
  if (input === null && !hasPendingImage()) {
@@ -28677,6 +28781,14 @@ async function startRepl(options = {}) {
28677
28781
  activeSpinner.start();
28678
28782
  }
28679
28783
  };
28784
+ const abortController = new AbortController();
28785
+ let wasAborted = false;
28786
+ const sigintHandler = () => {
28787
+ wasAborted = true;
28788
+ abortController.abort();
28789
+ clearSpinner();
28790
+ renderInfo("\nOperation cancelled");
28791
+ };
28680
28792
  try {
28681
28793
  if (typeof agentMessage === "string" && !isCocoMode() && !wasHintShown() && looksLikeFeatureRequest(agentMessage)) {
28682
28794
  markHintShown();
@@ -28689,14 +28801,6 @@ async function startRepl(options = {}) {
28689
28801
  session.config.agent.systemPrompt = originalSystemPrompt + "\n" + getCocoModeSystemPrompt();
28690
28802
  }
28691
28803
  inputHandler.pause();
28692
- const abortController = new AbortController();
28693
- let wasAborted = false;
28694
- const sigintHandler = () => {
28695
- wasAborted = true;
28696
- abortController.abort();
28697
- clearSpinner();
28698
- renderInfo("\nOperation cancelled");
28699
- };
28700
28804
  process.once("SIGINT", sigintHandler);
28701
28805
  const result = await executeAgentTurn(session, agentMessage, provider, toolRegistry, {
28702
28806
  onStream: renderStreamChunk,
@@ -28785,6 +28889,7 @@ async function startRepl(options = {}) {
28785
28889
  console.log();
28786
28890
  } catch (error) {
28787
28891
  clearSpinner();
28892
+ process.off("SIGINT", sigintHandler);
28788
28893
  if (error instanceof Error && error.name === "AbortError") {
28789
28894
  continue;
28790
28895
  }