tachibot-mcp 2.17.0 → 2.19.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/CHANGELOG.md CHANGED
@@ -5,6 +5,65 @@ All notable changes to TachiBot MCP will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.19.0] - 2026-03-21
9
+
10
+ ### Added
11
+ - **Sparse render mode** (`RENDER_OUTPUT=sparse`) — lightweight output formatting with ~72 tokens overhead per response
12
+ - **ANSI model badges** — colored background badges for model name (provider color) + tool name (charcoal bg)
13
+ - **Pastel section headers** — emoji section headers (`🧠 HEADER ───`) rendered as teal bg + dark bold text badges
14
+ - **Color-coded verdicts** — `✅ pass` (sage green), `🫠 partial` (soft yellow), `💀 fail` (rose) with colored bg badges
15
+ - **Summary badge** — tool name displayed as bold charcoal badge next to model badge
16
+ - **`stripMarkdown` options** — `{ boldHeaders: true }` converts markdown/emoji headers to ANSI-styled badges
17
+ - **Empty input guard** on `stripMarkdown` — early return for empty/whitespace input
18
+ - **Strip markdown headers** — `##` prefixes and `───` decorators removed from output
19
+ - **8 unit tests** for `stripMarkdown` covering headers, bold, bullets, code blocks, HR, empty input
20
+
21
+ ### Fixed
22
+ - **ANSI truncation corruption** — truncate raw content BEFORE applying ANSI badges (prevents mid-escape code corruption)
23
+ - **Summary badge without model** — tools returning null from `inferModelFromTool` (think, focus) still show tool name badge
24
+ - **Unused imports** — cleaned up 10+ unused imports/variables in server.ts
25
+
26
+ ### Changed
27
+ - **Emoji palette** — analysis 🧠, insight 🔮, key 🗝, verdict 👩‍⚖️ (replaced 🔍🧿🪩🎯)
28
+ - **Auditor/Challenger** — use `EMOJI_PALETTE` constants instead of hardcoded emoji
29
+ - **Planner** — topological task ordering with T-ID preservation and Dependencies metadata
30
+
31
+ ## [2.18.0] - 2026-03-21
32
+
33
+ ### Added
34
+ - **Goal-oriented checkpoints** — `planner_maker` and `planner_runner` now accept `goal` parameter for success criteria tracking
35
+ - **6 checkpoint gates** with 5 different models (no adjacent repeats): step1 (Gemini Sherlock), 10% (Grok), 25% (GPT + amendment protocol), 50% (Qwen), 80% (Kimi decompose), 100% (GPT+Gemini dual judge)
36
+ - **Reflexion Lite** — at 100%, Gemini reflects on what worked/failed, lesson saved to devlog
37
+ - **Amendment protocol** — at 25%, structured plan revision (evidence + proposed changes + impact) with human gate
38
+ - **Unblinded checkpoints** — `diff`, `testResults`, `modifiedFiles` params replace blind `code.substring(0,1500)` with real evidence
39
+ - **`files` param on all analysis tools** — 39 tools across 9 files can now read ACTUAL CODE from disk via `readFilesIntoContext()`
40
+ - **Shared `src/utils/file-reader.ts`** — reusable file reader with line range support (`file.ts:100-200`), size limits, directory expansion
41
+ - **Blueprint skill updated** — `goal` param, prompt template, `planner_runner` as default execution path
42
+
43
+ ### Fixed
44
+ - **Step index reset bug** — filtered arrays used local index instead of original step number (found by 3-model consensus: Kimi + Gemini + Qwen reading actual code)
45
+ - **Truncation indicators** — `code.substring()` now adds `[truncated]` so judge models know they're seeing partial code
46
+
47
+ ## [2.17.2] - 2026-03-21
48
+
49
+ ### Added
50
+ - **`files` parameter on 13+ more tools** — grok_architect, grok_brainstorm, grok_reason_v4, openai_explain, kimi_code, kimi_long_context, gemini_judge, gemini_brainstorm, gemini_query, gemini_summarize, qwq_reason, qwen_competitive, qwen_general (38 tools now support `files`)
51
+ - **Directory expansion in file reader** — pass `src/tools/` to read all code files in a directory (non-recursive, capped at 20 files)
52
+ - **Smart char budget** — multi-file reads distribute token budget across files to prevent context overflow
53
+
54
+ ## [2.17.1] - 2026-03-21
55
+
56
+ ### Fixed
57
+ - **kimi_decompose readability overhaul** — output now uses OVERVIEW/STRUCTURE/DETAILS/RISKS sections instead of dense inline metadata
58
+ - **Reasoning leak stripped** — Kimi K2.5 dumps CoT into content; now extracted via `<output>` tags with OVERVIEW fallback
59
+ - **Conflicting FORMAT_INSTRUCTION removed** — emoji headers and verdict lines no longer clash with decomposition formatting
60
+ - **Heartbeat interval fixed** — was incorrectly set to 240s instead of default 5s; network timeout now correctly passed to callOpenRouter (360s)
61
+ - **Type safety** — args typed from zod schema, unused `log` removed, `||` replaced with `??`
62
+
63
+ ### Changed
64
+ - **Smart decomposition** — model now infers context, constraints, risks, and measurable criteria even when user doesn't state them
65
+ - **Tuned for format adherence** — temperature 0.3 (was 0.5), maxTokens 4500 (was 6000), timeout 360s (was 180s default)
66
+
8
67
  ## [2.17.0] - 2026-03-21
