tachibot-mcp 2.22.0 → 2.23.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,13 @@ 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.23.0] - 2026-06-11
9
+
10
+ ### Changed
11
+ - **Tool registration via central scan registry** — merged the tool-standardization refactor (PR #5): all provider tools register through `src/tools/registry.ts` + `defineModelTool` factory, with golden wire-contract tests (57 tool schemas snapshot-locked) and a plop `add-tool` generator. No behavior change; the registered tool surface is byte-identical.
12
+ - **Jury: removed the `hermes` juror** — it was a persona variant of `local` calling the same `LOCAL_LLM_MODEL` weights while claiming "You are Hermes" (a false-role prompt). Council-reviewed rationale: jury independence comes from different model weights, not different system prompts on the same backend. `hermes` is kept as a **legacy alias** of `local`; panels are deduped after alias mapping, so `jurors: "hermes,local"` now yields one local vote instead of two correlated ones. 12 jurors total.
13
+ - Docs now describe the Hermes connection honestly (verified via grok_search against Nous docs/GitHub): the local juror runs whatever `LOCAL_LLM_MODEL` points at, e.g. a Nous Hermes build via Ollama. The Hermes *agent* is model-agnostic — it consumes 300+ backends (GPT, Claude, Gemini, self-hosted Ollama/vLLM); it is not an OpenAI-compatible endpoint to point `LOCAL_LLM_BASE_URL` at.
14
+
8
15
  ## [2.22.0] - 2026-06-10
9
16
 
10
17
  ### Added
package/CONTRIBUTING.md CHANGED
@@ -47,6 +47,10 @@ Use the issue templates and include:
47
47
  - Open a [Discussion](https://github.com/pavveu/tachibot-mcp/discussions)
48
48
  - Create an issue
49
49
 
50
+ ## Adding a tool
51
+
52
+ Run `npm run add-tool` and answer the prompts (provider, wire name, description). It scaffolds a `defineModelTool(...)` in the provider file at the `// plop:tools`/`// plop:register` anchors and a test stub. Fill in the `parameters` and `execute` TODOs. The emitted-schema golden test (`npm run test:golden`) guards the tool-name/schema wire contract.
53
+
50
54
  ## License
51
55
 
52
56
  By submitting code to this project, you agree that your contributions will be licensed under the AGPL-3.0 license and you grant Pawel Pawlowski the right to relicense your contributions under alternative licenses (including commercial licenses).
package/README.md CHANGED
@@ -202,7 +202,7 @@ See [Installation Guide](docs/INSTALLATION_BOTH.md) for detailed instructions.
202
202
  `list_prompt_techniques` · `preview_prompt_technique` · `execute_prompt_technique`
203
203
 
204
204
  ### Local Models (1)
205
- `local_query` — any OpenAI-compatible local server (Ollama / LM Studio / llama.cpp / vLLM). Zero-cost, offline, private; also available as `hermes`/`local` jury jurors.
205
+ `local_query` — any OpenAI-compatible local server (Ollama / LM Studio / llama.cpp / vLLM). Zero-cost, offline, private; also available as the `local` jury juror (`hermes` is accepted as a legacy alias). Runs whatever `LOCAL_LLM_MODEL` points at — e.g. a Nous Hermes build (`ollama pull hermes3`). Note the [Hermes agent](https://hermes-agent.nousresearch.com) itself is model-agnostic — it runs on 300+ backends (GPT, Claude, Gemini, DeepSeek, or self-hosted Ollama/vLLM) — so "Hermes" was never a guarantee of distinct weights.
206
206
 
207
207
  ### Advanced Modes (bonus)
208
208
  - **Challenger** — Critical analysis with multi-model fact-checking
@@ -65,16 +65,14 @@ import { getProviderInfo } from "./tools/unified-ai-provider.js";
65
65
  import { getAllPerplexityTools, isPerplexityAvailable } from "./tools/perplexity-tools.js";
66
66
  import { getAllGrokTools, isGrokAvailable } from "./tools/grok-tools.js";
67
67
  import { registerWorkflowTools } from "./tools/workflow-runner.js";
68
- import { validateWorkflowTool, validateWorkflowFileTool } from "./tools/workflow-validator-tool.js";
68
+ import { getAllTools } from "./tools/registry.js";
69
69
  import { canRunFocusDeep } from "./focus-deep.js";
70
70
  import { loadConfig } from "./config.js";
71
71
  // import { registerSessionTools } from "./session/session-tools.js"; // Removed - not needed for minimal tool set
72
72
  import { getAllAdvancedTools, areAdvancedModesAvailable } from "./tools/advanced-modes.js";
73
73
  import { isOpenAIAvailable, getAllOpenAITools } from "./tools/openai-tools.js";
74
- import { isGeminiAvailable, geminiBrainstormTool, geminiAnalyzeCodeTool } from "./tools/gemini-tools.js";
74
+ import { isGeminiAvailable } from "./tools/gemini-tools.js";
75
75
  import { isOpenRouterAvailable } from "./tools/openrouter-tools.js";
76
- import { getTachiTools } from "./tools/tachi-tool.js";
77
- import { getPromptTechniqueTools } from "./tools/prompt-technique-tools.js";
78
76
  import { withParamAliases } from "./utils/param-aliases.js";
79
77
  // import { registerGPT5Tools, isGPT5Available } from "./tools/openai-gpt5-fixed.js"; // DISABLED - using regular openai-tools.ts
80
78
  import { initializeOptimizations } from "./optimization/index.js";
@@ -553,114 +551,27 @@ const availableProviders = Object.entries(providerInfo)
553
551
  .filter(([_, info]) => info.available)
554
552
  .map(([name]) => name);
555
553
  console.error(`✅ Available AI providers: ${availableProviders.join(', ')}`);
556
- // Register Perplexity tools separately (custom API, not OpenAI-compatible)
557
- if (isPerplexityAvailable()) {
558
- const perplexityTools = getAllPerplexityTools();
559
- perplexityTools.forEach(tool => {
560
- safeAddTool(tool);
561
- });
562
- console.error(`✅ Registered ${perplexityTools.length} Perplexity tools (custom API)`);
563
- }
564
- // Register Grok tools separately (custom API, not OpenAI-compatible)
565
- if (isGrokAvailable()) {
566
- const grokTools = getAllGrokTools();
567
- grokTools.forEach(tool => {
568
- safeAddTool(tool);
569
- });
570
- console.error(`✅ Registered ${grokTools.length} Grok tools (custom API)`);
571
- }
572
- // Register all OpenAI tools (includes openai_reason, openai_brainstorm, etc.)
573
- if (isOpenAIAvailable()) {
574
- const openaiTools = getAllOpenAITools();
575
- openaiTools.forEach(tool => {
576
- safeAddTool(tool);
577
- });
578
- console.error(`✅ Registered ${openaiTools.length} OpenAI tools (GPT-5 suite)`);
579
- }
580
554
  // Async initialization function to handle dynamic imports and startup
581
555
  async function initializeServer() {
582
556
  try {
583
- // Register select Gemini tools (brainstorm, analyze, search)
584
- if (isGeminiAvailable()) {
585
- const { geminiAnalyzeTextTool, geminiJudgeTool, geminiSearchTool } = await import("./tools/gemini-tools.js");
586
- const geminiTools = [
587
- geminiBrainstormTool, // Creative brainstorming
588
- geminiAnalyzeCodeTool, // Code analysis
589
- geminiAnalyzeTextTool, // Text analysis (sentiment, summary, etc.)
590
- geminiJudgeTool, // Multi-perspective evaluation & synthesis
591
- geminiSearchTool // Web search with Google Search grounding
592
- ];
593
- geminiTools.forEach(tool => {
594
- safeAddTool(tool);
595
- });
596
- console.error(`✅ Registered ${geminiTools.length} Gemini tools (brainstorm, code, text, judge, search)`);
597
- // Register Jury tool (multi-model panel with Gemini judge)
598
- const { juryTool } = await import("./tools/jury-tool.js");
599
- safeAddTool(juryTool);
600
- console.error(`✅ Registered jury tool (multi-model panel)`);
601
- }
602
- // Register OpenRouter tools (Qwen, Kimi, MiniMax - filtered by profile via safeAddTool)
603
- if (isOpenRouterAvailable()) {
604
- const { qwenCoderTool, qwenAlgoTool, qwqReasoningTool, qwenCompetitiveTool, kimiThinkingTool, kimiCodeTool, kimiDecomposeTool, kimiLongContextTool, qwenReasonTool, minimaxCodeTool, minimaxAgentTool, deepseekReasonTool, deepseekAlgoTool, glmReasonTool, stepfunReasonTool, ernieReasonTool } = await import("./tools/openrouter-tools.js");
605
- // safeAddTool checks isToolEnabled internally
606
- safeAddTool(qwenCoderTool);
607
- safeAddTool(qwenAlgoTool);
608
- safeAddTool(qwqReasoningTool); // QwQ-32B - multi-perspective deliberation (free tier)
609
- safeAddTool(qwenCompetitiveTool);
610
- safeAddTool(kimiThinkingTool);
611
- safeAddTool(kimiCodeTool); // SWE-focused code (Kimi K2.5 - 76.8% SWE-Bench)
612
- safeAddTool(kimiDecomposeTool); // Task decomposition (Kimi K2.5 Agent Swarm)
613
- safeAddTool(kimiLongContextTool); // Long-context analysis (Kimi K2.5 - 256K)
614
- safeAddTool(qwenReasonTool); // Heavy reasoning (Qwen3-Max-Thinking >1T params)
615
- safeAddTool(minimaxCodeTool); // MiniMax M2.7 - SWE-Pro 56.22%, #1 AI Intelligence Index
616
- safeAddTool(minimaxAgentTool); // MiniMax M2.7 - agentic workflows, self-evolving
617
- safeAddTool(deepseekReasonTool); // DeepSeek V4 Pro - frontier reasoning/math (open-weight)
618
- safeAddTool(deepseekAlgoTool); // DeepSeek V4 Pro - algorithmic code review (top AIME/CodeElo)
619
- safeAddTool(glmReasonTool); // Zhipu GLM-5.1 - agentic reasoning (SWE-Bench Pro leader)
620
- safeAddTool(stepfunReasonTool); // StepFun Step 3.7 Flash - efficient reasoning
621
- safeAddTool(ernieReasonTool); // Baidu ERNIE 4.5 VL - broad-knowledge reasoning
622
- console.error(`✅ Registered OpenRouter tools (Qwen, QwQ, Kimi x4, MiniMax, DeepSeek x2, GLM, StepFun, ERNIE)`);
623
- // Register planner tools (multi-model council for plan creation/execution)
624
- const { plannerMakerTool, plannerRunnerTool, listPlansTool } = await import("./tools/planner-tools.js");
625
- safeAddTool(plannerMakerTool); // Council-based plan creation
626
- safeAddTool(plannerRunnerTool); // Plan execution with checkpoints
627
- safeAddTool(listPlansTool); // List recent plans
628
- console.error(`✅ Registered planner tools (planner_maker, planner_runner, list_plans)`);
557
+ // Register ALL provider tools via the central scan registry. getAllTools()
558
+ // evaluates the SAME is*Available() guards, in the SAME order, with the SAME
559
+ // async dynamic-import timing the per-provider blocks used to use inline here
560
+ // so the registered set is byte-identical across every API-key scenario.
561
+ // This single loop replaces the former Perplexity/Grok/OpenAI sync blocks and
562
+ // the Gemini+jury / OpenRouter+planner / local / workflow-validator /
563
+ // advanced / tachi / prompt-technique async blocks. The 5 inline tools
564
+ // (think/focus/nextThought/usage_stats/continue_focus) are registered above
565
+ // and are intentionally NOT returned by the registry, so there is no overlap.
566
+ for (const t of await getAllTools()) {
567
+ safeAddTool(t);
629
568
  }
630
- // Register local-model tools (Ollama / LM Studio / llama.cpp / vLLM - zero-cost,
631
- // offline, no API key). Registered unconditionally; profile membership is
632
- // enforced by safeAddTool/isToolEnabled.
633
- const { localQueryTool } = await import("./tools/local-tools.js");
634
- safeAddTool(localQueryTool);
635
- console.error(`✅ Registered local-model tools (local_query)`);
636
- // Register workflow tools
569
+ console.error(`✅ Registered provider tools via central registry`);
570
+ // Register workflow tools (registers directly onto FastMCP, not via
571
+ // safeAddTool intentionally OUT of the registry, kept in its original spot).
637
572
  registerWorkflowTools(server);
638
573
  console.error(`✅ Registered workflow tools (execute, list, create, visualize)`);
639
- // Register workflow validator tools
640
- safeAddTool(validateWorkflowTool);
641
- safeAddTool(validateWorkflowFileTool);
642
- console.error(`✅ Registered workflow validator tools`);
643
574
  // Session management tools removed - not needed for minimal TachiBot
644
- // Register advanced mode tools (Verifier, Challenger, Scout, etc.)
645
- if (areAdvancedModesAvailable()) {
646
- const advancedTools = getAllAdvancedTools();
647
- advancedTools.forEach(tool => {
648
- safeAddTool(tool);
649
- });
650
- console.error(`✅ Registered ${advancedTools.length} advanced mode tools`);
651
- }
652
- // Register tachi tools (smart auto-routing AI assistant)
653
- const tachiTools = getTachiTools();
654
- tachiTools.forEach(tool => {
655
- safeAddTool(tool);
656
- });
657
- console.error(`✅ Registered tachi tools (tachi, focus alias)`);
658
- // Register prompt technique tools (transparent prompt engineering)
659
- const promptTechniqueTools = getPromptTechniqueTools();
660
- promptTechniqueTools.forEach(tool => {
661
- safeAddTool(tool);
662
- });
663
- console.error(`✅ Registered ${promptTechniqueTools.length} prompt technique tools`);
664
575
  // Log startup information
665
576
  const perplexityCount = isPerplexityAvailable() ? getAllPerplexityTools().length : 0;
666
577
  const grokCount = isGrokAvailable() ? getAllGrokTools().length : 0;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared Zod base schema partials — Task 0.3.
3
+ *
4
+ * Each export is a plain object meant to be SPREAD into a `z.object({...})`.
5
+ * Spreading creates a fresh `ZodObject` whose shape owns a copy of each
6
+ * field reference; two tools that spread the same partial do NOT alias a
7
+ * single mutable ZodObject — they each get an independent schema instance.
8
+ *
9
+ * SELECTION CRITERIA (grep-verified across all src/tools/*-tools.ts):
10
+ * A field is factored HERE only if its FULL definition
11
+ * (type + .describe() text + optionality) is **byte-identical** across
12
+ * every use site (outliers with different describe text stay inline).
13
+ *
14
+ * ─────────────────────────────────────────────────────────────────────────
15
+ * filesField
16
+ * 30 of 31 occurrences across gemini/grok/openai/openrouter/perplexity/
17
+ * planner tools share this exact definition. The one outlier
18
+ * (openrouter-tools.ts line ~758) has a different describe text and stays
19
+ * inline in its tool.
20
+ *
21
+ * reasoningContextField
22
+ * 7 occurrences across openai/openrouter/perplexity tools share this exact
23
+ * definition. The remaining ~7 context fields have different describe text
24
+ * (e.g. "Additional context for the problem", "Additional context about the
25
+ * environment or conditions", "Additional context") and stay inline.
26
+ *
27
+ * REJECTED CANDIDATES (non-identical definitions — do NOT factor here):
28
+ * temperature — 3 defs, all differ (defaults 0.4 vs 0.7, z.coerce.number
29
+ * vs z.number, different describe text).
30
+ * query — many defs with varying describe text across tools.
31
+ * problem — varying describe text.
32
+ * prompt — varying describe text.
33
+ * ─────────────────────────────────────────────────────────────────────────
34
+ */
35
+ import { z } from "zod";
36
+ /**
37
+ * `files` — file-paths-as-code-context field.
38
+ *
39
+ * Identical across 30 tool definitions (verified by grep, Task 0.3 recon).
40
+ * Spread into z.object({...filesField, ...}) to include this field.
41
+ */
42
+ export const filesField = {
43
+ files: z
44
+ .array(z.string())
45
+ .optional()
46
+ .describe("File paths to read as code context. Supports line ranges: 'src/foo.ts:100-200'. Model sees ACTUAL CODE."),
47
+ };
48
+ /**
49
+ * `context` — additional reasoning context field.
50
+ *
51
+ * Identical across 7 tool definitions in openai/openrouter/perplexity tools
52
+ * (verified by grep, Task 0.3 recon). Context fields with different describe
53
+ * text stay inline in their respective tools.
54
+ * Spread into z.object({...reasoningContextField, ...}) to include this field.
55
+ */
56
+ export const reasoningContextField = {
57
+ context: z
58
+ .string()
59
+ .optional()
60
+ .describe("Additional context for the reasoning task"),
61
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * PURE pass-through. Adds types + a single definition site — NO schema
3
+ * transformation, NO added .describe()/defaults. The emitted JSON schema MUST
4
+ * be unchanged. The body is exactly `return tool;` and must stay that way.
5
+ *
6
+ * IDENTITY-PRESERVING: the RETURN type is the concrete tool type `T`, not
7
+ * `ModelTool<S>`. Wrapping a tool is therefore type-transparent — the wrapped
8
+ * const keeps the *exact* concrete type it would have unwrapped (e.g.
9
+ * `execute`'s real `Promise<string>` return and its concrete input where
10
+ * `.default()` fields are still optional at the call site). Returning
11
+ * `ModelTool<S>` instead would widen `execute` to
12
+ * `(input: z.infer<S>, ctx) => Promise<unknown>` and break direct callers such
13
+ * as `src/tools/prompt-technique-tools.ts` that invoke `.execute(...)` outside
14
+ * FastMCP's parse step.
15
+ *
16
+ * Two type params are required for BOTH goals at once:
17
+ * - `S` is inferred from `tool.parameters` so the shape constraint's
18
+ * `execute` input is `z.infer<S>` — i.e. the tool's OWN output type, which
19
+ * its concrete `execute` is assignable to (contravariantly). Constraining
20
+ * to the generic `ModelTool<z.ZodObject<z.ZodRawShape>>` instead resolves
21
+ * the input to `{ [x: string]: any }` (an index signature with no required
22
+ * keys), to which a concrete `execute(input: { prompt: string })` is NOT
23
+ * assignable — so a single `T extends ModelTool<…>` param wrongly rejects
24
+ * every real tool.
25
+ * - `T extends ModelTool<S>` captures and returns the concrete tool type.
26
+ * The `ModelTool<S>` bound still validates the shape (rejects a missing
27
+ * `name`/`description`/`execute` or a non-`ZodObject` `parameters`).
28
+ *
29
+ * C3: a future tool whose top-level `parameters` uses `.refine()` /
30
+ * `.superRefine()` / `.transform()` would produce a `ZodEffects` (not a
31
+ * `ZodObject`) and would require widening the `S` constraint here. No such
32
+ * tool exists today (grep-confirmed), so the `z.ZodObject` constraint is
33
+ * correct for now.
34
+ */
35
+ export function defineModelTool(tool) {
36
+ return tool;
37
+ }
@@ -12,6 +12,8 @@ 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
14
  import { readFilesIntoContext } from "../utils/file-reader.js";
15
+ import { defineModelTool } from "./factory/define-model-tool.js";
16
+ import { filesField } from "./factory/base-schemas.js";
15
17
  // Note: renderOutput is applied centrally in server.ts safeAddTool() - no need to import here
16
18
  // NOTE: dotenv is loaded in server.ts before any imports
17
19
  // No need to reload here - just read from process.env
@@ -164,7 +166,7 @@ export async function callGemini(prompt, model = GEMINI_MODELS.GEMINI_3_PRO, sys
164
166
  * Gemini Query Tool
165
167
  * Direct querying of Gemini models for general information
166
168
  */
167
- export const geminiQueryTool = {
169
+ export const geminiQueryTool = defineModelTool({
168
170
  name: "gemini_query",
169
171
  description: "Query Gemini. Put your PROMPT in the 'prompt' parameter.",
170
172
  parameters: z.object({
@@ -173,7 +175,7 @@ export const geminiQueryTool = {
173
175
  .optional()
174
176
  .default("gemini-3")
175
177
  .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.")
178
+ ...filesField
177
179
  }),
178
180
  execute: async (args, { log, reportProgress }) => {
179
181
  let model = GEMINI_MODELS.GEMINI_3_PRO; // Default to Gemini 3
@@ -189,7 +191,7 @@ export const geminiQueryTool = {
189
191
  const result = await withHeartbeat(() => callGemini(args.prompt + fileContext, model, undefined, 0.7, 'llm-orchestration'), reportFn);
190
192
  return stripFormatting(result);
191
193
  }
192
- };
194
+ });
193
195
  /**
194
196
  * Gemini Brainstorm Tool
195
197
  * Collaborative problem-solving and ideation
@@ -197,14 +199,14 @@ export const geminiQueryTool = {
197
199
  * Note: skipValidation is used for internal LLM-to-LLM calls where input
198
200
  * has already been validated at the MCP entry point (server.ts).
199
201
  */
200
- export const geminiBrainstormTool = {
202
+ export const geminiBrainstormTool = defineModelTool({
201
203
  name: "gemini_brainstorm",
202
204
  description: "Convergent synthesis: cluster, refine, and prioritize raw ideas into structured hierarchies. Use AFTER divergent ideation to organize and rank ideas by impact/feasibility. Put your PROMPT in the 'prompt' parameter.",
203
205
  parameters: z.object({
204
206
  prompt: z.string().describe("The ideas or topic to organize and refine (REQUIRED - put raw ideas or topic here)"),
205
207
  claudeThoughts: z.string().optional().describe("Claude's initial thoughts or raw ideas to cluster and refine"),
206
208
  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.")
209
+ ...filesField
208
210
  }),
209
211
  execute: async (args, { log, reportProgress }) => {
210
212
  const systemPrompt = `Convergent synthesis engine. Output consumed by automated toolchain.
@@ -237,18 +239,18 @@ ${FORMAT_INSTRUCTION}`;
237
239
  const response = await withHeartbeat(() => callGemini(args.prompt + fileContext, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.7, 'llm-orchestration'), reportFn);
238
240
  return stripFormatting(response);
239
241
  }
240
- };
242
+ });
241
243
  /**
242
244
  * Gemini Analyze Code Tool
243
245
  * Code quality and performance analysis
244
246
  */
245
- export const geminiAnalyzeCodeTool = {
247
+ export const geminiAnalyzeCodeTool = defineModelTool({
246
248
  name: "gemini_analyze_code",
247
249
  description: "Analyze code for bugs, quality, security, or performance issues. Put the CODE in the 'code' parameter, NOT in 'focus'.",
248
250
  parameters: z.object({
249
251
  code: z.string().describe("The actual source code to analyze (REQUIRED - put your code here)"),
250
252
  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."),
253
+ ...filesField,
252
254
  focus: z.string().optional().default("general").describe("Analysis focus (e.g., quality, security, performance, bugs, general)")
253
255
  }),
254
256
  execute: async (args, { log, reportProgress }) => {
@@ -271,17 +273,17 @@ ${FORMAT_INSTRUCTION}`;
271
273
  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);
272
274
  return stripFormatting(result);
273
275
  }
274
- };
276
+ });
275
277
  /**
276
278
  * Gemini Analyze Text Tool
277
279
  * Text analysis and sentiment detection
278
280
  */
279
- export const geminiAnalyzeTextTool = {
281
+ export const geminiAnalyzeTextTool = defineModelTool({
280
282
  name: "gemini_analyze_text",
281
283
  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.",
282
284
  parameters: z.object({
283
285
  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."),
286
+ ...filesField,
285
287
  type: z.string()
286
288
  .optional()
287
289
  .default("rhetoric")
@@ -325,12 +327,12 @@ ${FORMAT_INSTRUCTION}`;
325
327
  const result = await withHeartbeat(() => callGemini(`Analyze this text:\n\n${args.text}${fileContext}`, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
326
328
  return stripFormatting(result);
327
329
  }
328
- };
330
+ });
329
331
  /**
330
332
  * Gemini Summarize Tool
331
333
  * Content summarization at different levels
332
334
  */
333
- export const geminiSummarizeTool = {
335
+ export const geminiSummarizeTool = defineModelTool({
334
336
  name: "gemini_summarize",
335
337
  description: "Summarization. Put the CONTENT in the 'content' parameter.",
336
338
  parameters: z.object({
@@ -343,7 +345,7 @@ export const geminiSummarizeTool = {
343
345
  .optional()
344
346
  .default("paragraph")
345
347
  .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.")
348
+ ...filesField
347
349
  }),
348
350
  execute: async (args, { log, reportProgress }) => {
349
351
  const lengthGuides = {
@@ -370,12 +372,12 @@ ${FORMAT_INSTRUCTION}`;
370
372
  const result = await withHeartbeat(() => callGemini(`Summarize this content:\n\n${args.content}` + fileContext, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
371
373
  return stripFormatting(result);
372
374
  }
373
- };
375
+ });
374
376
  /**
375
377
  * Gemini Image Prompt Tool
376
378
  * Create detailed prompts for image generation
377
379
  */
378
- export const geminiImagePromptTool = {
380
+ export const geminiImagePromptTool = defineModelTool({
379
381
  name: "gemini_image_prompt",
380
382
  description: "Image prompt generation. Put the DESCRIPTION in the 'description' parameter.",
381
383
  parameters: z.object({
@@ -407,7 +409,7 @@ ${args.details ? `Additional details: ${args.details}` : ''}`;
407
409
  const result = await withHeartbeat(() => callGemini(userPrompt, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.7, 'llm-orchestration'), reportFn);
408
410
  return stripFormatting(result);
409
411
  }
410
- };
412
+ });
411
413
  /**
412
414
  * Gemini Judge Tool
413
415
  * Multi-perspective evaluation and synthesis (LLM-as-a-Judge)
@@ -417,7 +419,7 @@ ${args.details ? `Additional details: ${args.details}` : ''}`;
417
419
  * - Position bias mitigation (don't favor first/last)
418
420
  * - Extract-then-synthesize (not pick-a-winner)
419
421
  */
420
- export const geminiJudgeTool = {
422
+ export const geminiJudgeTool = defineModelTool({
421
423
  name: "gemini_judge",
422
424
  description: "Evaluate and synthesize multiple AI perspectives into a unified verdict. Put CONTENT in the 'perspectives' parameter.",
423
425
  parameters: z.object({
@@ -429,7 +431,7 @@ export const geminiJudgeTool = {
429
431
  .optional()
430
432
  .default("synthesize")
431
433
  .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.")
434
+ ...filesField
433
435
  }),
434
436
  execute: async (args, { log, reportProgress }) => {
435
437
  // Resolve perspectives from fallback params (AI clients sometimes use wrong param name)
@@ -500,13 +502,13 @@ ${FORMAT_INSTRUCTION}`;
500
502
  const result = await withHeartbeat(() => callGemini(userPrompt + fileContext, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
501
503
  return stripFormatting(result);
502
504
  }
503
- };
505
+ });
504
506
  /**
505
507
  * Gemini Search Tool
506
508
  * Web search using Google Search grounding
507
509
  * Uses google_search_retrieval with dynamic retrieval config
508
510
  */
509
- export const geminiSearchTool = {
511
+ export const geminiSearchTool = defineModelTool({
510
512
  name: "gemini_search",
511
513
  description: "Web search via Gemini with Google Search grounding",
512
514
  parameters: z.object({
@@ -634,7 +636,7 @@ IMPORTANT:
634
636
  return `[Gemini Search error: ${error instanceof Error ? error.message : String(error)}]`;
635
637
  }
636
638
  }
637
- };
639
+ });
638
640
  /**
639
641
  * Check if Gemini is available
640
642
  */
@@ -7,6 +7,8 @@ import { config } from "dotenv";
7
7
  import * as path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { grokSearchTool } from './grok-enhanced.js';
10
+ import { defineModelTool } from './factory/define-model-tool.js';
11
+ import { filesField } from './factory/base-schemas.js';
10
12
  import { validateToolInput } from "../utils/input-validator.js";
11
13
  import { getGrokApiKey, hasGrokApiKey } from "../utils/api-keys.js";
12
14
  import { stripFormatting } from "../utils/format-stripper.js";
@@ -143,7 +145,7 @@ forceVisibleOutput = true, validationContext = 'llm-orchestration', reasoningEff
143
145
  * Grok Reasoning Tool
144
146
  * Deep logical reasoning with first principles thinking
145
147
  */
146
- export const grokReasonTool = {
148
+ export const grokReasonTool = defineModelTool({
147
149
  name: "grok_reason",
148
150
  description: "Deep reasoning. Put your PROBLEM or QUESTION in the 'problem' parameter.",
149
151
  parameters: z.object({
@@ -152,7 +154,7 @@ export const grokReasonTool = {
152
154
  .optional()
153
155
  .describe("Reasoning approach (e.g., analytical, creative, systematic, first-principles)"),
154
156
  context: z.string().optional().describe("Additional context for the problem"),
155
- 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."),
157
+ ...filesField,
156
158
  useHeavy: z.boolean().optional().describe("Use expensive Grok 4 Heavy model ($3/$15) for complex tasks")
157
159
  }),
158
160
  execute: async (args, { log, reportProgress }) => {
@@ -188,12 +190,12 @@ ${FORMAT_INSTRUCTION}`
188
190
  const result = await withHeartbeat(() => callGrok(messages, model, 0.7, maxTokens, true, 'llm-orchestration'), reportFn);
189
191
  return stripFormatting(result);
190
192
  }
191
- };
193
+ });
192
194
  /**
193
195
  * Grok Code Tool
194
196
  * Code analysis, optimization, and debugging
195
197
  */
196
- export const grokCodeTool = {
198
+ export const grokCodeTool = defineModelTool({
197
199
  name: "grok_code",
198
200
  description: "Code analysis. Put the CODE in the 'code' parameter, NOT in 'task'.",
199
201
  parameters: z.object({
@@ -201,7 +203,7 @@ export const grokCodeTool = {
201
203
  .describe("Code task (e.g., analyze, optimize, debug, review, refactor)"),
202
204
  code: z.string().describe("The actual source code to analyze (REQUIRED - put your code here)"),
203
205
  language: z.string().optional().describe("Programming language (e.g., 'typescript', 'python')"),
204
- 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."),
206
+ ...filesField,
205
207
  requirements: z.string().optional().describe("Specific requirements or focus areas")
206
208
  }),
207
209
  execute: async (args, { log, reportProgress }) => {
@@ -236,19 +238,19 @@ ${FORMAT_INSTRUCTION}`
236
238
  const result = await withHeartbeat(() => callGrok(messages, GrokModel.GROK_4_20_NON_REASONING, 0.2, 4000, true, 'code-analysis'), reportFn);
237
239
  return stripFormatting(result);
238
240
  }
239
- };
241
+ });
240
242
  /**
241
243
  * Grok Debug Tool
242
244
  * Specialized debugging assistance
243
245
  */
244
- export const grokDebugTool = {
246
+ export const grokDebugTool = defineModelTool({
245
247
  name: "grok_debug",
246
248
  description: "Debug assistance. Describe the ISSUE in the 'issue' parameter.",
247
249
  parameters: z.object({
248
250
  issue: z.string().describe("Description of the issue or bug (REQUIRED - put your problem here)"),
249
251
  code: z.string().optional().describe("Relevant code that has the issue"),
250
252
  error: z.string().optional().describe("Error message or stack trace"),
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."),
253
+ ...filesField,
252
254
  context: z.string().optional().describe("Additional context about the environment or conditions")
253
255
  }),
254
256
  execute: async (args, { log, reportProgress }) => {
@@ -288,12 +290,12 @@ ${FORMAT_INSTRUCTION}`
288
290
  const result = await withHeartbeat(() => callGrok(messages, GrokModel.GROK_4_20_NON_REASONING, 0.3, 3000, true, 'code-analysis'), reportFn);
289
291
  return stripFormatting(result);
290
292
  }
291
- };
293
+ });
292
294
  /**
293
295
  * Grok Architect Tool
294
296
  * System architecture and design
295
297
  */
296
- export const grokArchitectTool = {
298
+ export const grokArchitectTool = defineModelTool({
297
299
  name: "grok_architect",
298
300
  description: "Architecture design. Put your REQUIREMENTS in the 'requirements' parameter.",
299
301
  parameters: z.object({
@@ -302,7 +304,7 @@ export const grokArchitectTool = {
302
304
  scale: z.string()
303
305
  .optional()
304
306
  .describe("Expected scale (e.g., small, medium, large, enterprise)"),
305
- 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
+ ...filesField,
306
308
  }),
307
309
  execute: async (args, { log, reportProgress }) => {
308
310
  const { requirements, constraints, scale } = args;
@@ -329,12 +331,12 @@ ${FORMAT_INSTRUCTION}`
329
331
  const result = await withHeartbeat(() => callGrok(messages, GrokModel.GROK_4_20_MULTI_AGENT, 0.6, 4000, true, 'llm-orchestration', 'high'), reportFn);
330
332
  return stripFormatting(result);
331
333
  }
332
- };
334
+ });
333
335
  /**
334
336
  * Grok Brainstorm Tool
335
337
  * Creative brainstorming with Grok 4 Heavy
336
338
  */
337
- export const grokBrainstormTool = {
339
+ export const grokBrainstormTool = defineModelTool({
338
340
  name: "grok_brainstorm",
339
341
  description: "Contrarian first-principles brainstorming: deconstruct a topic to atomic truths, challenge every assumption, then rebuild radical alternatives. Use when conventional thinking has stalled. Put your TOPIC in the 'topic' parameter.",
340
342
  parameters: z.object({
@@ -342,7 +344,7 @@ export const grokBrainstormTool = {
342
344
  constraints: z.string().optional().describe("Any constraints or requirements to consider"),
343
345
  numIdeas: z.number().optional().describe("Number of radical rebuilds to generate (default: 5)"),
344
346
  forceHeavy: z.boolean().optional().describe("Use expensive Grok 4 Heavy model ($3/$15) for deeper creativity"),
345
- 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."),
347
+ ...filesField,
346
348
  }),
347
349
  execute: async (args, { log, reportProgress }) => {
348
350
  const { topic, constraints, numIdeas = 5, forceHeavy = false } = args;
@@ -383,7 +385,7 @@ ${FORMAT_INSTRUCTION}`
383
385
  const result = await withHeartbeat(() => callGrok(messages, model, 0.95, 4000, true, 'llm-orchestration'), reportFn);
384
386
  return stripFormatting(result);
385
387
  }
386
- };
388
+ });
387
389
  /**
388
390
  * Check if Grok is available
389
391
  */