From 9146e40044faef2f64c7e17df02de1baa601ba50 Mon Sep 17 00:00:00 2001
From: FelixWaweru
Date: Wed, 15 Jul 2026 17:25:25 +0300
Subject: [PATCH 1/6] feat: added additional v2 endpoints
---
README.md | 26 ++-
package.json | 57 +++++--
src/client.ts | 101 ++++++++++-
src/formatters.ts | 351 ++++++++++++++++++++++++++++++++++++-
src/index.ts | 2 +
src/tools/chat.ts | 24 ++-
src/tools/intel.ts | 183 ++++++++++++++++++++
src/tools/repos.ts | 29 +++-
src/types.ts | 361 ++++++++++++++++++++++++++++++++++++++-
tests/client.test.ts | 162 ++++++++++++++++++
tests/formatters.test.ts | 191 ++++++++++++++++++++-
11 files changed, 1446 insertions(+), 41 deletions(-)
create mode 100644 src/tools/intel.ts
diff --git a/README.md b/README.md
index 541b3fd..76c85bf 100644
--- a/README.md
+++ b/README.md
@@ -37,11 +37,12 @@ Built with [FastMCP](https://github.com/punkpeye/fastmcp) (TypeScript) and [Zod]
- 🔍 **Semantic & grep code search** across indexed repositories
- 🧠 **AI-powered research** — search + AI analysis in a single call
-- 📦 **Repository management** — index, status, README, listing
+- 📦 **Repository management** — index, status, README, listing, public catalog
+- 🛰️ **Repository intelligence** — scope (dependencies/functions/variables), cross-repo dependency map, blueprint, blast radius, code review, and test-gap analysis
- 📄 **File documentation** — AI-generated docs for any indexed file
- 📊 **Usage statistics** — query usage, activity, language breakdowns
- 🔐 **Agent-driven authentication** — sign up/sign in via OTP without pre-configured keys
-- 💬 **AI chat** — direct conversation with Code-Fundi AI
+- 💬 **AI chat & model insight** — direct conversation with Code-Fundi AI, model catalog, and per-tier limits
## Quick Start
@@ -139,9 +140,9 @@ npx fastmcp inspect src/index.ts # Open MCP Inspector UI
npx fastmcp dev src/index.ts # Test with MCP CLI
```
-## Tools Reference (22 tools)
+## Tools Reference (30 tools)
-Covers the Code-Fundi **V2** API: search (including search-with-chat / research), repositories (list, index, status, readme), files, history, statistics, API keys, authentication, plus **Fundi chat** and the model catalog (`POST /v1/fundi/chat`, `GET /v1/fundi/models` — there is no separate `/v2/chat` in the published OpenAPI).
+Covers the Code-Fundi **V2** API: search (including search-with-chat / research), repositories (list, index, status, readme, public catalog), repository intelligence (scope, map, blueprint, radius, review, test-gaps), files, history, statistics, API keys, authentication, plus **Fundi chat** (`POST /v1/fundi/chat`) and the V2 model catalog / limits (`GET /v2/models`, `GET /v2/models/limits`).
### Search
@@ -157,7 +158,19 @@ Covers the Code-Fundi **V2** API: search (including search-with-chat / research)
| `code-fundi-list-repos` | List indexed repositories with pagination |
| `code-fundi-index-repo` | Index a new GitHub repository |
| `code-fundi-repo-status` | Check repository indexing status |
-| `code-fundi-repo-readme` | Get repository README documentation |
+| `code-fundi-repo-readme` | Get repository README documentation (deprecated — prefer blueprint) |
+| `code-fundi-list-public-repos` | Browse the global catalog of indexed public repositories (no key needed) |
+
+### Repository Intelligence
+
+| Tool | Description |
+|------|-------------|
+| `code-fundi-repo-scope` | Aggregated dependencies, functions, and variables (with drill-down and cross-repo comparison) |
+| `code-fundi-repo-map` | Cross-repository dependency map |
+| `code-fundi-repo-blueprint` | README + dependency/convention overview (successor to repo-readme) |
+| `code-fundi-repo-radius` | Blast radius / file impact analysis (PRO+) |
+| `code-fundi-repo-review` | Per-file code review signals: verdict, risk, complexity, coverage, debt (PRO+) |
+| `code-fundi-repo-test-gaps` | Test coverage gap analysis with suggested test cases (PRO+) |
### Files
@@ -198,7 +211,8 @@ Covers the Code-Fundi **V2** API: search (including search-with-chat / research)
| Tool | Description |
|------|-------------|
| `code-fundi-chat` | Fundi AI chat (`POST /v1/fundi/chat`; streamed responses are collected to text) |
-| `code-fundi-list-models` | List available AI models |
+| `code-fundi-list-models` | List the curated chat model catalog (`GET /v2/models`) |
+| `code-fundi-model-limits` | Get AI model limits and tier configuration (`GET /v2/models/limits`) |
## Authentication
diff --git a/package.json b/package.json
index d1d0d9d..2804134 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,48 @@
{
"name": "@codefundi/code-fundi-mcp",
- "version": "0.1.0",
- "description": "MCP server for the Code-Fundi API — search, research, index, and manage code repositories via any MCP-compatible AI assistant.",
+ "version": "0.1.1",
+ "description": "MCP server for CodeFundi: the codebase map and blast-radius guard for AI coding agents like Claude Code, Cursor, and Copilot.",
+ "keywords": [
+ "mcp",
+ "mcp-server",
+ "model-context-protocol",
+ "fastmcp",
+ "code-fundi",
+ "codefundi",
+ "codebase-map",
+ "blast-radius",
+ "code-intelligence",
+ "call-graph",
+ "dependency-graph",
+ "code-search",
+ "context-api",
+ "rag",
+ "llm-context",
+ "ai-agent",
+ "ai-coding-agent",
+ "claude-code",
+ "cursor",
+ "github-copilot"
+ ],
+ "homepage": "https://codefundi.app",
+ "bugs": {
+ "url": "https://github.com/Code-Fundi/code-fundi-mcp/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Code-Fundi/code-fundi-mcp"
+ },
+ "license": "MIT",
+ "author": "Code Fundi ",
"type": "module",
"main": "dist/index.js",
"bin": {
"code-fundi-mcp": "dist/index.js"
},
+ "files": [
+ "dist",
+ "README.md"
+ ],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build",
@@ -15,22 +51,9 @@
"typecheck": "tsc --noEmit",
"test": "vitest"
},
- "files": [
- "dist",
- "README.md"
- ],
- "keywords": [
- "mcp",
- "code-fundi",
- "codefundi",
- "ai",
- "code-search",
- "fastmcp"
- ],
- "license": "MIT",
"dependencies": {
"fastmcp": "^4.0.0",
- "zod": "^3.24.0"
+ "zod": "^4.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
@@ -41,4 +64,4 @@
"engines": {
"node": ">=20.0.0"
}
-}
+}
\ No newline at end of file
diff --git a/src/client.ts b/src/client.ts
index 807877b..9357f8c 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -15,8 +15,13 @@ import type {
ActivityStatsResponse, LanguageStatsResponse, ApiKeysListResponse,
ApiKeyRegenerateResponse, ApiKeyDeleteResponse, V2AuthAuthenticateResponse,
V2AuthVerifyResponse, V2AuthResendResponse, V2AuthMode,
- ChatRequest, ChatResponse, ModelsResponse, SearchFieldsParam,
+ ChatRequest, ChatResponse, ModelsResponse, V2ModelsListResponse,
+ ModelLimitsResponse, SearchFieldsParam,
SortOrder, StatsRange, RepoScope, HistoryCategory, ErrorResponse,
+ PublicRepoListResponse, RepoScopeRequest, RepoScopeResponse,
+ RepoMapOptions, RepoMapResponse, RepoBlueprintResponse,
+ RepoRadiusRequest, RepoRadiusResponse, RepoReviewResponse, RepoReviewOrder,
+ RepoTestGapsResponse, TestGapPriority,
} from "./types.js";
// ============================================================================
@@ -86,6 +91,14 @@ export class CodeFundiClient {
}
}
+ /**
+ * Require an API key unless the caller opts into demo mode (pre-signup, IP-based credits).
+ * Endpoints that expose the OpenAPI `demo` flag can execute anonymously in demo mode.
+ */
+ private requireKeyUnlessDemo(demo?: boolean): void {
+ if (!demo) this.requireApiKey();
+ }
+
private authHeaders(): Record {
const h: Record = { "Content-Type": "application/json" };
if (this.apiKey) h["X-API-Key"] = this.apiKey;
@@ -239,6 +252,76 @@ export class CodeFundiClient {
return this.request(`/v2/repos/${encodeRepoKey(repoKey)}/readme`, { method: "GET" });
}
+ /**
+ * `GET /v2/public/repos` — global public repository catalog. No API key required
+ * (anonymous callers are IP rate limited; pass `internalSecret` for unlimited enumeration).
+ */
+ async listPublicRepos(
+ opts: { limit?: number; offset?: number; since?: string; file_count?: number; internalSecret?: string } = {},
+ ): Promise {
+ const { internalSecret, ...query } = opts;
+ const headers: Record = {};
+ if (internalSecret) headers["X-Internal-Secret"] = internalSecret;
+ return this.request(`/v2/public/repos${buildQuery(query)}`, { method: "GET", headers });
+ }
+
+ // ==== V2 Repository Intelligence ====
+
+ async getRepoScope(repoKey: string, body: RepoScopeRequest = {}, demo?: boolean): Promise {
+ this.requireKeyUnlessDemo(demo);
+ const qs = buildQuery({ demo: demo || undefined });
+ return this.request(
+ `/v2/repos/${encodeRepoKey(repoKey)}/scope${qs}`,
+ { method: "POST", body: JSON.stringify(body) },
+ );
+ }
+
+ async getRepoMap(repoKey: string, opts: RepoMapOptions = {}): Promise {
+ const { demo, ...rest } = opts;
+ this.requireKeyUnlessDemo(demo);
+ const qs = buildQuery({ ...rest, demo: demo || undefined });
+ return this.request(`/v2/repos/${encodeRepoKey(repoKey)}/map${qs}`, { method: "GET" });
+ }
+
+ async getRepoBlueprint(
+ repoKey: string,
+ opts: { conventions_path_prefix?: string; demo?: boolean } = {},
+ ): Promise {
+ const { demo, ...rest } = opts;
+ this.requireKeyUnlessDemo(demo);
+ const qs = buildQuery({ ...rest, demo: demo || undefined });
+ return this.request(`/v2/repos/${encodeRepoKey(repoKey)}/blueprint${qs}`, { method: "GET" });
+ }
+
+ async getRepoRadius(repoKey: string, body: RepoRadiusRequest, demo?: boolean): Promise {
+ this.requireKeyUnlessDemo(demo);
+ const qs = buildQuery({ demo: demo || undefined });
+ return this.request(
+ `/v2/repos/${encodeRepoKey(repoKey)}/radius${qs}`,
+ { method: "POST", body: JSON.stringify(body) },
+ );
+ }
+
+ async getRepoReview(
+ repoKey: string,
+ opts: { verdict?: string; order?: RepoReviewOrder; limit?: number; offset?: number; demo?: boolean } = {},
+ ): Promise {
+ const { demo, ...rest } = opts;
+ this.requireKeyUnlessDemo(demo);
+ const qs = buildQuery({ ...rest, demo: demo || undefined });
+ return this.request(`/v2/repos/${encodeRepoKey(repoKey)}/review${qs}`, { method: "GET" });
+ }
+
+ async getRepoTestGaps(
+ repoKey: string,
+ opts: { priority?: TestGapPriority; min_risk?: number; untested_only?: boolean; limit?: number; offset?: number; demo?: boolean } = {},
+ ): Promise {
+ const { demo, ...rest } = opts;
+ this.requireKeyUnlessDemo(demo);
+ const qs = buildQuery({ ...rest, demo: demo || undefined });
+ return this.request(`/v2/repos/${encodeRepoKey(repoKey)}/test-gaps${qs}`, { method: "GET" });
+ }
+
// ==== V2 Files ====
async listFiles(repoKey: string, opts: { search?: string | null; limit?: number; offset?: number; order_by?: string; order?: SortOrder } = {}): Promise {
@@ -324,7 +407,7 @@ export class CodeFundiClient {
return this.postUnauth("/v2/auth/resend", { email: p.email, type: p.type ?? "signup" });
}
- // ==== V1 Chat & Models (OpenAPI: AI Chat; server body uses `question`, not `prompt`) ====
+ // ==== V1 Chat (OpenAPI: AI Chat; server body uses `question`, not `prompt`) ====
async chat(req: ChatRequest): Promise {
this.requireApiKey();
@@ -341,9 +424,21 @@ export class CodeFundiClient {
return this.parseFundiChatResponse(res);
}
+ // ==== V2 Models ====
+
+ /**
+ * `GET /v2/models` — curated CodeFundi chat model catalog. The API wraps the list under
+ * `data.models`; this method normalizes it to `{ models }` for MCP tools.
+ */
async getModels(): Promise {
this.requireApiKey();
- return this.request("/v1/fundi/models", { method: "GET" });
+ const res = await this.request("/v2/models", { method: "GET" });
+ return { models: res.data?.models ?? [] };
+ }
+
+ async getModelLimits(): Promise {
+ this.requireApiKey();
+ return this.request("/v2/models/limits", { method: "GET" });
}
}
diff --git a/src/formatters.ts b/src/formatters.ts
index ebdb665..49e0382 100644
--- a/src/formatters.ts
+++ b/src/formatters.ts
@@ -10,9 +10,28 @@ import type {
UsageByType, ActivityByDay, LanguageStat,
ApiKey, AIModel, ReadmeData, RepoStatusData,
RepositoryIndexInitRepo, Pagination, TierName,
+ PublicRepoListItem, RepoScopeData, RepoScopePagination,
+ RepoMapData, RepoBlueprintData, RepoBlueprintMeta,
+ RepoRadiusData, RepoReviewData, RepoTestGapsData,
+ ModelLimitsData,
} from "./types.js";
import { CodeFundiApiError } from "./client.js";
+// ============================================================================
+// Internal helpers
+// ============================================================================
+
+/** Render an opaque JSON value as a fenced code block for faithful display. */
+function jsonBlock(value: unknown): string {
+ return "```json\n" + JSON.stringify(value, null, 2) + "\n```";
+}
+
+/** Read a string field from a loosely-typed record, if present. */
+function readString(obj: Record | undefined, key: string): string | undefined {
+ const v = obj?.[key];
+ return typeof v === "string" ? v : undefined;
+}
+
// ============================================================================
// Search
// ============================================================================
@@ -145,6 +164,292 @@ export function formatReadme(data: ReadmeData): string {
return parts.join("\n");
}
+export function formatPublicRepoList(repos: PublicRepoListItem[], pagination?: Pagination): string {
+ const parts: string[] = [];
+ parts.push(`## Public Repositories (${pagination?.total ?? repos.length})\n`);
+
+ if (repos.length === 0) {
+ parts.push("No public repositories found.");
+ return parts.join("\n");
+ }
+
+ for (const r of repos) {
+ parts.push(`### ${r.name}`);
+ parts.push(`- **ID:** \`${r.id}\``);
+ parts.push(`- **Link:** ${r.link}`);
+ if (r.updated_at) parts.push(`- **Updated:** ${r.updated_at}`);
+ if (r.top_files?.length) {
+ parts.push(`- **Top files:**`);
+ for (const f of r.top_files) {
+ const size = f.file_size_kb != null ? ` (${f.file_size_kb} KB)` : "";
+ parts.push(` - \`${f.file_path}\`${size}`);
+ }
+ }
+ parts.push("");
+ }
+
+ if (pagination?.has_more) {
+ parts.push(`_Showing ${repos.length} of ${pagination.total}. Use offset=${pagination.offset + pagination.limit} for next page._`);
+ }
+
+ return parts.join("\n");
+}
+
+// ============================================================================
+// Repository Intelligence (scope, map, blueprint, radius, review, test-gaps)
+// ============================================================================
+
+export function formatRepoScope(data: RepoScopeData, pagination?: RepoScopePagination, gatedKinds?: string[]): string {
+ const parts: string[] = [];
+ const repoName = readString(data.repo, "name");
+ parts.push(`## Repository Scope${repoName ? `: ${repoName}` : ""}\n`);
+
+ const s = data.summary;
+ if (s) {
+ const info: string[] = [];
+ if (s.total_files !== undefined) info.push(`Files: ${s.total_files}`);
+ if (s.dependency_count !== undefined) info.push(`Dependencies: ${s.dependency_count}`);
+ if (s.function_count !== undefined) info.push(`Functions: ${s.function_count}`);
+ if (s.variable_count !== undefined) info.push(`Variables: ${s.variable_count}`);
+ if (s.compared_repo_count) info.push(`Compared repos: ${s.compared_repo_count}`);
+ if (info.length) parts.push(info.join(" | ") + "\n");
+ }
+ if (gatedKinds?.length) parts.push(`_Gated (upgrade to unlock): ${gatedKinds.join(", ")}._\n`);
+
+ const deps = data.scope?.dependencies ?? [];
+ if (deps.length) {
+ parts.push(`### Dependencies (${deps.length})`);
+ for (const d of deps) {
+ const ver = d.version ? `@${d.version}` : "";
+ const typ = d.type ? ` [${d.type}]` : "";
+ const usage = d.usage?.file_count !== undefined
+ ? ` — used in ${d.usage.file_count} file(s)${d.usage.percentage !== undefined ? ` (${d.usage.percentage}%)` : ""}`
+ : "";
+ parts.push(`- **${d.name}**${ver}${typ}${usage}`);
+ if (d.comparison?.status) parts.push(` - Comparison: ${d.comparison.status}`);
+ }
+ parts.push("");
+ }
+
+ const fns = data.scope?.functions ?? [];
+ if (fns.length) {
+ parts.push(`### Functions (${fns.length})`);
+ for (const f of fns) {
+ const usage = f.usage?.file_count !== undefined ? ` — ${f.usage.file_count} file(s)` : "";
+ parts.push(`- \`${f.name}\`${f.type ? ` [${f.type}]` : ""}${usage}`);
+ }
+ parts.push("");
+ }
+
+ const vars = data.scope?.variables ?? [];
+ if (vars.length) {
+ parts.push(`### Variables (${vars.length})`);
+ for (const v of vars) {
+ const usage = v.usage?.file_count !== undefined ? ` — ${v.usage.file_count} file(s)` : "";
+ parts.push(`- \`${v.name}\`${v.type ? ` [${v.type}]` : ""}${usage}`);
+ }
+ parts.push("");
+ }
+
+ if (!deps.length && !fns.length && !vars.length) parts.push("No scope data found.");
+
+ const more: string[] = [];
+ const paginate = (label: string, pg?: Pagination) => {
+ if (pg?.has_more) more.push(`${label} (offset ${pg.offset + pg.limit})`);
+ };
+ paginate("dependencies", pagination?.dependencies);
+ paginate("functions", pagination?.functions);
+ paginate("variables", pagination?.variables);
+ if (more.length) parts.push(`_More available: ${more.join(", ")}._`);
+
+ return parts.join("\n");
+}
+
+export function formatRepoMap(data: RepoMapData): string {
+ const parts: string[] = [];
+ const srcName = readString(data.source_repo, "name");
+ parts.push(`## Cross-Repository Dependency Map${srcName ? `: ${srcName}` : ""}\n`);
+
+ if (data.scope && Object.keys(data.scope).length) {
+ parts.push("### Scope");
+ parts.push(jsonBlock(data.scope));
+ parts.push("");
+ }
+
+ const deps = data.dependencies ?? [];
+ if (!deps.length) {
+ parts.push("No dependency map entries found.");
+ return parts.join("\n");
+ }
+
+ parts.push(`### Dependencies (${deps.length})`);
+ for (const d of deps) {
+ const name = readString(d, "name") ?? "(unnamed)";
+ const type = readString(d, "type");
+ parts.push(`- **${name}**${type ? ` [${type}]` : ""}`);
+ const repos = Array.isArray(d.repos) ? d.repos : undefined;
+ if (repos?.length) parts.push(` - Present in ${repos.length} repo(s)`);
+ }
+
+ return parts.join("\n");
+}
+
+export function formatRepoBlueprint(data: RepoBlueprintData, meta?: RepoBlueprintMeta): string {
+ const parts: string[] = [];
+ const name = data.repo?.name ?? data.file_name ?? "Repository";
+ parts.push(`## ${name} — Blueprint\n`);
+ if (meta?.deprecated) {
+ parts.push("_Note: the `/readme` endpoint is deprecated; prefer `code-fundi-repo-blueprint`._\n");
+ }
+ if (data.github_url) parts.push(`- **URL:** ${data.github_url}`);
+
+ const deps = data.dependencies ?? [];
+ if (deps.length) {
+ parts.push(`\n### Top Dependencies (${deps.length})`);
+ for (const d of deps.slice(0, 25)) {
+ const ver = d.version ? `@${d.version}` : "";
+ const typ = d.type ? ` [${d.type}]` : "";
+ const usage = d.usage?.file_count !== undefined ? ` — ${d.usage.file_count} file(s)` : "";
+ parts.push(`- **${d.name}**${ver}${typ}${usage}`);
+ }
+ if (deps.length > 25) parts.push(`_...and ${deps.length - 25} more_`);
+ }
+
+ const tf = data.symbols?.top_functions ?? [];
+ if (tf.length) {
+ parts.push(`\n### Top Functions (${tf.length})`);
+ for (const f of tf.slice(0, 25)) parts.push(`- \`${f.name}\`${f.type ? ` [${f.type}]` : ""}`);
+ }
+
+ const tv = data.symbols?.top_variables ?? [];
+ if (tv.length) {
+ parts.push(`\n### Top Variables (${tv.length})`);
+ for (const v of tv.slice(0, 25)) parts.push(`- \`${v.name}\`${v.type ? ` [${v.type}]` : ""}`);
+ }
+
+ if (data.conventions && Object.keys(data.conventions).length) {
+ parts.push(`\n### Coding Conventions`);
+ parts.push(jsonBlock(data.conventions));
+ } else if (meta?.conventions_tier_gated) {
+ parts.push(`\n_Coding conventions require PRO tier or higher._`);
+ }
+
+ if (data.documentation) {
+ parts.push(`\n### README\n`);
+ parts.push(data.documentation);
+ }
+
+ return parts.join("\n");
+}
+
+export function formatRepoRadius(data: RepoRadiusData): string {
+ const parts: string[] = [];
+ parts.push("## Blast Radius Analysis\n");
+
+ if (data.summary && Object.keys(data.summary).length) {
+ parts.push("### Summary");
+ parts.push(jsonBlock(data.summary));
+ parts.push("");
+ }
+
+ const targets = data.targets ?? [];
+ if (!targets.length) {
+ parts.push("No impact targets found.");
+ return parts.join("\n");
+ }
+
+ targets.forEach((t, i) => {
+ parts.push(`### Target ${i + 1}`);
+ if (t.entry_points?.length) parts.push(`- **Entry points:** ${t.entry_points.join(", ")}`);
+ if (t.data_flows?.length) {
+ parts.push(`- **Data flows (${t.data_flows.length}):**`);
+ for (const f of t.data_flows) {
+ parts.push(` - ${f.source_kind ?? "?"} → ${f.sink_kind ?? "?"} (risk: ${f.risk_level ?? "n/a"})`);
+ }
+ }
+ if (t.call_edges?.length) {
+ parts.push(`- **Call edges (${t.call_edges.length}):**`);
+ for (const e of t.call_edges) {
+ parts.push(` - ${e.caller ?? "?"} → ${e.callee ?? "?"}${e.call_type ? ` (${e.call_type})` : ""}`);
+ }
+ }
+ if (t.admin_enrichment && Object.keys(t.admin_enrichment).length) {
+ parts.push(`- **Admin enrichment:**`);
+ parts.push(jsonBlock(t.admin_enrichment));
+ }
+ parts.push("");
+ });
+
+ return parts.join("\n");
+}
+
+export function formatRepoReview(data: RepoReviewData): string {
+ const parts: string[] = [];
+ parts.push("## Code Review Signals\n");
+
+ if (data.summary && Object.keys(data.summary).length) {
+ parts.push("### Summary");
+ parts.push(jsonBlock(data.summary));
+ parts.push("");
+ }
+
+ const files = data.files ?? [];
+ if (!files.length) {
+ parts.push("No review signals found.");
+ return parts.join("\n");
+ }
+
+ parts.push(`### Files (${files.length})`);
+ parts.push("| File | Verdict | Risk | Cyclomatic | Coverage | Debt (min) |");
+ parts.push("|------|---------|------|-----------|----------|-----------|");
+ for (const f of files) {
+ const label = f.file_path ?? f.file_name ?? f.id ?? "?";
+ const cov = f.estimated_coverage === null || f.estimated_coverage === undefined ? "N/A" : `${f.estimated_coverage}`;
+ parts.push(`| ${label} | ${f.verdict ?? "?"} | ${f.risk_score ?? "?"} | ${f.cyclomatic_complexity ?? "?"} | ${cov} | ${f.technical_debt_minutes ?? "?"} |`);
+ }
+
+ return parts.join("\n");
+}
+
+export function formatRepoTestGaps(data: RepoTestGapsData): string {
+ const parts: string[] = [];
+ parts.push("## Test Coverage Gaps\n");
+
+ if (data.summary && Object.keys(data.summary).length) {
+ parts.push("### Summary");
+ parts.push(jsonBlock(data.summary));
+ parts.push("");
+ }
+
+ const gaps = data.gaps ?? [];
+ if (!gaps.length) {
+ parts.push("No test gaps found.");
+ return parts.join("\n");
+ }
+
+ for (const g of gaps) {
+ parts.push(`### ${g.file_path ?? g.file_name ?? g.id ?? "file"}`);
+ const info: string[] = [];
+ if (g.risk_score !== undefined) info.push(`Risk: ${g.risk_score}`);
+ if (g.estimated_coverage !== undefined) info.push(`Coverage: ${g.estimated_coverage}`);
+ if (g.security_risk_level) info.push(`Security: ${g.security_risk_level}`);
+ if (g.churn_rate) info.push(`Churn: ${g.churn_rate}`);
+ if (info.length) parts.push(`- ${info.join(" | ")}`);
+ if (g.suggested_test_cases?.length) {
+ parts.push(`- **Suggested tests:**`);
+ for (const c of g.suggested_test_cases) {
+ const fn = c.target_function ? ` (\`${c.target_function}\`)` : "";
+ parts.push(` - [${c.priority ?? "?"}] ${c.description ?? ""}${fn}`);
+ }
+ }
+ if (g.mockable_dependencies?.length) parts.push(`- **Mockable deps:** ${g.mockable_dependencies.join(", ")}`);
+ if (g.hard_to_test_reasons?.length) parts.push(`- **Hard to test:** ${g.hard_to_test_reasons.join("; ")}`);
+ parts.push("");
+ }
+
+ return parts.join("\n");
+}
+
// ============================================================================
// Files
// ============================================================================
@@ -301,17 +606,57 @@ export function formatApiKeys(keys: ApiKey[]): string {
export function formatModels(models: AIModel[]): string {
const parts: string[] = [];
parts.push(`## Available Models (${models.length})\n`);
- parts.push("| Name | Provider | Tier |");
- parts.push("|------|----------|------|");
+ parts.push("| Name | Provider | Tier | Context |");
+ parts.push("|------|----------|------|---------|");
for (const m of models) {
const tier =
m.tier_required
?? (m.tier_requirements?.premium_only ? "PRO" as TierName : "FREE" as TierName);
- parts.push(`| ${m.name} | ${m.provider} | ${tier} |`);
+ const ctx = m.context_length != null ? m.context_length.toLocaleString() : "—";
+ parts.push(`| ${m.name} | ${m.provider} | ${tier} | ${ctx} |`);
}
return parts.join("\n");
}
+export function formatModelLimits(data: ModelLimitsData): string {
+ const parts: string[] = [];
+ parts.push(`## Model Limits & Tier${data.tier ? `: ${data.tier}` : ""}\n`);
+
+ const sub = data.subscription;
+ if (sub) {
+ const info: string[] = [];
+ if (sub.tokens !== undefined) info.push(`Tokens: ${sub.tokens}`);
+ if (sub.bonus_tokens !== undefined) info.push(`Bonus: ${sub.bonus_tokens}`);
+ if (sub.expiry_date) info.push(`Expires: ${sub.expiry_date}`);
+ if (info.length) parts.push(`**Subscription:** ${info.join(" | ")}`);
+ }
+
+ const m = data.model;
+ if (m) {
+ parts.push(`\n### Active Model`);
+ if (m.name) parts.push(`- **Name:** ${m.name}${m.provider ? ` (${m.provider})` : ""}`);
+ if (m.max_tokens !== undefined) parts.push(`- **Max tokens:** ${m.max_tokens}`);
+ if (m.context_length !== undefined) parts.push(`- **Context length:** ${m.context_length}`);
+ if (m.vector_search_length !== undefined) parts.push(`- **Vector search length:** ${m.vector_search_length}`);
+ if (m.knowledge_search_length !== undefined) parts.push(`- **Knowledge search length:** ${m.knowledge_search_length}`);
+ if (m.knowledge_storage_limit !== undefined) parts.push(`- **Knowledge storage limit:** ${m.knowledge_storage_limit}`);
+ }
+
+ const l = data.limits;
+ if (l) {
+ parts.push(`\n### Limits`);
+ if (l.history_days !== undefined) parts.push(`- **History days:** ${l.history_days}`);
+ if (l.history_max_records !== undefined) parts.push(`- **History max records:** ${l.history_max_records}`);
+ if (l.repos_max !== undefined) parts.push(`- **Max repos:** ${l.repos_max}`);
+ if (l.files_per_repo_max !== undefined) parts.push(`- **Files per repo:** ${l.files_per_repo_max}`);
+ if (l.stats_range_max_days !== undefined) parts.push(`- **Stats range max days:** ${l.stats_range_max_days}`);
+ if (l.can_access_org_repos !== undefined) parts.push(`- **Can access org repos:** ${l.can_access_org_repos}`);
+ if (l.can_share_to_org !== undefined) parts.push(`- **Can share to org:** ${l.can_share_to_org}`);
+ }
+
+ return parts.join("\n");
+}
+
// ============================================================================
// Errors
// ============================================================================
diff --git a/src/index.ts b/src/index.ts
index 3635737..ac8aa24 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -14,6 +14,7 @@ import { FastMCP } from "fastmcp";
import { registerSearchTools } from "./tools/search.js";
import { registerRepoTools } from "./tools/repos.js";
+import { registerRepoIntelTools } from "./tools/intel.js";
import { registerFileTools } from "./tools/files.js";
import { registerHistoryTools } from "./tools/history.js";
import { registerStatsTools } from "./tools/stats.js";
@@ -28,6 +29,7 @@ const server = new FastMCP({
// Register all tool groups
registerSearchTools(server);
registerRepoTools(server);
+registerRepoIntelTools(server);
registerFileTools(server);
registerHistoryTools(server);
registerStatsTools(server);
diff --git a/src/tools/chat.ts b/src/tools/chat.ts
index 3478b76..ed6e040 100644
--- a/src/tools/chat.ts
+++ b/src/tools/chat.ts
@@ -3,13 +3,13 @@
*
* Chat uses `POST /v1/fundi/chat` (OpenAPI "AI Chat"). The production API streams HTML;
* the client collects the full body into a single string for MCP tools.
- * Model catalog uses `GET /v1/fundi/models` (no separate `/v2/chat` in the published OpenAPI).
+ * The model catalog uses `GET /v2/models` and per-tier limits use `GET /v2/models/limits`.
*/
import type { FastMCP } from "fastmcp";
import { z } from "zod";
import { getClient } from "../client.js";
-import { formatModels, formatError } from "../formatters.js";
+import { formatModels, formatModelLimits, formatError } from "../formatters.js";
export function registerChatTools(server: FastMCP): void {
server.addTool({
@@ -65,7 +65,8 @@ export function registerChatTools(server: FastMCP): void {
server.addTool({
name: "code-fundi-list-models",
- description: "List all available AI models on Code-Fundi with their providers and required tier.",
+ description:
+ "List the curated Code-Fundi chat model catalog (GET /v2/models) with providers, required tier, and context length.",
parameters: z.object({}),
annotations: { title: "List AI Models", readOnlyHint: true },
execute: async () => {
@@ -76,4 +77,21 @@ export function registerChatTools(server: FastMCP): void {
} catch (err) { return formatError(err); }
},
});
+
+ server.addTool({
+ name: "code-fundi-model-limits",
+ description:
+ "Get the AI model limits and tier configuration for your account (GET /v2/models/limits): " +
+ "subscription tokens, the active model's context/knowledge limits, and account limits such as " +
+ "max repositories, files per repo, history retention, and organization access.",
+ parameters: z.object({}),
+ annotations: { title: "Model Limits", readOnlyHint: true },
+ execute: async () => {
+ try {
+ const client = getClient();
+ const res = await client.getModelLimits();
+ return res.data ? formatModelLimits(res.data) : "No model limit data available.";
+ } catch (err) { return formatError(err); }
+ },
+ });
}
diff --git a/src/tools/intel.ts b/src/tools/intel.ts
new file mode 100644
index 0000000..e3e591f
--- /dev/null
+++ b/src/tools/intel.ts
@@ -0,0 +1,183 @@
+/**
+ * Code-Fundi MCP — Repository Intelligence Tools
+ *
+ * Advanced V2 repository intelligence endpoints:
+ * - scope (POST /v2/repos/{repo_id}/scope) dependencies / functions / variables
+ * - map (GET /v2/repos/{repo_id}/map) cross-repository dependency map
+ * - blueprint (GET /v2/repos/{repo_id}/blueprint) README + dependency overview
+ * - radius (POST /v2/repos/{repo_id}/radius) blast radius / file impact analysis
+ * - review (GET /v2/repos/{repo_id}/review) code review signals (PRO+)
+ * - test-gaps (GET /v2/repos/{repo_id}/test-gaps) test coverage gap analysis (PRO+)
+ *
+ * These endpoints consume credits and several require PRO tier or higher.
+ */
+
+import type { FastMCP } from "fastmcp";
+import { z } from "zod";
+import { getClient } from "../client.js";
+import {
+ formatRepoScope, formatRepoMap, formatRepoBlueprint,
+ formatRepoRadius, formatRepoReview, formatRepoTestGaps, formatError,
+} from "../formatters.js";
+
+export function registerRepoIntelTools(server: FastMCP): void {
+ server.addTool({
+ name: "code-fundi-repo-scope",
+ description:
+ "Get aggregated dependencies, functions, and/or variable names for a repository in a single call. " +
+ "Use `include` to restrict kinds (default: all three). Provide `name` (plus `expand`) to drill into a " +
+ "single dependency's files/functions, or `compare_repos` (up to 25 UUIDs) to compare dependencies across repos. " +
+ "Consumes credits; functions/variables may be tier-gated.",
+ parameters: z.object({
+ repo_key: z.string().describe("Repository UUID or clone URL"),
+ include: z.array(z.enum(["dependencies", "functions", "variables"])).optional().describe("Kinds to return (default: all three)"),
+ name: z.string().optional().describe("Exact name filter (dependency key or symbol name)"),
+ type: z.string().optional().describe("Type/role filter (dependency type, function type, or variable role)"),
+ expand: z.array(z.enum(["files", "functions"])).optional().describe("Drill-down expansions; requires `name`"),
+ compare_repos: z.array(z.string()).optional().describe("Up to 25 repository UUIDs for dependency comparison"),
+ limit: z.number().int().min(1).max(200).optional().describe("Max items per kind (default 50)"),
+ offset: z.number().int().min(0).optional().describe("Pagination offset"),
+ order: z.enum(["usage", "name"]).optional().describe("Ordering (default: usage)"),
+ demo: z.boolean().optional().describe("Run in demo mode (pre-signup, public data, IP-based credits)"),
+ }),
+ annotations: { title: "Repository Scope", readOnlyHint: true, openWorldHint: true },
+ execute: async (args) => {
+ try {
+ const client = getClient();
+ const { repo_key, demo, ...body } = args;
+ const res = await client.getRepoScope(repo_key, body, demo);
+ return res.data ? formatRepoScope(res.data, res.pagination, res.meta?.gated_kinds) : "No scope data available.";
+ } catch (err) { return formatError(err); }
+ },
+ });
+
+ server.addTool({
+ name: "code-fundi-repo-map",
+ description:
+ "Build a cross-repository dependency map showing where a dependency appears across the source repo, " +
+ "optional compared repos, and (optionally) the public index. Returns structured JSON. Consumes credits.",
+ parameters: z.object({
+ repo_key: z.string().describe("Repository UUID or clone URL"),
+ dependency: z.string().optional().describe("Dependency name to map"),
+ type: z.enum(["package", "import", "export", "env"]).optional().describe("Dependency type filter"),
+ compare_repos: z.array(z.string()).optional().describe("Repository UUIDs to compare against (max 10)"),
+ include_public_index: z.boolean().optional().describe("Include matches from the public index (default false)"),
+ limit: z.number().int().min(1).max(50).optional().describe("Max dependencies (default 25)"),
+ files_per_repo: z.number().int().min(1).max(100).optional().describe("Max files per repo (default 25)"),
+ demo: z.boolean().optional().describe("Run in demo mode (pre-signup, public data, IP-based credits)"),
+ }),
+ annotations: { title: "Repository Dependency Map", readOnlyHint: true, openWorldHint: true },
+ execute: async (args) => {
+ try {
+ const client = getClient();
+ const { repo_key, compare_repos, ...rest } = args;
+ const res = await client.getRepoMap(repo_key, {
+ ...rest,
+ compare_repos: compare_repos?.length ? compare_repos.join(",") : undefined,
+ });
+ return res.data ? formatRepoMap(res.data) : "No dependency map available.";
+ } catch (err) { return formatError(err); }
+ },
+ });
+
+ server.addTool({
+ name: "code-fundi-repo-blueprint",
+ description:
+ "Get the canonical repository intelligence overview: README documentation, top dependencies, " +
+ "top functions/variables, and aggregated coding conventions (PRO+ only). " +
+ "This is the successor to `code-fundi-repo-readme`. Consumes credits.",
+ parameters: z.object({
+ repo_key: z.string().describe("Repository UUID or clone URL"),
+ conventions_path_prefix: z.string().optional().describe("Optional path prefix filter for conventions aggregation (PRO+)"),
+ demo: z.boolean().optional().describe("Run in demo mode (pre-signup, public data, IP-based credits)"),
+ }),
+ annotations: { title: "Repository Blueprint", readOnlyHint: true, openWorldHint: true },
+ execute: async (args) => {
+ try {
+ const client = getClient();
+ const { repo_key, ...opts } = args;
+ const res = await client.getRepoBlueprint(repo_key, opts);
+ return res.data ? formatRepoBlueprint(res.data, res.meta) : "No blueprint available.";
+ } catch (err) { return formatError(err); }
+ },
+ });
+
+ server.addTool({
+ name: "code-fundi-repo-radius",
+ description:
+ "Run blast-radius / file impact analysis for one or more files or dependencies. " +
+ "Returns projected data flows, entry points, and call edges. Requires PRO, ENTERPRISE, or ADMIN tier " +
+ "(STARTUP is not eligible) and consumes credits.",
+ parameters: z.object({
+ repo_key: z.string().describe("Repository UUID or clone URL"),
+ file: z.string().optional().describe("Single file path to analyze"),
+ files: z.array(z.string()).optional().describe("Multiple file paths to analyze"),
+ dependency: z.string().optional().describe("Single dependency name to analyze"),
+ dependencies: z.array(z.string()).optional().describe("Multiple dependency names to analyze"),
+ include: z.array(z.string()).optional().describe("Projection kinds to include (e.g. data_flows, entry_points, call_edges)"),
+ limit: z.number().int().min(1).optional().describe("Max targets to return"),
+ demo: z.boolean().optional().describe("Run in demo mode (pre-signup, public data, IP-based credits)"),
+ }),
+ annotations: { title: "Repository Blast Radius", readOnlyHint: true, openWorldHint: true },
+ execute: async (args) => {
+ try {
+ const client = getClient();
+ const { repo_key, demo, ...body } = args;
+ const res = await client.getRepoRadius(repo_key, body, demo);
+ return res.data ? formatRepoRadius(res.data) : "No blast radius data available.";
+ } catch (err) { return formatError(err); }
+ },
+ });
+
+ server.addTool({
+ name: "code-fundi-repo-review",
+ description:
+ "Get code review signals per file: verdict (pass/warn/block), risk score, complexity, duplication, " +
+ "estimated coverage, and technical debt. Requires PRO tier or higher and consumes credits.",
+ parameters: z.object({
+ repo_key: z.string().describe("Repository UUID or clone URL"),
+ verdict: z.array(z.enum(["pass", "warn", "block"])).optional().describe("Filter by verdict(s)"),
+ order: z.enum(["risk", "complexity", "coverage", "debt"]).optional().describe("Sort order (default: risk)"),
+ limit: z.number().int().min(1).optional().describe("Max files (default 50)"),
+ offset: z.number().int().min(0).optional().describe("Pagination offset"),
+ demo: z.boolean().optional().describe("Run in demo mode (pre-signup, public data, IP-based credits)"),
+ }),
+ annotations: { title: "Repository Code Review", readOnlyHint: true, openWorldHint: true },
+ execute: async (args) => {
+ try {
+ const client = getClient();
+ const { repo_key, verdict, ...rest } = args;
+ const res = await client.getRepoReview(repo_key, {
+ ...rest,
+ verdict: verdict?.length ? verdict.join(",") : undefined,
+ });
+ return res.data ? formatRepoReview(res.data) : "No review signals available.";
+ } catch (err) { return formatError(err); }
+ },
+ });
+
+ server.addTool({
+ name: "code-fundi-repo-test-gaps",
+ description:
+ "Analyze test coverage gaps: per-file risk score, estimated coverage, suggested test cases, mockable " +
+ "dependencies, and reasons code is hard to test. Requires PRO tier or higher and consumes credits.",
+ parameters: z.object({
+ repo_key: z.string().describe("Repository UUID or clone URL"),
+ priority: z.enum(["high", "medium", "low"]).optional().describe("Filter by gap priority"),
+ min_risk: z.number().min(0).max(1).optional().describe("Minimum risk score (0-1)"),
+ untested_only: z.boolean().optional().describe("Only return untested files (default true)"),
+ limit: z.number().int().min(1).optional().describe("Max gaps (default 50)"),
+ offset: z.number().int().min(0).optional().describe("Pagination offset"),
+ demo: z.boolean().optional().describe("Run in demo mode (pre-signup, public data, IP-based credits)"),
+ }),
+ annotations: { title: "Repository Test Gaps", readOnlyHint: true, openWorldHint: true },
+ execute: async (args) => {
+ try {
+ const client = getClient();
+ const { repo_key, ...opts } = args;
+ const res = await client.getRepoTestGaps(repo_key, opts);
+ return res.data ? formatRepoTestGaps(res.data) : "No test gap data available.";
+ } catch (err) { return formatError(err); }
+ },
+ });
+}
diff --git a/src/tools/repos.ts b/src/tools/repos.ts
index 7eb0629..9efba9b 100644
--- a/src/tools/repos.ts
+++ b/src/tools/repos.ts
@@ -7,7 +7,7 @@ import { z } from "zod";
import { getClient } from "../client.js";
import {
formatRepoList, formatIndexResult, formatRepoStatus,
- formatReadme, formatError,
+ formatReadme, formatPublicRepoList, formatError,
} from "../formatters.js";
export function registerRepoTools(server: FastMCP): void {
@@ -77,7 +77,8 @@ export function registerRepoTools(server: FastMCP): void {
name: "code-fundi-repo-readme",
description:
"Get the parsed README documentation for a repository. " +
- "Provide either a repository UUID or a clone URL.",
+ "Provide either a repository UUID or a clone URL. " +
+ "Deprecated in favor of code-fundi-repo-blueprint, which returns the same README plus dependency and convention intelligence.",
parameters: z.object({
repo_key: z.string().describe("Repository UUID or clone URL"),
}),
@@ -90,4 +91,28 @@ export function registerRepoTools(server: FastMCP): void {
} catch (err) { return formatError(err); }
},
});
+
+ server.addTool({
+ name: "code-fundi-list-public-repos",
+ description:
+ "Browse the global catalog of indexed public repositories (across all Code-Fundi users). " +
+ "Supports pagination, an `updated_since` cursor, and optional embedding of each repo's largest files. " +
+ "This endpoint does not require an API key.",
+ parameters: z.object({
+ limit: z.number().int().min(1).max(500).optional().describe("Max results per page (default 100)"),
+ offset: z.number().int().min(0).optional().describe("Pagination offset"),
+ since: z.string().optional().describe("Return repos updated strictly after this ISO 8601 timestamp"),
+ file_count: z.number().int().min(0).max(50).optional().describe("When > 0, embed this many largest files per repo (default 0)"),
+ internal_secret: z.string().optional().describe("Internal sitemap secret for unlimited server-side enumeration (optional)"),
+ }),
+ annotations: { title: "List Public Repositories", readOnlyHint: true, openWorldHint: true },
+ execute: async (args) => {
+ try {
+ const client = getClient();
+ const { internal_secret, ...opts } = args;
+ const res = await client.listPublicRepos({ ...opts, internalSecret: internal_secret });
+ return formatPublicRepoList(res.data || [], res.pagination);
+ } catch (err) { return formatError(err); }
+ },
+ });
}
diff --git a/src/types.ts b/src/types.ts
index 03a3467..fdf3e39 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -9,7 +9,7 @@
// Common Types
// ============================================================================
-export type TierName = "FREE" | "DEV" | "PRO" | "ENTERPRISE";
+export type TierName = "FREE" | "DEV" | "STARTUP" | "PRO" | "ENTERPRISE";
export type SearchScope = "all" | "repos" | "files" | "code" | "functions";
@@ -268,6 +268,302 @@ export interface ReadmeData {
export interface ReadmeResponse extends BaseResponse {}
+// ============================================================================
+// Public Repository Catalog Types (`GET /v2/public/repos`)
+// ============================================================================
+
+export interface PublicRepoTopFile {
+ id: string;
+ file_name: string;
+ file_path: string;
+ file_size_kb?: number | null;
+ updated_at?: string | null;
+}
+
+export interface PublicRepoListItem {
+ id: string;
+ link: string;
+ name: string;
+ updated_at?: string | null;
+ /** Present only when the `file_count` query param is > 0. */
+ top_files?: PublicRepoTopFile[];
+}
+
+export interface PublicRepoListMeta extends Meta {
+ scope?: string;
+ file_count?: number;
+}
+
+export interface PublicRepoListResponse extends PaginatedResponse {
+ meta?: PublicRepoListMeta;
+}
+
+// ============================================================================
+// Repository Intelligence Types
+// (scope, map, blueprint, radius, review, test-gaps)
+// ============================================================================
+
+export type DependencyType = "package" | "import" | "export" | "env";
+
+export interface DependencyUsage {
+ file_count?: number;
+ total_files?: number;
+ percentage?: number;
+ occurrence_count?: number;
+}
+
+export interface DependencyComparison {
+ status?: "source_only" | "shared" | "comparison_only";
+ repos?: Record[];
+}
+
+export interface DependencyItem {
+ name: string;
+ version?: string | null;
+ type?: string;
+ symbols?: string[];
+ usage?: DependencyUsage;
+ comparison?: DependencyComparison;
+ /** Present only when `include=files`; includes Markdown documentation per file. */
+ files?: Record[];
+ /** Present only when `include=functions`; no control_flow fields. */
+ functions?: Record[];
+}
+
+export interface ScopeSymbolItem {
+ name: string;
+ type?: string;
+ usage?: DependencyUsage;
+ detail?: Record | null;
+}
+
+// ---- Scope (`POST /v2/repos/{repo_id}/scope`) ----
+
+export type RepoScopeKind = "dependencies" | "functions" | "variables";
+export type RepoScopeExpand = "files" | "functions";
+export type RepoScopeOrder = "usage" | "name";
+
+export interface RepoScopeRequest {
+ include?: RepoScopeKind[] | string;
+ name?: string;
+ type?: string;
+ expand?: RepoScopeExpand[] | string;
+ compare_repos?: string[] | string;
+ limit?: number;
+ offset?: number;
+ order?: RepoScopeOrder;
+}
+
+export interface RepoScopeSummary {
+ total_files?: number;
+ files_with_dependencies?: number;
+ dependency_count?: number;
+ function_count?: number;
+ variable_count?: number;
+ compared_repo_count?: number;
+}
+
+export interface RepoScopeCollections {
+ dependencies?: DependencyItem[];
+ functions?: ScopeSymbolItem[];
+ variables?: ScopeSymbolItem[];
+}
+
+export interface RepoScopeData {
+ repo?: Record;
+ summary?: RepoScopeSummary;
+ scope?: RepoScopeCollections;
+}
+
+export interface RepoScopePagination {
+ dependencies?: Pagination;
+ functions?: Pagination;
+ variables?: Pagination;
+}
+
+export interface RepoScopeMeta extends Meta {
+ gated_kinds?: Array<"functions" | "variables">;
+}
+
+export interface RepoScopeResponse extends BaseResponse {
+ pagination?: RepoScopePagination;
+ meta?: RepoScopeMeta;
+}
+
+// ---- Cross-repository dependency map (`GET /v2/repos/{repo_id}/map`) ----
+
+export interface RepoMapOptions {
+ dependency?: string;
+ type?: DependencyType;
+ /** Comma-separated repository UUIDs (max 10). */
+ compare_repos?: string;
+ include_public_index?: boolean;
+ limit?: number;
+ files_per_repo?: number;
+ demo?: boolean;
+}
+
+export interface RepoMapData {
+ source_repo?: Record;
+ scope?: Record;
+ dependencies?: Record[];
+}
+
+export interface RepoMapResponse extends BaseResponse {}
+
+// ---- Blueprint (`GET /v2/repos/{repo_id}/blueprint`) ----
+
+export interface RepoBlueprintRepo {
+ id?: string;
+ name?: string;
+ link?: string;
+ description?: string | null;
+ [key: string]: unknown;
+}
+
+export interface RepoBlueprintSymbols {
+ top_functions?: ScopeSymbolItem[];
+ top_variables?: ScopeSymbolItem[];
+}
+
+export interface RepoBlueprintData {
+ id?: string | null;
+ file_name?: string;
+ file_path?: string;
+ github_url?: string | null;
+ repo?: RepoBlueprintRepo;
+ /** Markdown README documentation. */
+ documentation?: string;
+ /** Raw README file content. */
+ data?: string | null;
+ dependencies?: DependencyItem[];
+ symbols?: RepoBlueprintSymbols;
+ /** Aggregated conventions (null below PRO tier). */
+ conventions?: Record | null;
+ created_at?: string | null;
+ updated_at?: string | null;
+}
+
+export interface RepoBlueprintMeta extends Meta {
+ conventions_tier_gated?: boolean;
+ /** true on the `/readme` alias only. */
+ deprecated?: boolean;
+}
+
+export interface RepoBlueprintResponse extends BaseResponse {
+ meta?: RepoBlueprintMeta;
+}
+
+// ---- Blast radius (`POST /v2/repos/{repo_id}/radius`) ----
+
+export type DataFlowKind = "http_input" | "env_config" | "filesystem" | "cli_input" | "internal";
+
+export interface DataFlowProjection {
+ id?: string | null;
+ risk_level?: string;
+ source_kind?: DataFlowKind;
+ sink_kind?: DataFlowKind;
+}
+
+export interface CallEdgeProjection {
+ caller?: string;
+ callee?: string;
+ call_type?: string;
+}
+
+export interface RepoRadiusRequest {
+ file?: string;
+ files?: string[];
+ dependency?: string;
+ dependencies?: string[];
+ include?: string[];
+ limit?: number;
+}
+
+export interface RepoRadiusTarget {
+ data_flows?: DataFlowProjection[];
+ entry_points?: string[];
+ call_edges?: CallEdgeProjection[];
+ /** ADMIN only. */
+ admin_enrichment?: Record;
+ [key: string]: unknown;
+}
+
+export interface RepoRadiusData {
+ summary?: Record;
+ targets?: RepoRadiusTarget[];
+}
+
+export interface RepoRadiusResponse extends BaseResponse {}
+
+// ---- Code review (`GET /v2/repos/{repo_id}/review`) ----
+
+export type RepoReviewOrder = "risk" | "complexity" | "coverage" | "debt";
+
+export interface ReviewFileMetrics {
+ cognitive_complexity?: number;
+ cyclomatic_complexity?: number;
+ duplication_percentage?: number;
+ duplicate_block_count?: number;
+ technical_debt_minutes?: number;
+ estimated_coverage?: number | null;
+ churn_rate?: string;
+}
+
+export interface ReviewFile extends ReviewFileMetrics {
+ id?: string;
+ file_name?: string;
+ file_path?: string;
+ verdict?: string;
+ risk_score?: number;
+ triggered_rules?: Record[];
+ admin_enrichment?: Record;
+}
+
+export interface RepoReviewData {
+ summary?: Record;
+ files?: ReviewFile[];
+}
+
+export interface RepoReviewResponse extends BaseResponse {
+ pagination?: Pagination;
+}
+
+// ---- Test gaps (`GET /v2/repos/{repo_id}/test-gaps`) ----
+
+export type TestGapPriority = "high" | "medium" | "low";
+
+export interface TestCaseProjection {
+ priority?: TestGapPriority;
+ description?: string;
+ target_function?: string;
+}
+
+export interface TestGapProjection {
+ risk_score?: number;
+ estimated_coverage?: number;
+ churn_rate?: string;
+ security_risk_level?: string;
+ suggested_test_cases?: TestCaseProjection[];
+ mockable_dependencies?: string[];
+ hard_to_test_reasons?: string[];
+}
+
+export interface TestGapFile extends TestGapProjection {
+ id?: string;
+ file_name?: string;
+ file_path?: string;
+}
+
+export interface RepoTestGapsData {
+ summary?: Record;
+ gaps?: TestGapFile[];
+}
+
+export interface RepoTestGapsResponse extends BaseResponse {
+ pagination?: Pagination;
+}
+
// ============================================================================
// File Types
// ============================================================================
@@ -592,24 +888,77 @@ export interface ChatResponse extends BaseResponse {
}
// ============================================================================
-// V1 Models Types (no V2 equivalent)
+// Models Types (`GET /v2/models`, `GET /v2/models/limits`)
// ============================================================================
+export interface AIModelCreditCost {
+ /** null means use tier credit_search_base from GET /v2/models/limits. */
+ search_base?: number | null;
+ search_base_from_tier?: boolean;
+ chat_min?: number | null;
+ chat_min_from_tier?: boolean;
+}
+
export interface AIModel {
id: string;
name: string;
provider: string;
- /** Present on normalized MCP catalog entries. */
+ /** Curated tier requirement for the model. */
tier_required?: TierName;
- /** Raw `/v1/fundi/models` entries from OpenRouter catalog mapping. */
+ /** Raw catalog entries from the OpenRouter catalog mapping. */
tier_requirements?: {
premium_only?: boolean;
default_model?: boolean;
- preview_mode?: unknown;
+ preview_mode?: Record | null;
};
- context_length?: number;
+ context_length?: number | null;
+ /** Search/research credit costs (present on the V2 curated catalog). */
+ credit_cost?: AIModelCreditCost;
}
+/** `GET /v2/models` returns the curated catalog wrapped under `data.models`. */
+export interface V2ModelsListData {
+ models: AIModel[];
+}
+
+export interface V2ModelsListResponse extends BaseResponse {}
+
+/** Normalized shape returned by the client for MCP tools. */
export interface ModelsResponse {
models: AIModel[];
}
+
+export interface ModelLimitsSubscription {
+ tokens?: number;
+ bonus_tokens?: number;
+ expiry_date?: string;
+}
+
+export interface ModelLimitsModel {
+ name?: string;
+ provider?: string;
+ max_tokens?: number;
+ context_length?: number;
+ vector_search_length?: number;
+ knowledge_search_length?: number;
+ knowledge_storage_limit?: number;
+}
+
+export interface ModelLimitsLimits {
+ history_days?: number;
+ history_max_records?: number;
+ repos_max?: number;
+ files_per_repo_max?: number;
+ stats_range_max_days?: number;
+ can_access_org_repos?: boolean;
+ can_share_to_org?: boolean;
+}
+
+export interface ModelLimitsData {
+ tier?: string;
+ subscription?: ModelLimitsSubscription;
+ model?: ModelLimitsModel | null;
+ limits?: ModelLimitsLimits;
+}
+
+export interface ModelLimitsResponse extends BaseResponse {}
diff --git a/tests/client.test.ts b/tests/client.test.ts
index 3f53663..c2a4696 100644
--- a/tests/client.test.ts
+++ b/tests/client.test.ts
@@ -109,6 +109,168 @@ describe("CodeFundiClient", () => {
it("getModels should throw without API key", async () => {
await expect(noKeyClient.getModels()).rejects.toThrow(CodeFundiApiError);
});
+
+ it("getModelLimits should throw without API key", async () => {
+ await expect(noKeyClient.getModelLimits()).rejects.toThrow(CodeFundiApiError);
+ });
+
+ it("getRepoScope should throw without API key (non-demo)", async () => {
+ await expect(noKeyClient.getRepoScope("repo-id")).rejects.toThrow(CodeFundiApiError);
+ });
+
+ it("getRepoMap should throw without API key (non-demo)", async () => {
+ await expect(noKeyClient.getRepoMap("repo-id")).rejects.toThrow(CodeFundiApiError);
+ });
+
+ it("getRepoBlueprint should throw without API key (non-demo)", async () => {
+ await expect(noKeyClient.getRepoBlueprint("repo-id")).rejects.toThrow(CodeFundiApiError);
+ });
+
+ it("getRepoRadius should throw without API key (non-demo)", async () => {
+ await expect(noKeyClient.getRepoRadius("repo-id", { file: "src/index.ts" })).rejects.toThrow(CodeFundiApiError);
+ });
+
+ it("getRepoReview should throw without API key (non-demo)", async () => {
+ await expect(noKeyClient.getRepoReview("repo-id")).rejects.toThrow(CodeFundiApiError);
+ });
+
+ it("getRepoTestGaps should throw without API key (non-demo)", async () => {
+ await expect(noKeyClient.getRepoTestGaps("repo-id")).rejects.toThrow(CodeFundiApiError);
+ });
+ });
+
+ // ==== V2 repository intelligence & demo mode ====
+
+ describe("V2 repository intelligence", () => {
+ it("listPublicRepos should not require an API key", async () => {
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({ status: "success", data: [], pagination: { total: 0, limit: 100, offset: 0, has_more: false } }),
+ });
+ vi.stubGlobal("fetch", mockFetch);
+
+ const noKeyClient = new CodeFundiClient("https://api.test.codefundi.app");
+ await noKeyClient.listPublicRepos({ limit: 10, file_count: 3 });
+
+ const [url, options] = mockFetch.mock.calls[0];
+ expect(url).toContain("/v2/public/repos");
+ expect(url).toContain("limit=10");
+ expect(url).toContain("file_count=3");
+ expect(options.method).toBe("GET");
+ expect(options.headers["X-API-Key"]).toBeUndefined();
+
+ vi.unstubAllGlobals();
+ });
+
+ it("listPublicRepos should send X-Internal-Secret header when provided", async () => {
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({ status: "success", data: [] }),
+ });
+ vi.stubGlobal("fetch", mockFetch);
+
+ const noKeyClient = new CodeFundiClient("https://api.test.codefundi.app");
+ await noKeyClient.listPublicRepos({ internalSecret: "s3cr3t" });
+
+ const [url, options] = mockFetch.mock.calls[0];
+ expect(url).not.toContain("internalSecret");
+ expect(options.headers["X-Internal-Secret"]).toBe("s3cr3t");
+
+ vi.unstubAllGlobals();
+ });
+
+ it("getRepoScope should POST to the scope endpoint with a JSON body", async () => {
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({ status: "success", data: { scope: {} } }),
+ });
+ vi.stubGlobal("fetch", mockFetch);
+
+ await client.getRepoScope("repo-id", { include: ["dependencies"], name: "express" });
+
+ const [url, options] = mockFetch.mock.calls[0];
+ expect(url).toBe("https://api.test.codefundi.app/v2/repos/repo-id/scope");
+ expect(options.method).toBe("POST");
+ const body = JSON.parse(options.body as string);
+ expect(body.include).toEqual(["dependencies"]);
+ expect(body.name).toBe("express");
+
+ vi.unstubAllGlobals();
+ });
+
+ it("getRepoScope should run in demo mode without an API key", async () => {
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({ status: "success", data: { scope: {} } }),
+ });
+ vi.stubGlobal("fetch", mockFetch);
+
+ const noKeyClient = new CodeFundiClient("https://api.test.codefundi.app");
+ await noKeyClient.getRepoScope("repo-id", {}, true);
+
+ const [url, options] = mockFetch.mock.calls[0];
+ expect(url).toContain("/v2/repos/repo-id/scope");
+ expect(url).toContain("demo=true");
+ expect(options.method).toBe("POST");
+
+ vi.unstubAllGlobals();
+ });
+
+ it("getRepoReview should build query parameters", async () => {
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({ status: "success", data: { files: [] } }),
+ });
+ vi.stubGlobal("fetch", mockFetch);
+
+ await client.getRepoReview("repo-id", { verdict: "warn,block", order: "risk", limit: 10 });
+
+ const [url, options] = mockFetch.mock.calls[0];
+ expect(url).toContain("/v2/repos/repo-id/review");
+ expect(url).toContain("verdict=warn%2Cblock");
+ expect(url).toContain("order=risk");
+ expect(url).toContain("limit=10");
+ expect(options.method).toBe("GET");
+
+ vi.unstubAllGlobals();
+ });
+
+ it("getRepoTestGaps should include min_risk=0 and untested_only=false", async () => {
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({ status: "success", data: { gaps: [] } }),
+ });
+ vi.stubGlobal("fetch", mockFetch);
+
+ await client.getRepoTestGaps("repo-id", { min_risk: 0, untested_only: false });
+
+ const [url] = mockFetch.mock.calls[0];
+ expect(url).toContain("min_risk=0");
+ expect(url).toContain("untested_only=false");
+
+ vi.unstubAllGlobals();
+ });
+
+ it("getModels should GET /v2/models and normalize data.models", async () => {
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({
+ status: "success",
+ data: { models: [{ id: "gpt-4", name: "GPT-4", provider: "openai", tier_required: "PRO" }] },
+ }),
+ });
+ vi.stubGlobal("fetch", mockFetch);
+
+ const res = await client.getModels();
+
+ const [url, options] = mockFetch.mock.calls[0];
+ expect(url).toBe("https://api.test.codefundi.app/v2/models");
+ expect(options.method).toBe("GET");
+ expect(res.models).toHaveLength(1);
+ expect(res.models[0].id).toBe("gpt-4");
+
+ vi.unstubAllGlobals();
+ });
});
// ==== Auth methods do NOT require API key ====
diff --git a/tests/formatters.test.ts b/tests/formatters.test.ts
index 167d8f8..87caf86 100644
--- a/tests/formatters.test.ts
+++ b/tests/formatters.test.ts
@@ -9,12 +9,17 @@ import {
formatFileList, formatFileDocumentation, formatHistoryList,
formatHistoryDetail, formatConversation,
formatUsageStats, formatActivityStats, formatLanguageStats,
- formatApiKeys, formatModels, formatError,
+ formatApiKeys, formatModels, formatModelLimits, formatError,
+ formatPublicRepoList, formatRepoScope, formatRepoMap,
+ formatRepoBlueprint, formatRepoRadius, formatRepoReview,
+ formatRepoTestGaps,
} from "../src/formatters.js";
import { CodeFundiApiError } from "../src/client.js";
import type {
SearchResult, Repository, FileListItem, HistoryItem,
FileDocumentationData, ReadmeData, RepositoryIndexInitRepo,
+ PublicRepoListItem, RepoScopeData, RepoMapData, RepoBlueprintData,
+ RepoRadiusData, RepoReviewData, RepoTestGapsData, ModelLimitsData,
} from "../src/types.js";
// ==== Search ====
@@ -228,6 +233,190 @@ describe("formatModels", () => {
});
});
+describe("formatModelLimits", () => {
+ it("should format tier, subscription, model, and limits", () => {
+ const data: ModelLimitsData = {
+ tier: "PRO",
+ subscription: { tokens: 100000, bonus_tokens: 5000, expiry_date: "2026-12-31T00:00:00Z" },
+ model: { name: "GPT-4o", provider: "openai", max_tokens: 4096, context_length: 128000 },
+ limits: { repos_max: 50, files_per_repo_max: 2000, history_days: 90, can_share_to_org: true },
+ };
+ const result = formatModelLimits(data);
+ expect(result).toContain("PRO");
+ expect(result).toContain("100000");
+ expect(result).toContain("GPT-4o");
+ expect(result).toContain("Max repos:** 50");
+ expect(result).toContain("Can share to org:** true");
+ });
+});
+
+// ==== Public Repos ====
+
+describe("formatPublicRepoList", () => {
+ it("should format empty catalog", () => {
+ const result = formatPublicRepoList([]);
+ expect(result).toContain("No public repositories found");
+ });
+
+ it("should format public repos with top files", () => {
+ const repos: PublicRepoListItem[] = [{
+ id: "p1", name: "public-repo", link: "https://github.com/org/public-repo",
+ updated_at: "2026-01-01T00:00:00Z",
+ top_files: [{ id: "f1", file_name: "index.ts", file_path: "src/index.ts", file_size_kb: 12 }],
+ }];
+ const result = formatPublicRepoList(repos, { total: 1, limit: 100, offset: 0, has_more: false });
+ expect(result).toContain("public-repo");
+ expect(result).toContain("p1");
+ expect(result).toContain("src/index.ts");
+ expect(result).toContain("12 KB");
+ });
+});
+
+// ==== Repository Intelligence ====
+
+describe("formatRepoScope", () => {
+ it("should format dependencies, functions, variables and pagination hints", () => {
+ const data: RepoScopeData = {
+ repo: { name: "my-repo" },
+ summary: { total_files: 100, dependency_count: 3, function_count: 2, variable_count: 1 },
+ scope: {
+ dependencies: [{ name: "express", version: "4.18.0", type: "package", usage: { file_count: 10, percentage: 25 } }],
+ functions: [{ name: "handler", type: "function", usage: { file_count: 4 } }],
+ variables: [{ name: "PORT", type: "env" }],
+ },
+ };
+ const result = formatRepoScope(data, { dependencies: { total: 20, limit: 1, offset: 0, has_more: true } }, ["variables"]);
+ expect(result).toContain("my-repo");
+ expect(result).toContain("**express**@4.18.0");
+ expect(result).toContain("used in 10 file(s)");
+ expect(result).toContain("handler");
+ expect(result).toContain("PORT");
+ expect(result).toContain("Gated");
+ expect(result).toContain("More available");
+ });
+
+ it("should handle empty scope", () => {
+ const result = formatRepoScope({ scope: {} });
+ expect(result).toContain("No scope data found");
+ });
+});
+
+describe("formatRepoMap", () => {
+ it("should format map dependencies", () => {
+ const data: RepoMapData = {
+ source_repo: { name: "core" },
+ dependencies: [{ name: "lodash", type: "package", repos: [{ id: "r1" }, { id: "r2" }] }],
+ };
+ const result = formatRepoMap(data);
+ expect(result).toContain("core");
+ expect(result).toContain("lodash");
+ expect(result).toContain("Present in 2 repo(s)");
+ });
+
+ it("should handle empty map", () => {
+ const result = formatRepoMap({ dependencies: [] });
+ expect(result).toContain("No dependency map entries found");
+ });
+});
+
+describe("formatRepoBlueprint", () => {
+ it("should format dependencies, symbols, conventions gating, and README", () => {
+ const data: RepoBlueprintData = {
+ file_name: "README.md",
+ github_url: "https://github.com/org/repo",
+ repo: { name: "repo" },
+ documentation: "# My Project",
+ dependencies: [{ name: "react", version: "18.0.0", type: "package", usage: { file_count: 30 } }],
+ symbols: { top_functions: [{ name: "main" }], top_variables: [{ name: "config" }] },
+ conventions: null,
+ };
+ const result = formatRepoBlueprint(data, { conventions_tier_gated: true });
+ expect(result).toContain("repo — Blueprint");
+ expect(result).toContain("**react**@18.0.0");
+ expect(result).toContain("main");
+ expect(result).toContain("config");
+ expect(result).toContain("require PRO tier");
+ expect(result).toContain("# My Project");
+ });
+
+ it("should note deprecation when meta.deprecated is set", () => {
+ const result = formatRepoBlueprint({ repo: { name: "repo" } }, { deprecated: true });
+ expect(result).toContain("deprecated");
+ });
+});
+
+describe("formatRepoRadius", () => {
+ it("should format targets with data flows and call edges", () => {
+ const data: RepoRadiusData = {
+ summary: { impacted_files: 3 },
+ targets: [{
+ entry_points: ["src/server.ts"],
+ data_flows: [{ source_kind: "http_input", sink_kind: "filesystem", risk_level: "high" }],
+ call_edges: [{ caller: "handler", callee: "writeFile", call_type: "direct" }],
+ }],
+ };
+ const result = formatRepoRadius(data);
+ expect(result).toContain("Blast Radius Analysis");
+ expect(result).toContain("src/server.ts");
+ expect(result).toContain("http_input → filesystem");
+ expect(result).toContain("handler → writeFile");
+ });
+
+ it("should handle no targets", () => {
+ const result = formatRepoRadius({ targets: [] });
+ expect(result).toContain("No impact targets found");
+ });
+});
+
+describe("formatRepoReview", () => {
+ it("should format the file metrics table", () => {
+ const data: RepoReviewData = {
+ summary: { blockers: 1 },
+ files: [{
+ id: "rf1", file_path: "src/app.ts", verdict: "block", risk_score: 0.9,
+ cyclomatic_complexity: 22, estimated_coverage: null, technical_debt_minutes: 120,
+ }],
+ };
+ const result = formatRepoReview(data);
+ expect(result).toContain("Code Review Signals");
+ expect(result).toContain("src/app.ts");
+ expect(result).toContain("block");
+ expect(result).toContain("N/A");
+ expect(result).toContain("120");
+ });
+
+ it("should handle no files", () => {
+ const result = formatRepoReview({ files: [] });
+ expect(result).toContain("No review signals found");
+ });
+});
+
+describe("formatRepoTestGaps", () => {
+ it("should format gaps with suggested test cases", () => {
+ const data: RepoTestGapsData = {
+ gaps: [{
+ id: "g1", file_path: "src/auth.ts", risk_score: 0.8, estimated_coverage: 0.2,
+ security_risk_level: "high",
+ suggested_test_cases: [{ priority: "high", description: "Test invalid token", target_function: "verify" }],
+ mockable_dependencies: ["jwt"],
+ hard_to_test_reasons: ["network calls"],
+ }],
+ };
+ const result = formatRepoTestGaps(data);
+ expect(result).toContain("Test Coverage Gaps");
+ expect(result).toContain("src/auth.ts");
+ expect(result).toContain("Test invalid token");
+ expect(result).toContain("verify");
+ expect(result).toContain("jwt");
+ expect(result).toContain("network calls");
+ });
+
+ it("should handle no gaps", () => {
+ const result = formatRepoTestGaps({ gaps: [] });
+ expect(result).toContain("No test gaps found");
+ });
+});
+
// ==== Errors ====
describe("formatError", () => {
From d24010a91c89c9a53d13f931a884fd71730dbacb Mon Sep 17 00:00:00 2001
From: FelixWaweru
Date: Wed, 15 Jul 2026 17:26:58 +0300
Subject: [PATCH 2/6] fix: build fixes
---
pnpm-lock.yaml | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1e94acc..a0bf03a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -12,8 +12,8 @@ importers:
specifier: ^4.0.0
version: 4.0.1
zod:
- specifier: ^3.24.0
- version: 3.25.76
+ specifier: ^4.0.0
+ version: 4.3.6
devDependencies:
'@types/node':
specifier: ^22.0.0
@@ -1327,9 +1327,6 @@ packages:
peerDependencies:
zod: ^3.25.28 || ^4
- zod@3.25.76:
- resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
-
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
@@ -2506,6 +2503,4 @@ snapshots:
dependencies:
zod: 4.3.6
- zod@3.25.76: {}
-
zod@4.3.6: {}
From 5ef321d97a69ed741a5e5ede23b3797684866aee Mon Sep 17 00:00:00 2001
From: FelixWaweru
Date: Wed, 15 Jul 2026 17:44:48 +0300
Subject: [PATCH 3/6] feat: updated docs in README
---
README.md | 50 ++++++++++++++++++++++++++------------------------
1 file changed, 26 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index 24faa94..8597348 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
Code Fundi
- The Codebase Context Layer for Agents, Teams and Applications.
+ The Codebase Map for AI Agents, Teams and Applications.
@@ -29,20 +29,22 @@
# Code-Fundi MCP Server
-A production-grade [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for the [Code-Fundi](https://api.codefundi.app) API. Enables any MCP-compatible AI assistant — Claude, Cursor, Windsurf, VS Code Copilot, and more — to search, research, index, and scan code repositories through natural language.
+A production-grade [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for the [Code-Fundi](https://api.codefundi.app) API: a codebase map and blast-radius engine for AI coding agents. Enables any MCP-compatible AI assistant (Claude, Claude Code, Cursor, Windsurf, VS Code Copilot, and more) to search, research, index, and scan code repositories through natural language, and see what a change breaks before it ships.
Built with [FastMCP](https://github.com/punkpeye/fastmcp) (TypeScript) and [Zod](https://zod.dev) for schema validation.
## Features
-- 🔍 **Semantic & grep code search** across indexed repositories
-- 🧠 **AI-powered research** — search + AI analysis in a single call
-- 📦 **Repository management** — index, status, README, listing, public catalog
-- 🛰️ **Repository intelligence** — scope (dependencies/functions/variables), cross-repo dependency map, blueprint, blast radius, code review, and test-gap analysis
-- 📄 **File documentation** — AI-generated docs for any indexed file
-- 📊 **Usage statistics** — query usage, activity, language breakdowns
-- 🔐 **Agent-driven authentication** — sign up/sign in via OTP without pre-configured keys
-- 💬 **AI chat & model insight** — direct conversation with Code-Fundi AI, model catalog, and per-tier limits
+Every tool below is backed by the same codebase map: structural dependencies, call graph, and blast radius, indexed once and queried in milliseconds.
+
+- 🔍 **Semantic & grep code search** across your indexed codebase map
+- 🧠 **AI-powered research**: search plus AI analysis in a single call
+- 📦 **Repository management**: index, status, README, listing, public catalog
+- 🛰️ **Repository intelligence**: scope (dependencies/functions/variables), cross-repo dependency map, blueprint, Blast-Radius Guard (impact analysis before you merge), code review, and test-gap analysis
+- 📄 **File documentation**: AI-generated docs for any indexed file
+- 📊 **Usage statistics**: query usage, activity, language breakdowns
+- 🔐 **Agent-driven authentication**: sign up/sign in via OTP without pre-configured keys
+- 💬 **AI chat & model insight**: direct conversation with Code-Fundi AI, model catalog, and per-tier limits
## Quick Start
@@ -77,11 +79,11 @@ Set your API key as an environment variable:
export CODEFUNDI_API_KEY=your_api_key_here
```
-Or skip this step — agents can authenticate dynamically using the `code-fundi-auth-*` tools.
+Or skip this step, agents can authenticate dynamically using the `code-fundi-auth-*` tools.
### Use with Claude Desktop
-After a global install (`npm i -g @codefundi/code-fundi-mcp`), point MCP at the published binary — **no path to `dist/index.js` required**:
+After a global install (`npm i -g @codefundi/code-fundi-mcp`), point MCP at the published binary (no path to `dist/index.js` required):
```json
{
@@ -114,7 +116,7 @@ If the binary is not on your `PATH`, use `npx` (downloads or uses the local pack
### Use with Cursor
-Same pattern as Claude — `command` + optional `args` only; no manual path to the repo:
+Same pattern as Claude: `command` plus optional `args` only, no manual path to the repo:
```json
{
@@ -142,33 +144,33 @@ npx fastmcp dev src/index.ts # Test with MCP CLI
## Tools Reference (30 tools)
-Covers the Code-Fundi **V2** API: search (including search-with-chat / research), repositories (list, index, status, readme, public catalog), repository intelligence (scope, map, blueprint, radius, review, test-gaps), files, history, statistics, API keys, authentication, plus **Fundi chat** (`POST /v1/fundi/chat`) and the V2 model catalog / limits (`GET /v2/models`, `GET /v2/models/limits`).
+Covers the Code-Fundi **V2** API for codebase mapping and blast-radius analysis: search (including search-with-chat / research), repositories (list, index, status, readme, public catalog), repository intelligence (scope, map, blueprint, radius, review, test-gaps), files, history, statistics, API keys, authentication, plus **Fundi chat** (`POST /v1/fundi/chat`) and the V2 model catalog / limits (`GET /v2/models`, `GET /v2/models/limits`).
### Search
| Tool | Description |
|------|-------------|
-| `code-fundi-search` | Semantic/grep search across repositories with filters |
-| `code-fundi-research` | Search + AI-synthesized analysis of matching code |
+| `code-fundi-search` | Semantic and grep search across your indexed codebase map, with filters |
+| `code-fundi-research` | Search plus AI-synthesized analysis of matching code |
### Repositories
| Tool | Description |
|------|-------------|
| `code-fundi-list-repos` | List indexed repositories with pagination |
-| `code-fundi-index-repo` | Index a new GitHub repository |
+| `code-fundi-index-repo` | Index a new GitHub repository into your codebase map |
| `code-fundi-repo-status` | Check repository indexing status |
-| `code-fundi-repo-readme` | Get repository README documentation (deprecated — prefer blueprint) |
+| `code-fundi-repo-readme` | Get repository README documentation (deprecated, prefer blueprint) |
| `code-fundi-list-public-repos` | Browse the global catalog of indexed public repositories (no key needed) |
### Repository Intelligence
| Tool | Description |
|------|-------------|
-| `code-fundi-repo-scope` | Aggregated dependencies, functions, and variables (with drill-down and cross-repo comparison) |
-| `code-fundi-repo-map` | Cross-repository dependency map |
-| `code-fundi-repo-blueprint` | README + dependency/convention overview (successor to repo-readme) |
-| `code-fundi-repo-radius` | Blast radius / file impact analysis (PRO+) |
+| `code-fundi-repo-scope` | Aggregated dependencies, functions, and variables across the codebase map (with drill-down and cross-repo comparison) |
+| `code-fundi-repo-map` | Cross-repository dependency map: how services and packages actually connect |
+| `code-fundi-repo-blueprint` | README plus dependency and convention overview (successor to repo-readme) |
+| `code-fundi-repo-radius` | Blast-Radius Guard: every file and function that breaks before you merge (PRO+) |
| `code-fundi-repo-review` | Per-file code review signals: verdict, risk, complexity, coverage, debt (PRO+) |
| `code-fundi-repo-test-gaps` | Test coverage gap analysis with suggested test cases (PRO+) |
@@ -229,7 +231,7 @@ When no API key is set, agents can self-authenticate:
1. Call `code-fundi-auth-authenticate` with email and `auth_mode: "otp"`
2. User receives an OTP code via email
3. Call `code-fundi-auth-verify` with the 6-digit code
-4. API key is automatically configured — all tools now work
+4. API key is automatically configured, all tools now work
This enables fully autonomous agent setup without manual configuration.
@@ -244,4 +246,4 @@ This enables fully autonomous agent setup without manual configuration.
## License
-MIT
+MIT
\ No newline at end of file
From d4676bb7cc79dffc3d6846666230a3b09884412c Mon Sep 17 00:00:00 2001
From: FelixWaweru
Date: Wed, 15 Jul 2026 17:46:09 +0300
Subject: [PATCH 4/6] feat: updated package
---
package.json | 6 ------
1 file changed, 6 deletions(-)
diff --git a/package.json b/package.json
index 1638838..e49d21a 100644
--- a/package.json
+++ b/package.json
@@ -36,12 +36,6 @@
"author": "Code Fundi ",
"type": "module",
"main": "dist/index.js",
- "repository": {
- "type": "git",
- "url": "https://github.com/Code-Fundi/code-fundi-mcp"
- },
- "homepage": "https://codefundi.app",
- "author": "Code Fundi",
"bin": {
"code-fundi-mcp": "dist/index.js"
},
From 231e87f5b93fb011c3f1c48ae8b0292040488ebe Mon Sep 17 00:00:00 2001
From: FelixWaweru
Date: Wed, 15 Jul 2026 18:09:57 +0300
Subject: [PATCH 5/6] feat: updated documentation
---
README.md | 103 ++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 85 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 8597348..4810ad0 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,75 @@ A production-grade [Model Context Protocol (MCP)](https://modelcontextprotocol.i
Built with [FastMCP](https://github.com/punkpeye/fastmcp) (TypeScript) and [Zod](https://zod.dev) for schema validation.
+## Getting started for AI agents
+
+If you are an AI agent (or configuring one) and **no `CODEFUNDI_API_KEY` is set**, you must self-authenticate before using search, repos, chat, or other authenticated tools. The auth tools (`code-fundi-auth-*`) work without a pre-existing key.
+
+### Which path to use
+
+| Situation | Action |
+|-----------|--------|
+| `CODEFUNDI_API_KEY` is set in MCP config | Use any tool immediately — the server sends it as `X-API-Key` on every request |
+| No API key configured | Run the **OTP self-auth flow** below (recommended) or **password sign-in** |
+
+### Zero-config MCP setup (no API key)
+
+You can start the MCP server without `CODEFUNDI_API_KEY` and authenticate at runtime:
+
+```json
+{
+ "mcpServers": {
+ "code-fundi": {
+ "command": "npx",
+ "args": ["-y", "@codefundi/code-fundi-mcp"]
+ }
+ }
+}
+```
+
+### Self-authenticate with OTP (recommended)
+
+Code-Fundi uses Supabase-backed auth (`POST /v2/auth/authenticate`, `/v2/auth/verify`, `/v2/auth/resend`). OTP emails contain a **6-digit code** — magic links are not supported on this path.
+
+1. **Ask the human user for their email address.**
+2. Call **`code-fundi-auth-authenticate`** with:
+ - `auth_mode`: `"otp"`
+ - `email`: the user's email
+ - `should_create_user`: `true` for a **new** account, `false` for a **returning** user
+3. Tell the user to check their inbox for a 6-digit code. The API may return `verification_required: true` and `api_key.key_state: "agent_pending"` until verification completes.
+4. **Ask the user for the 6-digit OTP** (human-in-the-loop — you cannot guess or bypass this step).
+5. Call **`code-fundi-auth-verify`** with the same `email` and the `token` (6 digits).
+6. On success, the MCP server **automatically configures the API key in memory** for all subsequent tool calls in this session.
+
+If the code expired or was not received, call **`code-fundi-auth-resend`** with the same `email`, then repeat step 5.
+
+**Example dialogue:**
+
+```
+Agent: What email should I use to sign in to Code-Fundi?
+User: dev@example.com
+Agent: [calls code-fundi-auth-authenticate] I've sent a 6-digit code to dev@example.com. Please paste it here.
+User: 482913
+Agent: [calls code-fundi-auth-verify] You're signed in. I can now search and index your repositories.
+```
+
+### Password sign-in (alternative)
+
+For existing accounts with a password, call **`code-fundi-auth-authenticate`** with `auth_mode: "password"`, the user's `email`, `should_create_user: false`, and the `password` parameter. The MCP client sends the password only in the `X-CodeFundi-Auth-Password` header (never in the JSON body). Production requires HTTPS. Returning users may receive an active API key immediately without a separate verify step.
+
+### After authentication
+
+- The API key is held **in memory** for the lifetime of the MCP server process. It is **not** persisted across IDE or MCP restarts.
+- Recommend the user add the key to their MCP config as `CODEFUNDI_API_KEY` so future sessions start authenticated.
+- A FREE-tier account and API key are created automatically on first signup.
+
+### Errors
+
+| HTTP status | What to do |
+|-------------|------------|
+| **401** Unauthorized | No valid key — run the OTP flow above, or set `CODEFUNDI_API_KEY` |
+| **429** Too many requests | Auth endpoints are rate-limited per IP; wait for `Retry-After` seconds, then retry |
+
## Features
Every tool below is backed by the same codebase map: structural dependencies, call graph, and blast radius, indexed once and queried in milliseconds.
@@ -73,13 +142,26 @@ npm run build
### Configure
-Set your API key as an environment variable:
+**Option A — API key (fastest):** set your key as an environment variable:
```bash
export CODEFUNDI_API_KEY=your_api_key_here
```
-Or skip this step, agents can authenticate dynamically using the `code-fundi-auth-*` tools.
+**Option B — no API key:** skip the env var and let the agent self-authenticate at runtime. See [Getting started for AI agents](#getting-started-for-ai-agents).
+
+Zero-config MCP example (no `env` block):
+
+```json
+{
+ "mcpServers": {
+ "code-fundi": {
+ "command": "npx",
+ "args": ["-y", "@codefundi/code-fundi-mcp"]
+ }
+ }
+}
+```
### Use with Claude Desktop
@@ -218,22 +300,7 @@ Covers the Code-Fundi **V2** API for codebase mapping and blast-radius analysis:
## Authentication
-The server supports two authentication modes:
-
-### Pre-configured API Key (Recommended)
-
-Set `CODEFUNDI_API_KEY` in your MCP config environment. All tools work immediately.
-
-### Agent-Driven Auth (Dynamic)
-
-When no API key is set, agents can self-authenticate:
-
-1. Call `code-fundi-auth-authenticate` with email and `auth_mode: "otp"`
-2. User receives an OTP code via email
-3. Call `code-fundi-auth-verify` with the 6-digit code
-4. API key is automatically configured, all tools now work
-
-This enables fully autonomous agent setup without manual configuration.
+Two modes: **pre-configured API key** (`CODEFUNDI_API_KEY` in MCP config) or **agent-driven OTP/password auth** at runtime. Full step-by-step instructions, tool names, and error handling are in [Getting started for AI agents](#getting-started-for-ai-agents) at the top of this README.
## Environment Variables
From 669c6dcc317af93ddeb366076c41782ee1dada15 Mon Sep 17 00:00:00 2001
From: FelixWaweru
Date: Fri, 17 Jul 2026 12:46:29 +0300
Subject: [PATCH 6/6] feat: scope refinement
---
README.md | 9 +--
src/client.ts | 34 +---------
src/formatters.ts | 141 ++-------------------------------------
src/tools/auth.ts | 20 +++---
src/tools/intel.ts | 96 +-------------------------
src/types.ts | 126 +---------------------------------
tests/client.test.ts | 84 -----------------------
tests/formatters.test.ts | 87 ++----------------------
8 files changed, 31 insertions(+), 566 deletions(-)
diff --git a/README.md b/README.md
index 4810ad0..8f80bec 100644
--- a/README.md
+++ b/README.md
@@ -109,7 +109,7 @@ Every tool below is backed by the same codebase map: structural dependencies, ca
- 🔍 **Semantic & grep code search** across your indexed codebase map
- 🧠 **AI-powered research**: search plus AI analysis in a single call
- 📦 **Repository management**: index, status, README, listing, public catalog
-- 🛰️ **Repository intelligence**: scope (dependencies/functions/variables), cross-repo dependency map, blueprint, Blast-Radius Guard (impact analysis before you merge), code review, and test-gap analysis
+- 🛰️ **Repository intelligence**: cross-repo dependency map, blueprint, Blast-Radius Guard (impact analysis before you merge)
- 📄 **File documentation**: AI-generated docs for any indexed file
- 📊 **Usage statistics**: query usage, activity, language breakdowns
- 🔐 **Agent-driven authentication**: sign up/sign in via OTP without pre-configured keys
@@ -224,9 +224,9 @@ npx fastmcp inspect src/index.ts # Open MCP Inspector UI
npx fastmcp dev src/index.ts # Test with MCP CLI
```
-## Tools Reference (30 tools)
+## Tools Reference (27 tools)
-Covers the Code-Fundi **V2** API for codebase mapping and blast-radius analysis: search (including search-with-chat / research), repositories (list, index, status, readme, public catalog), repository intelligence (scope, map, blueprint, radius, review, test-gaps), files, history, statistics, API keys, authentication, plus **Fundi chat** (`POST /v1/fundi/chat`) and the V2 model catalog / limits (`GET /v2/models`, `GET /v2/models/limits`).
+Covers the Code-Fundi **V2** API for codebase mapping and blast-radius analysis: search (including search-with-chat / research), repositories (list, index, status, readme, public catalog), repository intelligence (map, blueprint, radius), files, history, statistics, API keys, authentication, plus **Fundi chat** (`POST /v1/fundi/chat`) and the V2 model catalog / limits (`GET /v2/models`, `GET /v2/models/limits`).
### Search
@@ -249,12 +249,9 @@ Covers the Code-Fundi **V2** API for codebase mapping and blast-radius analysis:
| Tool | Description |
|------|-------------|
-| `code-fundi-repo-scope` | Aggregated dependencies, functions, and variables across the codebase map (with drill-down and cross-repo comparison) |
| `code-fundi-repo-map` | Cross-repository dependency map: how services and packages actually connect |
| `code-fundi-repo-blueprint` | README plus dependency and convention overview (successor to repo-readme) |
| `code-fundi-repo-radius` | Blast-Radius Guard: every file and function that breaks before you merge (PRO+) |
-| `code-fundi-repo-review` | Per-file code review signals: verdict, risk, complexity, coverage, debt (PRO+) |
-| `code-fundi-repo-test-gaps` | Test coverage gap analysis with suggested test cases (PRO+) |
### Files
diff --git a/src/client.ts b/src/client.ts
index 9357f8c..50d580a 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -18,10 +18,9 @@ import type {
ChatRequest, ChatResponse, ModelsResponse, V2ModelsListResponse,
ModelLimitsResponse, SearchFieldsParam,
SortOrder, StatsRange, RepoScope, HistoryCategory, ErrorResponse,
- PublicRepoListResponse, RepoScopeRequest, RepoScopeResponse,
+ PublicRepoListResponse,
RepoMapOptions, RepoMapResponse, RepoBlueprintResponse,
- RepoRadiusRequest, RepoRadiusResponse, RepoReviewResponse, RepoReviewOrder,
- RepoTestGapsResponse, TestGapPriority,
+ RepoRadiusRequest, RepoRadiusResponse,
} from "./types.js";
// ============================================================================
@@ -267,15 +266,6 @@ export class CodeFundiClient {
// ==== V2 Repository Intelligence ====
- async getRepoScope(repoKey: string, body: RepoScopeRequest = {}, demo?: boolean): Promise {
- this.requireKeyUnlessDemo(demo);
- const qs = buildQuery({ demo: demo || undefined });
- return this.request(
- `/v2/repos/${encodeRepoKey(repoKey)}/scope${qs}`,
- { method: "POST", body: JSON.stringify(body) },
- );
- }
-
async getRepoMap(repoKey: string, opts: RepoMapOptions = {}): Promise {
const { demo, ...rest } = opts;
this.requireKeyUnlessDemo(demo);
@@ -302,26 +292,6 @@ export class CodeFundiClient {
);
}
- async getRepoReview(
- repoKey: string,
- opts: { verdict?: string; order?: RepoReviewOrder; limit?: number; offset?: number; demo?: boolean } = {},
- ): Promise {
- const { demo, ...rest } = opts;
- this.requireKeyUnlessDemo(demo);
- const qs = buildQuery({ ...rest, demo: demo || undefined });
- return this.request(`/v2/repos/${encodeRepoKey(repoKey)}/review${qs}`, { method: "GET" });
- }
-
- async getRepoTestGaps(
- repoKey: string,
- opts: { priority?: TestGapPriority; min_risk?: number; untested_only?: boolean; limit?: number; offset?: number; demo?: boolean } = {},
- ): Promise {
- const { demo, ...rest } = opts;
- this.requireKeyUnlessDemo(demo);
- const qs = buildQuery({ ...rest, demo: demo || undefined });
- return this.request(`/v2/repos/${encodeRepoKey(repoKey)}/test-gaps${qs}`, { method: "GET" });
- }
-
// ==== V2 Files ====
async listFiles(repoKey: string, opts: { search?: string | null; limit?: number; offset?: number; order_by?: string; order?: SortOrder } = {}): Promise {
diff --git a/src/formatters.ts b/src/formatters.ts
index 49e0382..03567d7 100644
--- a/src/formatters.ts
+++ b/src/formatters.ts
@@ -10,9 +10,9 @@ import type {
UsageByType, ActivityByDay, LanguageStat,
ApiKey, AIModel, ReadmeData, RepoStatusData,
RepositoryIndexInitRepo, Pagination, TierName,
- PublicRepoListItem, RepoScopeData, RepoScopePagination,
+ PublicRepoListItem,
RepoMapData, RepoBlueprintData, RepoBlueprintMeta,
- RepoRadiusData, RepoReviewData, RepoTestGapsData,
+ RepoRadiusData,
ModelLimitsData,
} from "./types.js";
import { CodeFundiApiError } from "./client.js";
@@ -196,75 +196,9 @@ export function formatPublicRepoList(repos: PublicRepoListItem[], pagination?: P
}
// ============================================================================
-// Repository Intelligence (scope, map, blueprint, radius, review, test-gaps)
+// Repository Intelligence (map, blueprint, radius)
// ============================================================================
-export function formatRepoScope(data: RepoScopeData, pagination?: RepoScopePagination, gatedKinds?: string[]): string {
- const parts: string[] = [];
- const repoName = readString(data.repo, "name");
- parts.push(`## Repository Scope${repoName ? `: ${repoName}` : ""}\n`);
-
- const s = data.summary;
- if (s) {
- const info: string[] = [];
- if (s.total_files !== undefined) info.push(`Files: ${s.total_files}`);
- if (s.dependency_count !== undefined) info.push(`Dependencies: ${s.dependency_count}`);
- if (s.function_count !== undefined) info.push(`Functions: ${s.function_count}`);
- if (s.variable_count !== undefined) info.push(`Variables: ${s.variable_count}`);
- if (s.compared_repo_count) info.push(`Compared repos: ${s.compared_repo_count}`);
- if (info.length) parts.push(info.join(" | ") + "\n");
- }
- if (gatedKinds?.length) parts.push(`_Gated (upgrade to unlock): ${gatedKinds.join(", ")}._\n`);
-
- const deps = data.scope?.dependencies ?? [];
- if (deps.length) {
- parts.push(`### Dependencies (${deps.length})`);
- for (const d of deps) {
- const ver = d.version ? `@${d.version}` : "";
- const typ = d.type ? ` [${d.type}]` : "";
- const usage = d.usage?.file_count !== undefined
- ? ` — used in ${d.usage.file_count} file(s)${d.usage.percentage !== undefined ? ` (${d.usage.percentage}%)` : ""}`
- : "";
- parts.push(`- **${d.name}**${ver}${typ}${usage}`);
- if (d.comparison?.status) parts.push(` - Comparison: ${d.comparison.status}`);
- }
- parts.push("");
- }
-
- const fns = data.scope?.functions ?? [];
- if (fns.length) {
- parts.push(`### Functions (${fns.length})`);
- for (const f of fns) {
- const usage = f.usage?.file_count !== undefined ? ` — ${f.usage.file_count} file(s)` : "";
- parts.push(`- \`${f.name}\`${f.type ? ` [${f.type}]` : ""}${usage}`);
- }
- parts.push("");
- }
-
- const vars = data.scope?.variables ?? [];
- if (vars.length) {
- parts.push(`### Variables (${vars.length})`);
- for (const v of vars) {
- const usage = v.usage?.file_count !== undefined ? ` — ${v.usage.file_count} file(s)` : "";
- parts.push(`- \`${v.name}\`${v.type ? ` [${v.type}]` : ""}${usage}`);
- }
- parts.push("");
- }
-
- if (!deps.length && !fns.length && !vars.length) parts.push("No scope data found.");
-
- const more: string[] = [];
- const paginate = (label: string, pg?: Pagination) => {
- if (pg?.has_more) more.push(`${label} (offset ${pg.offset + pg.limit})`);
- };
- paginate("dependencies", pagination?.dependencies);
- paginate("functions", pagination?.functions);
- paginate("variables", pagination?.variables);
- if (more.length) parts.push(`_More available: ${more.join(", ")}._`);
-
- return parts.join("\n");
-}
-
export function formatRepoMap(data: RepoMapData): string {
const parts: string[] = [];
const srcName = readString(data.source_repo, "name");
@@ -383,73 +317,6 @@ export function formatRepoRadius(data: RepoRadiusData): string {
return parts.join("\n");
}
-export function formatRepoReview(data: RepoReviewData): string {
- const parts: string[] = [];
- parts.push("## Code Review Signals\n");
-
- if (data.summary && Object.keys(data.summary).length) {
- parts.push("### Summary");
- parts.push(jsonBlock(data.summary));
- parts.push("");
- }
-
- const files = data.files ?? [];
- if (!files.length) {
- parts.push("No review signals found.");
- return parts.join("\n");
- }
-
- parts.push(`### Files (${files.length})`);
- parts.push("| File | Verdict | Risk | Cyclomatic | Coverage | Debt (min) |");
- parts.push("|------|---------|------|-----------|----------|-----------|");
- for (const f of files) {
- const label = f.file_path ?? f.file_name ?? f.id ?? "?";
- const cov = f.estimated_coverage === null || f.estimated_coverage === undefined ? "N/A" : `${f.estimated_coverage}`;
- parts.push(`| ${label} | ${f.verdict ?? "?"} | ${f.risk_score ?? "?"} | ${f.cyclomatic_complexity ?? "?"} | ${cov} | ${f.technical_debt_minutes ?? "?"} |`);
- }
-
- return parts.join("\n");
-}
-
-export function formatRepoTestGaps(data: RepoTestGapsData): string {
- const parts: string[] = [];
- parts.push("## Test Coverage Gaps\n");
-
- if (data.summary && Object.keys(data.summary).length) {
- parts.push("### Summary");
- parts.push(jsonBlock(data.summary));
- parts.push("");
- }
-
- const gaps = data.gaps ?? [];
- if (!gaps.length) {
- parts.push("No test gaps found.");
- return parts.join("\n");
- }
-
- for (const g of gaps) {
- parts.push(`### ${g.file_path ?? g.file_name ?? g.id ?? "file"}`);
- const info: string[] = [];
- if (g.risk_score !== undefined) info.push(`Risk: ${g.risk_score}`);
- if (g.estimated_coverage !== undefined) info.push(`Coverage: ${g.estimated_coverage}`);
- if (g.security_risk_level) info.push(`Security: ${g.security_risk_level}`);
- if (g.churn_rate) info.push(`Churn: ${g.churn_rate}`);
- if (info.length) parts.push(`- ${info.join(" | ")}`);
- if (g.suggested_test_cases?.length) {
- parts.push(`- **Suggested tests:**`);
- for (const c of g.suggested_test_cases) {
- const fn = c.target_function ? ` (\`${c.target_function}\`)` : "";
- parts.push(` - [${c.priority ?? "?"}] ${c.description ?? ""}${fn}`);
- }
- }
- if (g.mockable_dependencies?.length) parts.push(`- **Mockable deps:** ${g.mockable_dependencies.join(", ")}`);
- if (g.hard_to_test_reasons?.length) parts.push(`- **Hard to test:** ${g.hard_to_test_reasons.join("; ")}`);
- parts.push("");
- }
-
- return parts.join("\n");
-}
-
// ============================================================================
// Files
// ============================================================================
@@ -665,7 +532,7 @@ export function formatError(err: unknown): string {
if (err instanceof CodeFundiApiError) {
let msg = `**Code-Fundi API Error (${err.statusCode}):** ${err.message}`;
if (err.statusCode === 401) {
- msg += "\n\n_Tip: Use `code-fundi-auth-authenticate` and `code-fundi-auth-verify` to sign in, or set the CODEFUNDI_API_KEY environment variable._";
+ msg += "\n\n_Tip: Use `code-fundi-auth-authenticate` and `code-fundi-auth-verify` to sign in, or set the CODEFUNDI_API_KEY environment variable. After auth, persist the key in MCP config so future sessions stay signed in._";
}
if (err.retryAfter) msg += `\n_Retry after: ${err.retryAfter}s_`;
return msg;
diff --git a/src/tools/auth.ts b/src/tools/auth.ts
index 9de3ffa..3fa7c69 100644
--- a/src/tools/auth.ts
+++ b/src/tools/auth.ts
@@ -16,10 +16,11 @@ export function registerAuthTools(server: FastMCP): void {
server.addTool({
name: "code-fundi-auth-authenticate",
description:
- "Start a Code-Fundi authentication flow. Supports OTP (email code) or password modes. " +
- "For new users, set should_create_user to true. " +
- "After calling this, use code-fundi-auth-verify to complete the sign-in with the OTP code. " +
- "This tool does NOT require an existing API key.",
+ "Start Code-Fundi authentication (POST /v2/auth/authenticate). No existing API key required. " +
+ "Use auth_mode otp for new or returning users: set should_create_user true on first signup, false on sign-in. " +
+ "Sends a 6-digit email OTP (not a magic link); then call code-fundi-auth-verify after the human user provides the code. " +
+ "Password mode: pass password here (sent as X-CodeFundi-Auth-Password header only); should_create_user false for sign-in. " +
+ "On success with an active key, the API key is configured in-memory for this MCP session only.",
parameters: z.object({
email: z.string().email().describe("Email address to authenticate"),
auth_mode: z.enum(["otp", "password"]).describe("Authentication mode: 'otp' for email code, 'password' for password-based"),
@@ -70,9 +71,10 @@ export function registerAuthTools(server: FastMCP): void {
server.addTool({
name: "code-fundi-auth-verify",
description:
- "Verify a Code-Fundi OTP code to complete authentication. " +
- "Use the 6-digit code sent to the email from code-fundi-auth-authenticate. " +
- "On success, the API key is automatically configured for all subsequent tool calls.",
+ "Complete OTP sign-in (POST /v2/auth/verify). Requires the 6-digit code from the user's email after code-fundi-auth-authenticate. " +
+ "The human user must supply the token — ask them explicitly. " +
+ "On success, activates the API key and configures it in-memory for all subsequent tools in this MCP session. " +
+ "Persist CODEFUNDI_API_KEY in MCP config if the user wants auth to survive restarts.",
parameters: z.object({
email: z.string().email().describe("Email address used in the authenticate step"),
token: z.string().min(6).max(6).describe("6-digit OTP verification code"),
@@ -108,7 +110,9 @@ export function registerAuthTools(server: FastMCP): void {
server.addTool({
name: "code-fundi-auth-resend",
- description: "Resend the OTP verification email if the previous one expired or wasn't received.",
+ description:
+ "Resend the OTP email (POST /v2/auth/resend) when the previous code expired or was not received. " +
+ "Use after code-fundi-auth-authenticate, before code-fundi-auth-verify. Default type is signup for new accounts.",
parameters: z.object({
email: z.string().email().describe("Email address to resend the code to"),
type: z.enum(["signup", "email_change", "email"]).optional().describe("Resend type (default: signup)"),
diff --git a/src/tools/intel.ts b/src/tools/intel.ts
index e3e591f..a1ab3cf 100644
--- a/src/tools/intel.ts
+++ b/src/tools/intel.ts
@@ -1,56 +1,18 @@
/**
* Code-Fundi MCP — Repository Intelligence Tools
*
- * Advanced V2 repository intelligence endpoints:
- * - scope (POST /v2/repos/{repo_id}/scope) dependencies / functions / variables
- * - map (GET /v2/repos/{repo_id}/map) cross-repository dependency map
- * - blueprint (GET /v2/repos/{repo_id}/blueprint) README + dependency overview
- * - radius (POST /v2/repos/{repo_id}/radius) blast radius / file impact analysis
- * - review (GET /v2/repos/{repo_id}/review) code review signals (PRO+)
- * - test-gaps (GET /v2/repos/{repo_id}/test-gaps) test coverage gap analysis (PRO+)
- *
- * These endpoints consume credits and several require PRO tier or higher.
+ * V2 repository intelligence: dependency map, blueprint, blast radius.
*/
import type { FastMCP } from "fastmcp";
import { z } from "zod";
import { getClient } from "../client.js";
import {
- formatRepoScope, formatRepoMap, formatRepoBlueprint,
- formatRepoRadius, formatRepoReview, formatRepoTestGaps, formatError,
+ formatRepoMap, formatRepoBlueprint,
+ formatRepoRadius, formatError,
} from "../formatters.js";
export function registerRepoIntelTools(server: FastMCP): void {
- server.addTool({
- name: "code-fundi-repo-scope",
- description:
- "Get aggregated dependencies, functions, and/or variable names for a repository in a single call. " +
- "Use `include` to restrict kinds (default: all three). Provide `name` (plus `expand`) to drill into a " +
- "single dependency's files/functions, or `compare_repos` (up to 25 UUIDs) to compare dependencies across repos. " +
- "Consumes credits; functions/variables may be tier-gated.",
- parameters: z.object({
- repo_key: z.string().describe("Repository UUID or clone URL"),
- include: z.array(z.enum(["dependencies", "functions", "variables"])).optional().describe("Kinds to return (default: all three)"),
- name: z.string().optional().describe("Exact name filter (dependency key or symbol name)"),
- type: z.string().optional().describe("Type/role filter (dependency type, function type, or variable role)"),
- expand: z.array(z.enum(["files", "functions"])).optional().describe("Drill-down expansions; requires `name`"),
- compare_repos: z.array(z.string()).optional().describe("Up to 25 repository UUIDs for dependency comparison"),
- limit: z.number().int().min(1).max(200).optional().describe("Max items per kind (default 50)"),
- offset: z.number().int().min(0).optional().describe("Pagination offset"),
- order: z.enum(["usage", "name"]).optional().describe("Ordering (default: usage)"),
- demo: z.boolean().optional().describe("Run in demo mode (pre-signup, public data, IP-based credits)"),
- }),
- annotations: { title: "Repository Scope", readOnlyHint: true, openWorldHint: true },
- execute: async (args) => {
- try {
- const client = getClient();
- const { repo_key, demo, ...body } = args;
- const res = await client.getRepoScope(repo_key, body, demo);
- return res.data ? formatRepoScope(res.data, res.pagination, res.meta?.gated_kinds) : "No scope data available.";
- } catch (err) { return formatError(err); }
- },
- });
-
server.addTool({
name: "code-fundi-repo-map",
description:
@@ -128,56 +90,4 @@ export function registerRepoIntelTools(server: FastMCP): void {
} catch (err) { return formatError(err); }
},
});
-
- server.addTool({
- name: "code-fundi-repo-review",
- description:
- "Get code review signals per file: verdict (pass/warn/block), risk score, complexity, duplication, " +
- "estimated coverage, and technical debt. Requires PRO tier or higher and consumes credits.",
- parameters: z.object({
- repo_key: z.string().describe("Repository UUID or clone URL"),
- verdict: z.array(z.enum(["pass", "warn", "block"])).optional().describe("Filter by verdict(s)"),
- order: z.enum(["risk", "complexity", "coverage", "debt"]).optional().describe("Sort order (default: risk)"),
- limit: z.number().int().min(1).optional().describe("Max files (default 50)"),
- offset: z.number().int().min(0).optional().describe("Pagination offset"),
- demo: z.boolean().optional().describe("Run in demo mode (pre-signup, public data, IP-based credits)"),
- }),
- annotations: { title: "Repository Code Review", readOnlyHint: true, openWorldHint: true },
- execute: async (args) => {
- try {
- const client = getClient();
- const { repo_key, verdict, ...rest } = args;
- const res = await client.getRepoReview(repo_key, {
- ...rest,
- verdict: verdict?.length ? verdict.join(",") : undefined,
- });
- return res.data ? formatRepoReview(res.data) : "No review signals available.";
- } catch (err) { return formatError(err); }
- },
- });
-
- server.addTool({
- name: "code-fundi-repo-test-gaps",
- description:
- "Analyze test coverage gaps: per-file risk score, estimated coverage, suggested test cases, mockable " +
- "dependencies, and reasons code is hard to test. Requires PRO tier or higher and consumes credits.",
- parameters: z.object({
- repo_key: z.string().describe("Repository UUID or clone URL"),
- priority: z.enum(["high", "medium", "low"]).optional().describe("Filter by gap priority"),
- min_risk: z.number().min(0).max(1).optional().describe("Minimum risk score (0-1)"),
- untested_only: z.boolean().optional().describe("Only return untested files (default true)"),
- limit: z.number().int().min(1).optional().describe("Max gaps (default 50)"),
- offset: z.number().int().min(0).optional().describe("Pagination offset"),
- demo: z.boolean().optional().describe("Run in demo mode (pre-signup, public data, IP-based credits)"),
- }),
- annotations: { title: "Repository Test Gaps", readOnlyHint: true, openWorldHint: true },
- execute: async (args) => {
- try {
- const client = getClient();
- const { repo_key, ...opts } = args;
- const res = await client.getRepoTestGaps(repo_key, opts);
- return res.data ? formatRepoTestGaps(res.data) : "No test gap data available.";
- } catch (err) { return formatError(err); }
- },
- });
}
diff --git a/src/types.ts b/src/types.ts
index fdf3e39..b122796 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -299,8 +299,7 @@ export interface PublicRepoListResponse extends PaginatedResponse[];
- /** Present only when `include=functions`; no control_flow fields. */
functions?: Record[];
}
@@ -337,59 +334,6 @@ export interface ScopeSymbolItem {
detail?: Record | null;
}
-// ---- Scope (`POST /v2/repos/{repo_id}/scope`) ----
-
-export type RepoScopeKind = "dependencies" | "functions" | "variables";
-export type RepoScopeExpand = "files" | "functions";
-export type RepoScopeOrder = "usage" | "name";
-
-export interface RepoScopeRequest {
- include?: RepoScopeKind[] | string;
- name?: string;
- type?: string;
- expand?: RepoScopeExpand[] | string;
- compare_repos?: string[] | string;
- limit?: number;
- offset?: number;
- order?: RepoScopeOrder;
-}
-
-export interface RepoScopeSummary {
- total_files?: number;
- files_with_dependencies?: number;
- dependency_count?: number;
- function_count?: number;
- variable_count?: number;
- compared_repo_count?: number;
-}
-
-export interface RepoScopeCollections {
- dependencies?: DependencyItem[];
- functions?: ScopeSymbolItem[];
- variables?: ScopeSymbolItem[];
-}
-
-export interface RepoScopeData {
- repo?: Record;
- summary?: RepoScopeSummary;
- scope?: RepoScopeCollections;
-}
-
-export interface RepoScopePagination {
- dependencies?: Pagination;
- functions?: Pagination;
- variables?: Pagination;
-}
-
-export interface RepoScopeMeta extends Meta {
- gated_kinds?: Array<"functions" | "variables">;
-}
-
-export interface RepoScopeResponse extends BaseResponse {
- pagination?: RepoScopePagination;
- meta?: RepoScopeMeta;
-}
-
// ---- Cross-repository dependency map (`GET /v2/repos/{repo_id}/map`) ----
export interface RepoMapOptions {
@@ -496,74 +440,6 @@ export interface RepoRadiusData {
export interface RepoRadiusResponse extends BaseResponse {}
-// ---- Code review (`GET /v2/repos/{repo_id}/review`) ----
-
-export type RepoReviewOrder = "risk" | "complexity" | "coverage" | "debt";
-
-export interface ReviewFileMetrics {
- cognitive_complexity?: number;
- cyclomatic_complexity?: number;
- duplication_percentage?: number;
- duplicate_block_count?: number;
- technical_debt_minutes?: number;
- estimated_coverage?: number | null;
- churn_rate?: string;
-}
-
-export interface ReviewFile extends ReviewFileMetrics {
- id?: string;
- file_name?: string;
- file_path?: string;
- verdict?: string;
- risk_score?: number;
- triggered_rules?: Record[];
- admin_enrichment?: Record;
-}
-
-export interface RepoReviewData {
- summary?: Record;
- files?: ReviewFile[];
-}
-
-export interface RepoReviewResponse extends BaseResponse {
- pagination?: Pagination;
-}
-
-// ---- Test gaps (`GET /v2/repos/{repo_id}/test-gaps`) ----
-
-export type TestGapPriority = "high" | "medium" | "low";
-
-export interface TestCaseProjection {
- priority?: TestGapPriority;
- description?: string;
- target_function?: string;
-}
-
-export interface TestGapProjection {
- risk_score?: number;
- estimated_coverage?: number;
- churn_rate?: string;
- security_risk_level?: string;
- suggested_test_cases?: TestCaseProjection[];
- mockable_dependencies?: string[];
- hard_to_test_reasons?: string[];
-}
-
-export interface TestGapFile extends TestGapProjection {
- id?: string;
- file_name?: string;
- file_path?: string;
-}
-
-export interface RepoTestGapsData {
- summary?: Record;
- gaps?: TestGapFile[];
-}
-
-export interface RepoTestGapsResponse extends BaseResponse {
- pagination?: Pagination;
-}
-
// ============================================================================
// File Types
// ============================================================================
diff --git a/tests/client.test.ts b/tests/client.test.ts
index c2a4696..c73700a 100644
--- a/tests/client.test.ts
+++ b/tests/client.test.ts
@@ -114,10 +114,6 @@ describe("CodeFundiClient", () => {
await expect(noKeyClient.getModelLimits()).rejects.toThrow(CodeFundiApiError);
});
- it("getRepoScope should throw without API key (non-demo)", async () => {
- await expect(noKeyClient.getRepoScope("repo-id")).rejects.toThrow(CodeFundiApiError);
- });
-
it("getRepoMap should throw without API key (non-demo)", async () => {
await expect(noKeyClient.getRepoMap("repo-id")).rejects.toThrow(CodeFundiApiError);
});
@@ -129,14 +125,6 @@ describe("CodeFundiClient", () => {
it("getRepoRadius should throw without API key (non-demo)", async () => {
await expect(noKeyClient.getRepoRadius("repo-id", { file: "src/index.ts" })).rejects.toThrow(CodeFundiApiError);
});
-
- it("getRepoReview should throw without API key (non-demo)", async () => {
- await expect(noKeyClient.getRepoReview("repo-id")).rejects.toThrow(CodeFundiApiError);
- });
-
- it("getRepoTestGaps should throw without API key (non-demo)", async () => {
- await expect(noKeyClient.getRepoTestGaps("repo-id")).rejects.toThrow(CodeFundiApiError);
- });
});
// ==== V2 repository intelligence & demo mode ====
@@ -179,78 +167,6 @@ describe("CodeFundiClient", () => {
vi.unstubAllGlobals();
});
- it("getRepoScope should POST to the scope endpoint with a JSON body", async () => {
- const mockFetch = vi.fn().mockResolvedValue({
- ok: true,
- json: async () => ({ status: "success", data: { scope: {} } }),
- });
- vi.stubGlobal("fetch", mockFetch);
-
- await client.getRepoScope("repo-id", { include: ["dependencies"], name: "express" });
-
- const [url, options] = mockFetch.mock.calls[0];
- expect(url).toBe("https://api.test.codefundi.app/v2/repos/repo-id/scope");
- expect(options.method).toBe("POST");
- const body = JSON.parse(options.body as string);
- expect(body.include).toEqual(["dependencies"]);
- expect(body.name).toBe("express");
-
- vi.unstubAllGlobals();
- });
-
- it("getRepoScope should run in demo mode without an API key", async () => {
- const mockFetch = vi.fn().mockResolvedValue({
- ok: true,
- json: async () => ({ status: "success", data: { scope: {} } }),
- });
- vi.stubGlobal("fetch", mockFetch);
-
- const noKeyClient = new CodeFundiClient("https://api.test.codefundi.app");
- await noKeyClient.getRepoScope("repo-id", {}, true);
-
- const [url, options] = mockFetch.mock.calls[0];
- expect(url).toContain("/v2/repos/repo-id/scope");
- expect(url).toContain("demo=true");
- expect(options.method).toBe("POST");
-
- vi.unstubAllGlobals();
- });
-
- it("getRepoReview should build query parameters", async () => {
- const mockFetch = vi.fn().mockResolvedValue({
- ok: true,
- json: async () => ({ status: "success", data: { files: [] } }),
- });
- vi.stubGlobal("fetch", mockFetch);
-
- await client.getRepoReview("repo-id", { verdict: "warn,block", order: "risk", limit: 10 });
-
- const [url, options] = mockFetch.mock.calls[0];
- expect(url).toContain("/v2/repos/repo-id/review");
- expect(url).toContain("verdict=warn%2Cblock");
- expect(url).toContain("order=risk");
- expect(url).toContain("limit=10");
- expect(options.method).toBe("GET");
-
- vi.unstubAllGlobals();
- });
-
- it("getRepoTestGaps should include min_risk=0 and untested_only=false", async () => {
- const mockFetch = vi.fn().mockResolvedValue({
- ok: true,
- json: async () => ({ status: "success", data: { gaps: [] } }),
- });
- vi.stubGlobal("fetch", mockFetch);
-
- await client.getRepoTestGaps("repo-id", { min_risk: 0, untested_only: false });
-
- const [url] = mockFetch.mock.calls[0];
- expect(url).toContain("min_risk=0");
- expect(url).toContain("untested_only=false");
-
- vi.unstubAllGlobals();
- });
-
it("getModels should GET /v2/models and normalize data.models", async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
diff --git a/tests/formatters.test.ts b/tests/formatters.test.ts
index 87caf86..386aa8e 100644
--- a/tests/formatters.test.ts
+++ b/tests/formatters.test.ts
@@ -10,16 +10,15 @@ import {
formatHistoryDetail, formatConversation,
formatUsageStats, formatActivityStats, formatLanguageStats,
formatApiKeys, formatModels, formatModelLimits, formatError,
- formatPublicRepoList, formatRepoScope, formatRepoMap,
- formatRepoBlueprint, formatRepoRadius, formatRepoReview,
- formatRepoTestGaps,
+ formatPublicRepoList, formatRepoMap,
+ formatRepoBlueprint, formatRepoRadius,
} from "../src/formatters.js";
import { CodeFundiApiError } from "../src/client.js";
import type {
SearchResult, Repository, FileListItem, HistoryItem,
FileDocumentationData, ReadmeData, RepositoryIndexInitRepo,
- PublicRepoListItem, RepoScopeData, RepoMapData, RepoBlueprintData,
- RepoRadiusData, RepoReviewData, RepoTestGapsData, ModelLimitsData,
+ PublicRepoListItem, RepoMapData, RepoBlueprintData,
+ RepoRadiusData, ModelLimitsData,
} from "../src/types.js";
// ==== Search ====
@@ -274,33 +273,6 @@ describe("formatPublicRepoList", () => {
// ==== Repository Intelligence ====
-describe("formatRepoScope", () => {
- it("should format dependencies, functions, variables and pagination hints", () => {
- const data: RepoScopeData = {
- repo: { name: "my-repo" },
- summary: { total_files: 100, dependency_count: 3, function_count: 2, variable_count: 1 },
- scope: {
- dependencies: [{ name: "express", version: "4.18.0", type: "package", usage: { file_count: 10, percentage: 25 } }],
- functions: [{ name: "handler", type: "function", usage: { file_count: 4 } }],
- variables: [{ name: "PORT", type: "env" }],
- },
- };
- const result = formatRepoScope(data, { dependencies: { total: 20, limit: 1, offset: 0, has_more: true } }, ["variables"]);
- expect(result).toContain("my-repo");
- expect(result).toContain("**express**@4.18.0");
- expect(result).toContain("used in 10 file(s)");
- expect(result).toContain("handler");
- expect(result).toContain("PORT");
- expect(result).toContain("Gated");
- expect(result).toContain("More available");
- });
-
- it("should handle empty scope", () => {
- const result = formatRepoScope({ scope: {} });
- expect(result).toContain("No scope data found");
- });
-});
-
describe("formatRepoMap", () => {
it("should format map dependencies", () => {
const data: RepoMapData = {
@@ -368,55 +340,6 @@ describe("formatRepoRadius", () => {
});
});
-describe("formatRepoReview", () => {
- it("should format the file metrics table", () => {
- const data: RepoReviewData = {
- summary: { blockers: 1 },
- files: [{
- id: "rf1", file_path: "src/app.ts", verdict: "block", risk_score: 0.9,
- cyclomatic_complexity: 22, estimated_coverage: null, technical_debt_minutes: 120,
- }],
- };
- const result = formatRepoReview(data);
- expect(result).toContain("Code Review Signals");
- expect(result).toContain("src/app.ts");
- expect(result).toContain("block");
- expect(result).toContain("N/A");
- expect(result).toContain("120");
- });
-
- it("should handle no files", () => {
- const result = formatRepoReview({ files: [] });
- expect(result).toContain("No review signals found");
- });
-});
-
-describe("formatRepoTestGaps", () => {
- it("should format gaps with suggested test cases", () => {
- const data: RepoTestGapsData = {
- gaps: [{
- id: "g1", file_path: "src/auth.ts", risk_score: 0.8, estimated_coverage: 0.2,
- security_risk_level: "high",
- suggested_test_cases: [{ priority: "high", description: "Test invalid token", target_function: "verify" }],
- mockable_dependencies: ["jwt"],
- hard_to_test_reasons: ["network calls"],
- }],
- };
- const result = formatRepoTestGaps(data);
- expect(result).toContain("Test Coverage Gaps");
- expect(result).toContain("src/auth.ts");
- expect(result).toContain("Test invalid token");
- expect(result).toContain("verify");
- expect(result).toContain("jwt");
- expect(result).toContain("network calls");
- });
-
- it("should handle no gaps", () => {
- const result = formatRepoTestGaps({ gaps: [] });
- expect(result).toContain("No test gaps found");
- });
-});
-
// ==== Errors ====
describe("formatError", () => {
@@ -425,6 +348,8 @@ describe("formatError", () => {
const result = formatError(err);
expect(result).toContain("401");
expect(result).toContain("code-fundi-auth-authenticate");
+ expect(result).toContain("CODEFUNDI_API_KEY");
+ expect(result).toContain("persist");
});
it("should format rate limit error with retry info", () => {