9
68
 
10
69
  ### Changed
@@ -1,3 +1,4 @@
1
+ import { EMOJI_PALETTE } from '../utils/format-constants.js';
1
2
  export class Auditor {
2
3
  constructor() {
3
4
  this.defaultModel = 'perplexity-sonar-pro';
@@ -328,7 +329,7 @@ export class Auditor {
328
329
  // Critical assumptions
329
330
  const criticalAssumptions = assumptions.filter(a => a.risk === 'high');
330
331
  if (criticalAssumptions.length > 0) {
331
- synthesis += `**Critical Assumptions** 🔴:\n`;
332
+ synthesis += `**Critical Assumptions** ${EMOJI_PALETTE.bad}:\n`;
332
333
  criticalAssumptions.forEach(assumption => {
333
334
  synthesis += `- ${assumption.description}\n`;
334
335
  synthesis += ` Validation: ${assumption.validationMethod}\n`;
@@ -20,6 +20,7 @@ import { getChallengerModels } from '../config/model-defaults.js';
20
20
  import { createProgressStream } from '../utils/progress-stream.js';
21
21
  import { smartAPIClient } from '../utils/smart-api-client.js';
22
22
  import { getSmartTimeout } from '../config/timeout-config.js';
23
+ import { EMOJI_PALETTE } from '../utils/format-constants.js';
23
24
  import { execSync } from 'child_process';
24
25
  // import {
25
26
  // renderTable,
@@ -755,7 +756,7 @@ Write concrete, specific analysis. Do NOT include brackets or placeholders.`;
755
756
  lines.push('');
756
757
  const challengesTableData = challenges.map((ch, i) => {
757
758
  const claim = claims.find(c => c.id === ch.claimId);
758
- const severityIcon = ch.severity === 'high' ? '🔴' : ch.severity === 'medium' ? '🟡' : '🟢';
759
+ const severityIcon = ch.severity === 'high' ? EMOJI_PALETTE.bad : ch.severity === 'medium' ? EMOJI_PALETTE.warn : EMOJI_PALETTE.good;
759
760
  return {
760
761
  '#': String(i + 1),
761
762
  Severity: `${severityIcon} ${ch.severity}`,
@@ -768,7 +769,7 @@ Write concrete, specific analysis. Do NOT include brackets or placeholders.`;
768
769
  // Show full challenges below
769
770
  challenges.forEach((ch, i) => {
770
771
  const claim = claims.find(c => c.id === ch.claimId);
771
- const severityIcon = ch.severity === 'high' ? '🔴' : ch.severity === 'medium' ? '🟡' : '🟢';
772
+ const severityIcon = ch.severity === 'high' ? EMOJI_PALETTE.bad : ch.severity === 'medium' ? EMOJI_PALETTE.warn : EMOJI_PALETTE.good;
772
773
  lines.push(`${severityIcon} **Challenge #${i + 1}** (${ch.severity} severity)`);
773
774
  lines.push(`Original: "${claim?.text}"`);
774
775
  lines.push('');
@@ -863,9 +864,9 @@ Write concrete, specific analysis. Do NOT include brackets or placeholders.`;
863
864
  lines.push('');
864
865
  lines.push(renderKeyValueTable({
865
866
  'Total Challenges': String(challenges.length),
866
- '🔴 High Severity': String(highSeverity),
867
- '🟡 Medium Severity': String(mediumSeverity),
868
- '🟢 Low Severity': String(lowSeverity),
867
+ [EMOJI_PALETTE.bad + ' High Severity']: String(highSeverity),
868
+ [EMOJI_PALETTE.warn + ' Medium Severity']: String(mediumSeverity),
869
+ [EMOJI_PALETTE.good + ' Low Severity']: String(lowSeverity),
869
870
  'Alternatives': String(alternatives.length),
870
871
  'Groupthink Risk': groupthinkRisk,
871
872
  }));
@@ -25,7 +25,7 @@ const savedThemeVars = {
25
25
  RENDER_OUTPUT: process.env.RENDER_OUTPUT,
26
26
  TACHIBOT_THEME: process.env.TACHIBOT_THEME,
27
27
  };
28
- const envResult = dotenvConfig({
28
+ dotenvConfig({
29
29
  path: envPath,
30
30
  override: true // API keys from .env take priority
31
31
  });
@@ -52,7 +52,7 @@ import { z } from "zod";
52
52
  import { InstructionOrchestrator } from "./orchestrator-instructions.js";
53
53
  import { validateToolInput, sanitizeForLogging } from "./utils/input-validator.js";
54
54
  import { isToolEnabled, logToolConfiguration } from "./utils/tool-config.js";
55
- import { stripMarkdown } from "./utils/ansi-renderer.js";
55
+ import { renderOutput } from "./utils/ansi-renderer.js";
56
56
  import { getToolAnnotations } from "./utils/tool-annotations.js";
57
57
  import { truncateSmart } from "./utils/stream-distill.js";
58
58
  import { trackToolCall, inferModelFromTool, estimateTokens, isTrackingEnabled, getUsageSummary, getAllReposSummary, getStatsJson, resetStats } from "./utils/usage-tracker.js";
@@ -158,16 +158,17 @@ function safeAddTool(tool) {
158
158
  // Silently ignore tracking errors
159
159
  }
160
160
  }
161
- // Return clean plain text strip markdown formatting since Claude Code
162
- // doesn't render markdown in tool results (shows raw ** and ### as text)
161
+ // Apply render mode (sparse = badge + bold headers + stripped, etc.)
163
162
  if (typeof result === 'string') {
164
- let cleanText = stripMarkdown(result);
165
- // Safety net: cap at 25K chars to prevent Claude Code's 30K truncation
166
- const MAX_RESPONSE_CHARS = 25000;
167
- if (cleanText.length > MAX_RESPONSE_CHARS) {
168
- cleanText = truncateSmart(cleanText, MAX_RESPONSE_CHARS);
163
+ const model = inferModelFromTool(tool.name) || undefined;
164
+ // Truncate raw content BEFORE ANSI rendering prevents mid-escape corruption
165
+ let raw = result;
166
+ const MAX_RAW_CHARS = 24000;
167
+ if (raw.length > MAX_RAW_CHARS) {
168
+ raw = truncateSmart(raw, MAX_RAW_CHARS);
169
169
  }
170
- return { type: "text", text: cleanText };
170
+ const rendered = renderOutput(raw, { model, summary: tool.name });
171
+ return { type: "text", text: rendered };
171
172
  }
172
173
  return result;
173
174
  }
@@ -226,7 +227,7 @@ safeAddTool({
226
227
  }),
227
228
  execute: async (args, mcpContext) => {
228
229
  const { log } = mcpContext;
229
- let { query, mode = "simple", context, domain, tokenEfficient = false, rounds = 5, executeNow = true, models, temperature = 0.7, saveSession = true, maxTokensPerRound = 2000, pingPongStyle = "collaborative" } = args;
230
+ let { query, mode = "simple", context, domain, tokenEfficient = false, rounds = 5, executeNow: _executeNow = true, models, temperature = 0.7, saveSession: _saveSession = true, maxTokensPerRound: _maxTokensPerRound = 2000, pingPongStyle: _pingPongStyle = "collaborative" } = args;
230
231
  // Validate and sanitize input
231
232
  const queryValidation = validateToolInput(query);
232
233
  if (!queryValidation.valid) {
@@ -332,8 +333,7 @@ TIPS FOR EFFECTIVE SYNTHESIS:
332
333
  Ready to help synthesize your collective intelligence results!`;
333
334
  default: // simple mode
334
335
  // BigText header disabled - plain text only
335
- const focusHeader = '';
336
- const focusBadge = '';
336
+ // BigText header/badge removed — plain text only
337
337
  return `Enhanced reasoning for: "${query}"
338
338
  ${context ? `Context: ${context}` : ''}
339
339
 
@@ -458,8 +458,7 @@ MemoryProvider: Pluggable memory (devlog, mem0, custom). Set TACHIBOT_MEMORY_PRO
458
458
  });
459
459
  // Build response with model output if available
460
460
  // BigText header disabled - plain text only
461
- const thinkBadge = '';
462
- const thinkHeader = '';
461
+ // BigText header/badge removed — plain text only
463
462
  let response = '';
464
463
  if (result.modelResponse) {
465
464
  response += `## Model Response (${args.model}):\n\n${result.modelResponse}\n\n---\n\n`;
@@ -691,7 +690,7 @@ async function initializeServer() {
691
690
  console.error("✅ Server.start() called successfully");
692
691
  // Keep the process alive with a heartbeat
693
692
  // This ensures the server doesn't exit prematurely
694
- const heartbeatInterval = setInterval(() => {
693
+ setInterval(() => {
695
694
  // Heartbeat to keep process alive
696
695
  // Log every 30 seconds to show we're still alive
697
696
  const now = new Date().toISOString();
@@ -5,6 +5,7 @@ import { Architect } from "../modes/architect.js";
5
5
  import { CodeReviewer } from "../modes/code-reviewer.js";
6
6
  import { DocumentationWriter } from "../modes/documentation-writer.js";
7
7
  import { TestArchitect } from "../modes/test-architect.js";
8
+ import { readFilesIntoContext } from "../utils/file-reader.js";
8
9
  // Workflow tools removed - using workflow-runner.ts instead
9
10
  const auditor = new Auditor();
10
11
  const commitGuardian = new CommitGuardian();
@@ -18,11 +19,15 @@ export const auditorTool = {
18
19
  description: "Evidence-based audit. Put the CONTEXT in the 'context' parameter.",
19
20
  parameters: z.object({
20
21
  context: z.string().describe("What to audit (REQUIRED - put your audit request here)"),
22
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
21
23
  evidenceRequired: z.boolean().optional().describe("Require evidence for claims")
22
24
  }),
23
25
  execute: async (args, { log }) => {
24
26
  log.info("Starting audit");
25
- const result = await auditor.audit(args.context, {
27
+ const fileContext = args.files?.length
28
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
29
+ : "";
30
+ const result = await auditor.audit(args.context + fileContext, {
26
31
  evidenceRequired: args.evidenceRequired
27
32
  });
28
33
  log.info("Audit complete", {
@@ -38,6 +43,7 @@ export const commitGuardianTool = {
38
43
  description: "Pre-commit validation. Put the CONTEXT in the 'context' parameter.",
39
44
  parameters: z.object({
40
45
  context: z.string().describe("Code changes to validate (REQUIRED - put your diff/changes here)"),
46
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
41
47
  strict: z.boolean().optional().describe("Use strict validation rules"),
42
48
  checkSecurity: z.boolean().optional().describe("Check for security issues"),
43
49
  checkQuality: z.boolean().optional().describe("Check code quality"),
@@ -45,7 +51,10 @@ export const commitGuardianTool = {
45
51
  }),
46
52
  execute: async (args, { log }) => {
47
53
  log.info("Validating commit");
48
- const result = await commitGuardian.validate(args.context, {
54
+ const fileContext = args.files?.length
55
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
56
+ : "";
57
+ const result = await commitGuardian.validate(args.context + fileContext, {
49
58
  strict: args.strict,
50
59
  checkSecurity: args.checkSecurity,
51
60
  checkQuality: args.checkQuality,
@@ -66,6 +75,7 @@ export const architectTool = {
66
75
  parameters: z.object({
67
76
  query: z.string().describe("What to analyze in the codebase (REQUIRED - put your question here)"),
68
77
  path: z.string().optional().describe("Path to the codebase to analyze"),
78
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
69
79
  depth: z.enum(["shallow", "normal", "deep"])
70
80
  .optional()
71
81
  .describe("Analysis depth - must be one of: shallow, normal, deep"),
@@ -76,7 +86,10 @@ export const architectTool = {
76
86
  depth: args.depth || "normal",
77
87
  path: args.path
78
88
  });
79
- const result = await architect.analyze(args.query, {
89
+ const fileContext = args.files?.length
90
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
91
+ : "";
92
+ const result = await architect.analyze(args.query + fileContext, {
80
93
  path: args.path,
81
94
  depth: args.depth,
82
95
  focusAreas: args.focusAreas
@@ -98,6 +111,7 @@ export const codeReviewerTool = {
98
111
  parameters: z.object({
99
112
  code: z.string().describe("The actual source code to review (REQUIRED - put your code here)"),
100
113
  language: z.string().optional().describe("Programming language (e.g., 'typescript', 'python')"),
114
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
101
115
  focusAreas: z.array(z.enum(['security', 'performance', 'readability', 'bugs', 'best-practices']))
102
116
  .optional()
103
117
  .describe("Focus areas - array of: security, performance, readability, bugs, best-practices"),
@@ -111,7 +125,10 @@ export const codeReviewerTool = {
111
125
  language: args.language || 'auto-detect',
112
126
  focusAreas: args.focusAreas || 'all'
113
127
  });
114
- const result = await codeReviewer.review(args.code, {
128
+ const fileContext = args.files?.length
129
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
130
+ : "";
131
+ const result = await codeReviewer.review(args.code + fileContext, {
115
132
  language: args.language,
116
133
  focusAreas: args.focusAreas,
117
134
  severity: args.severity,
@@ -131,6 +148,7 @@ export const documentationWriterTool = {
131
148
  description: "Documentation generation. Put the CODE in the 'code' parameter.",
132
149
  parameters: z.object({
133
150
  code: z.string().describe("The source code to document (REQUIRED - put your code here)"),
151
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
134
152
  style: z.enum(['narrative', 'technical', 'beginner-friendly', 'api-reference'])
135
153
  .optional()
136
154
  .describe("Documentation style - must be one of: narrative, technical, beginner-friendly, api-reference"),
@@ -145,7 +163,10 @@ export const documentationWriterTool = {
145
163
  style: args.style || 'narrative',
146
164
  format: args.format || 'markdown'
147
165
  });
148
- const result = await documentationWriter.generateDocs(args.code, {
166
+ const fileContext = args.files?.length
167
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
168
+ : "";
169
+ const result = await documentationWriter.generateDocs(args.code + fileContext, {
149
170
  style: args.style,
150
171
  includeExamples: args.includeExamples,
151
172
  generateToc: args.generateToc,
@@ -166,6 +187,7 @@ export const testArchitectTool = {
166
187
  description: "Test suite design. Put the CODE in the 'code' parameter.",
167
188
  parameters: z.object({
168
189
  code: z.string().describe("The source code to create tests for (REQUIRED - put your code here)"),
190
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
169
191
  testFramework: z.enum(['jest', 'mocha', 'vitest', 'cypress', 'playwright'])
170
192
  .optional()
171
193
  .describe("Test framework - must be one of: jest, mocha, vitest, cypress, playwright"),
@@ -183,7 +205,10 @@ export const testArchitectTool = {
183
205
  testTypes: args.testTypes || ['unit', 'integration', 'e2e'],
184
206
  coverage: args.coverage || 'thorough'
185
207
  });
186
- const result = await testArchitect.architectTests(args.code, {
208
+ const fileContext = args.files?.length
209
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
210
+ : "";
211
+ const result = await testArchitect.architectTests(args.code + fileContext, {
187
212
  testFramework: args.testFramework,
188
213
  testTypes: args.testTypes,
189
214
  coverage: args.coverage,
@@ -11,6 +11,7 @@ import { stripFormatting } from "../utils/format-stripper.js";
11
11
  import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
12
12
  import { withHeartbeat } from "../utils/streaming-helper.js";
13
13
  import { getTimeoutConfig } from "../config/timeout-config.js";
14
+ import { readFilesIntoContext } from "../utils/file-reader.js";
14
15
  // Note: renderOutput is applied centrally in server.ts safeAddTool() - no need to import here
15
16
  // NOTE: dotenv is loaded in server.ts before any imports
16
17
  // No need to reload here - just read from process.env
@@ -171,7 +172,8 @@ export const geminiQueryTool = {
171
172
  model: z.enum(["gemini-3", "pro", "flash"])
172
173
  .optional()
173
174
  .default("gemini-3")
174
- .describe("Model variant - must be one of: gemini-3 (default), pro, flash")
175
+ .describe("Model variant - must be one of: gemini-3 (default), pro, flash"),
176
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE.")
175
177
  }),
176
178
  execute: async (args, { log, reportProgress }) => {
177
179
  let model = GEMINI_MODELS.GEMINI_3_PRO; // Default to Gemini 3
@@ -182,8 +184,9 @@ export const geminiQueryTool = {
182
184
  model = GEMINI_MODELS.PRO;
183
185
  }
184
186
  // Skip validation - queries may contain code or LLM-generated content
187
+ const fileContext = args.files?.length ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}` : "";
185
188
  const reportFn = reportProgress ?? (async () => { });
186
- const result = await withHeartbeat(() => callGemini(args.prompt, model, undefined, 0.7, 'llm-orchestration'), reportFn);
189
+ const result = await withHeartbeat(() => callGemini(args.prompt + fileContext, model, undefined, 0.7, 'llm-orchestration'), reportFn);
187
190
  return stripFormatting(result);
188
191
  }
189
192
  };
@@ -200,7 +203,8 @@ export const geminiBrainstormTool = {
200
203
  parameters: z.object({
201
204
  prompt: z.string().describe("The ideas or topic to organize and refine (REQUIRED - put raw ideas or topic here)"),
202
205
  claudeThoughts: z.string().optional().describe("Claude's initial thoughts or raw ideas to cluster and refine"),
203
- maxClusters: z.number().optional().default(5).describe("Number of idea clusters to create (default: 5)")
206
+ maxClusters: z.number().optional().default(5).describe("Number of idea clusters to create (default: 5)"),
207
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE.")
204
208
  }),
205
209
  execute: async (args, { log, reportProgress }) => {
206
210
  const systemPrompt = `Convergent synthesis engine. Output consumed by automated toolchain.
@@ -226,8 +230,11 @@ Per cluster: Name | Score (1-10) | Top ideas (ranked) | Key insight
226
230
  Final: Which cluster has highest expected value and why. State the meta-pattern.
227
231
  No preamble. Structured output only.
228
232
  ${FORMAT_INSTRUCTION}`;
233
+ const fileContext = args.files?.length
234
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
235
+ : "";
229
236
  const reportFn = reportProgress ?? (async () => { });
230
- const response = await withHeartbeat(() => callGemini(args.prompt, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.7, 'llm-orchestration'), reportFn);
237
+ const response = await withHeartbeat(() => callGemini(args.prompt + fileContext, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.7, 'llm-orchestration'), reportFn);
231
238
  return stripFormatting(response);
232
239
  }
233
240
  };
@@ -241,6 +248,7 @@ export const geminiAnalyzeCodeTool = {
241
248
  parameters: z.object({
242
249
  code: z.string().describe("The actual source code to analyze (REQUIRED - put your code here)"),
243
250
  language: z.string().optional().describe("Programming language (e.g., 'typescript', 'python')"),
251
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
244
252
  focus: z.string().optional().default("general").describe("Analysis focus (e.g., quality, security, performance, bugs, general)")
245
253
  }),
246
254
  execute: async (args, { log, reportProgress }) => {
@@ -255,9 +263,12 @@ export const geminiAnalyzeCodeTool = {
255
263
  const systemPrompt = `Expert code reviewer. ${args.language || ''} code.
256
264
  ${focusText}.
257
265
  ${FORMAT_INSTRUCTION}`;
266
+ const fileContext = args.files?.length
267
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
268
+ : "";
258
269
  // Skip validation - code analysis naturally contains patterns that trigger false positives
259
270
  const reportFn = reportProgress ?? (async () => { });
260
- const result = await withHeartbeat(() => callGemini(`Analyze this code:\n\n\`\`\`${args.language || ''}\n${args.code}\n\`\`\``, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
271
+ const result = await withHeartbeat(() => callGemini(`Analyze this code:\n\n\`\`\`${args.language || ''}\n${args.code}\n\`\`\`${fileContext}`, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
261
272
  return stripFormatting(result);
262
273
  }
263
274
  };
@@ -270,6 +281,7 @@ export const geminiAnalyzeTextTool = {
270
281
  description: "Rhetorical analysis: dissect arguments for bias, logical fallacies, and persuasion tactics. Use for evaluating claims, detecting manipulation, or understanding argument structure. Put the TEXT in the 'text' parameter.",
271
282
  parameters: z.object({
272
283
  text: z.string().describe("The text to analyze (REQUIRED - put your text here)"),
284
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
273
285
  type: z.string()
274
286
  .optional()
275
287
  .default("rhetoric")
@@ -306,8 +318,11 @@ OUTPUT:
306
318
  ${analysisText}
307
319
  No preamble. Structured output only.
308
320
  ${FORMAT_INSTRUCTION}`;
321
+ const fileContext = args.files?.length
322
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
323
+ : "";
309
324
  const reportFn = reportProgress ?? (async () => { });
310
- const result = await withHeartbeat(() => callGemini(`Analyze this text:\n\n${args.text}`, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
325
+ const result = await withHeartbeat(() => callGemini(`Analyze this text:\n\n${args.text}${fileContext}`, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
311
326
  return stripFormatting(result);
312
327
  }
313
328
  };
@@ -327,7 +342,8 @@ export const geminiSummarizeTool = {
327
342
  format: z.enum(["paragraph", "bullet-points", "outline"])
328
343
  .optional()
329
344
  .default("paragraph")
330
- .describe("Output format - must be one of: paragraph, bullet-points, outline")
345
+ .describe("Output format - must be one of: paragraph, bullet-points, outline"),
346
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE.")
331
347
  }),
332
348
  execute: async (args, { log, reportProgress }) => {
333
349
  const lengthGuides = {
@@ -349,8 +365,9 @@ Focus on:
349
365
  - Conclusions and implications
350
366
  ${FORMAT_INSTRUCTION}`;
351
367
  // Skip validation for internal summarization calls
368
+ const fileContext = args.files?.length ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}` : "";
352
369
  const reportFn = reportProgress ?? (async () => { });
353
- const result = await withHeartbeat(() => callGemini(`Summarize this content:\n\n${args.content}`, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
370
+ const result = await withHeartbeat(() => callGemini(`Summarize this content:\n\n${args.content}` + fileContext, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
354
371
  return stripFormatting(result);
355
372
  }
356
373
  };
@@ -411,7 +428,8 @@ export const geminiJudgeTool = {
411
428
  mode: z.enum(["synthesize", "evaluate", "rank", "resolve"])
412
429
  .optional()
413
430
  .default("synthesize")
414
- .describe("Judge mode: synthesize (merge best), evaluate (score each), rank (order by quality), resolve (settle conflicts)")
431
+ .describe("Judge mode: synthesize (merge best), evaluate (score each), rank (order by quality), resolve (settle conflicts)"),
432
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE.")
415
433
  }),
416
434
  execute: async (args, { log, reportProgress }) => {
417
435
  // Resolve perspectives from fallback params (AI clients sometimes use wrong param name)
@@ -475,8 +493,11 @@ ${FORMAT_INSTRUCTION}`;
475
493
  const userPrompt = args.question
476
494
  ? `QUESTION: ${args.question}\n\nPERSPECTIVES TO JUDGE:\n${args.perspectives}`
477
495
  : args.perspectives;
496
+ const fileContext = args.files?.length
497
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
498
+ : "";
478
499
  const reportFn = reportProgress ?? (async () => { });
479
- const result = await withHeartbeat(() => callGemini(userPrompt, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
500
+ const result = await withHeartbeat(() => callGemini(userPrompt + fileContext, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
480
501
  return stripFormatting(result);
481
502
  }
482
503
  };
@@ -7,6 +7,7 @@ import { config } from "dotenv";
7
7
  import * as path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { getGrokApiKey, hasGrokApiKey } from "../utils/api-keys.js";
10
+ import { readFilesIntoContext } from "../utils/file-reader.js";
10
11
  import { link } from "../utils/ansi-renderer.js";
11
12
  import { stripFormatting } from "../utils/format-stripper.js";
12
13
  import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
@@ -222,10 +223,11 @@ export const grokReasonEnhanced = {
222
223
  context: z.string().optional(),
223
224
  useHeavy: z.boolean().optional(),
224
225
  enableLiveSearch: z.boolean().optional(),
225
- maxSteps: z.number().optional()
226
+ maxSteps: z.number().optional(),
227
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE.")
226
228
  }),
227
229
  execute: async (args, { log }) => {
228
- const { problem, approach = "first-principles", context, useHeavy = false, enableLiveSearch = false, maxSteps = 5 } = args;
230
+ const { problem, approach = "first-principles", context, useHeavy = false, enableLiveSearch = false, maxSteps = 5, files } = args;
229
231
  const approachPrompts = {
230
232
  analytical: "Break down the problem systematically and analyze each component",
231
233
  creative: "Think outside the box and consider unconventional solutions",
@@ -233,6 +235,7 @@ export const grokReasonEnhanced = {
233
235
  "first-principles": "Break down to fundamental truths and build up from there",
234
236
  "multi-agent": "Consider multiple perspectives and synthesize them"
235
237
  };
238
+ const fileContext = files?.length ? `\n\nSOURCE CODE:\n${readFilesIntoContext(files)}` : "";
236
239
  const messages = [
237
240
  {
238
241
  role: "system",
@@ -245,7 +248,7 @@ ${FORMAT_INSTRUCTION}`
245
248
  },
246
249
  {
247
250
  role: "user",
248
- content: problem
251
+ content: problem + fileContext
249
252
  }
250
253
  ];
251
254
  const modelName = useHeavy ? 'Grok-4-Heavy' : 'Grok-4.1';
@@ -13,6 +13,7 @@ import { stripFormatting } from "../utils/format-stripper.js";
13
13
  import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
14
14
  import { tryOpenRouterGateway, isGatewayEnabled } from "../utils/openrouter-gateway.js";
15
15
  import { withHeartbeat } from "../utils/streaming-helper.js";
16
+ import { readFilesIntoContext } from "../utils/file-reader.js";
16
17
  // Note: renderOutput is applied centrally in server.ts safeAddTool() - no need to import here
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = path.dirname(__filename);
@@ -130,6 +131,7 @@ export const grokReasonTool = {
130
131
  .optional()
131
132
  .describe("Reasoning approach (e.g., analytical, creative, systematic, first-principles)"),
132
133
  context: z.string().optional().describe("Additional context for the problem"),
134
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
133
135
  useHeavy: z.boolean().optional().describe("Use expensive Grok 4 Heavy model ($3/$15) for complex tasks")
134
136
  }),
135
137
  execute: async (args, { log, reportProgress }) => {
@@ -140,6 +142,9 @@ export const grokReasonTool = {
140
142
  systematic: "Follow a step-by-step logical process",
141
143
  "first-principles": "Break down to fundamental truths and build up from there"
142
144
  };
145
+ const fileContext = args.files?.length
146
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
147
+ : "";
143
148
  const messages = [
144
149
  {
145
150
  role: "system",
@@ -150,7 +155,7 @@ ${FORMAT_INSTRUCTION}`
150
155
  },
151
156
  {
152
157
  role: "user",
153
- content: problem
158
+ content: problem + fileContext
154
159
  }
155
160
  ];
156
161
  // Use GROK_4_1_FAST_REASONING by default (latest with enhanced reasoning!), GROK_4_HEAVY only if explicitly requested
@@ -175,6 +180,7 @@ export const grokCodeTool = {
175
180
  .describe("Code task (e.g., analyze, optimize, debug, review, refactor)"),
176
181
  code: z.string().describe("The actual source code to analyze (REQUIRED - put your code here)"),
177
182
  language: z.string().optional().describe("Programming language (e.g., 'typescript', 'python')"),
183
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
178
184
  requirements: z.string().optional().describe("Specific requirements or focus areas")
179
185
  }),
180
186
  execute: async (args, { log, reportProgress }) => {
@@ -186,6 +192,9 @@ export const grokCodeTool = {
186
192
  review: "Review this code for best practices and improvements",
187
193
  refactor: "Refactor this code for better maintainability and clarity"
188
194
  };
195
+ const fileContext = args.files?.length
196
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
197
+ : "";
189
198
  const messages = [
190
199
  {
191
200
  role: "system",
@@ -197,7 +206,7 @@ ${FORMAT_INSTRUCTION}`
197
206
  },
198
207
  {
199
208
  role: "user",
200
- content: `Code:\n\`\`\`${language || ''}\n${code}\n\`\`\``
209
+ content: `Code:\n\`\`\`${language || ''}\n${code}\n\`\`\`` + fileContext
201
210
  }
202
211
  ];
203
212
  log?.info(`Using Grok 4.1 Fast Non-Reasoning (2M context, tool-calling optimized, $0.20/$0.50)`);
@@ -218,6 +227,7 @@ export const grokDebugTool = {
218
227
  issue: z.string().describe("Description of the issue or bug (REQUIRED - put your problem here)"),
219
228
  code: z.string().optional().describe("Relevant code that has the issue"),
220
229
  error: z.string().optional().describe("Error message or stack trace"),
230
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
221
231
  context: z.string().optional().describe("Additional context about the environment or conditions")
222
232
  }),
223
233
  execute: async (args, { log, reportProgress }) => {
@@ -232,6 +242,9 @@ export const grokDebugTool = {
232
242
  if (context) {
233
243
  prompt += `\nContext: ${context}\n`;
234
244
  }
245
+ const fileContext = args.files?.length
246
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
247
+ : "";
235
248
  const messages = [
236
249
  {
237
250
  role: "system",
@@ -245,7 +258,7 @@ ${FORMAT_INSTRUCTION}`
245
258
  },
246
259
  {
247
260
  role: "user",
248
- content: prompt
261
+ content: prompt + fileContext
249
262
  }
250
263
  ];
251
264
  log?.info(`Using Grok 4.1 Fast Non-Reasoning for debugging (tool-calling optimized, $0.20/$0.50)`);
@@ -267,10 +280,14 @@ export const grokArchitectTool = {
267
280
  constraints: z.string().optional().describe("Technical or business constraints to consider"),
268
281
  scale: z.string()
269
282
  .optional()
270
- .describe("Expected scale (e.g., small, medium, large, enterprise)")
283
+ .describe("Expected scale (e.g., small, medium, large, enterprise)"),
284
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
271
285
  }),
272
286
  execute: async (args, { log, reportProgress }) => {
273
287
  const { requirements, constraints, scale } = args;
288
+ const fileContext = args.files?.length
289
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
290
+ : "";
274
291
  const messages = [
275
292
  {
276
293
  role: "system",
@@ -282,7 +299,7 @@ ${FORMAT_INSTRUCTION}`
282
299
  },
283
300
  {
284
301
  role: "user",
285
- content: requirements
302
+ content: requirements + fileContext
286
303
  }
287
304
  ];
288
305
  log?.info(`Using Grok 4.1 Fast Reasoning for architecture (latest model, $0.20/$0.50)`);
@@ -303,10 +320,14 @@ export const grokBrainstormTool = {
303
320
  topic: z.string().describe("The topic to brainstorm about (REQUIRED - put your idea/topic here)"),
304
321
  constraints: z.string().optional().describe("Any constraints or requirements to consider"),
305
322
  numIdeas: z.number().optional().describe("Number of radical rebuilds to generate (default: 5)"),
306
- forceHeavy: z.boolean().optional().describe("Use expensive Grok 4 Heavy model ($3/$15) for deeper creativity")
323
+ forceHeavy: z.boolean().optional().describe("Use expensive Grok 4 Heavy model ($3/$15) for deeper creativity"),
324
+ files: z.array(z.string()).optional().describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
307
325
  }),
308
326
  execute: async (args, { log, reportProgress }) => {
309
327
  const { topic, constraints, numIdeas = 5, forceHeavy = false } = args;
328
+ const fileContext = args.files?.length
329
+ ? `\n\nSOURCE CODE:\n${readFilesIntoContext(args.files)}`
330
+ : "";
310
331
  const messages = [
311
332
  {
312
333
  role: "system",
@@ -332,7 +353,7 @@ ${FORMAT_INSTRUCTION}`
332
353
  },
333
354
  {
334
355
  role: "user",
335
- content: topic
356
+ content: topic + fileContext
336
357
  }
337
358
  ];
338
359
  const model = forceHeavy ? GrokModel.GROK_4_HEAVY : GrokModel.GROK_4_1_FAST_REASONING;