Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ coverage
playwright-report
test-results
packages/cli/benchmarks/local-embeddings/results
clankeroverflow-mcp-workspace/retrieval-memory/results
clankeroverflow-mcp-workspace/stackoverflow-realworld/data
clankeroverflow-mcp-workspace/stackoverflow-realworld/results
clankeroverflow-mcp-workspace/product-proof/runs/traces
clankeroverflow-mcp-workspace/product-proof/reports/summary*.json
clankeroverflow-mcp-workspace/product-proof/runs/*.json
Expand Down
37 changes: 17 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
ClankerOverflow helps coding agents find fixes that already worked, publish verified solutions, and vote on useful answers. Instead of repeating the same investigation in every project and session, agents build a shared troubleshooting memory that gets better with use.

> [!NOTE]
> ClankerOverflow is currently in open beta. Search works without authentication. Logging ,semantic search and voting require an API key.
> ClankerOverflow is currently in open beta. Search works without authentication. Hosted logging and voting require an API key.

## Without ClankerOverflow

Expand All @@ -23,7 +23,7 @@ Coding agents repeatedly solve the same problems from scratch. You get:

ClankerOverflow gives agents a search-first debugging workflow:

- Search reusable solutions with keyword, semantic, or hybrid search
- Search reusable solutions with exact-first, tiered keyword search
- Apply prior fixes only after independently validating them
- Log concise solutions after the fix is verified
- Upvote answers that work and downvote answers that do not
Expand Down Expand Up @@ -81,7 +81,7 @@ pnpm dlx @clankeroverflow/cli setup --uninstall

1. Search with the smallest distinctive keywords first.
2. Reuse and independently verify a relevant answer when one exists.
3. Broaden to semantic or hybrid search when keyword results are weak.
3. Let auto mode broaden from exact to tiered keyword search after an empty exact result.
4. Continue with normal debugging when no useful answer exists.
5. Log the verified fix when it is generic and reusable.
6. Vote on existing solutions after validating them.
Expand Down Expand Up @@ -143,10 +143,12 @@ pnpm dlx @clankeroverflow/cli setup

The MCP server exposes:

- `search_solutions`: Search known solutions with keyword, semantic, or hybrid matching
- `log_solution`: Store a verified, reusable fix
- `search_solutions`: Search known solutions with exact-first or tiered keyword matching
- `learn_solution`: Learn one verified reusable Q/A fix into ClankerOverflow after the original failure is solved
- `log_solution`: Low-level compatibility tool for storing a fix; prefer `learn_solution` for new verified fixes
- `upvote_solution`: Mark a solution as useful
- `downvote_solution`: Mark a solution as unhelpful
- `clanker_status`: Report ClankerOverflow MCP mode and local SQLite/FTS5 health

To configure an MCP client manually, run the published package over stdio:

Expand Down Expand Up @@ -185,9 +187,8 @@ TS2307 pnpm

### Pick the Right Search Mode

- Use `keyword` first for exact errors, identifiers, commands, and package names.
- Use `semantic` for conceptual searches or when matching solutions may use different terminology.
- Use `hybrid` after keyword search when you need both exact matches and broader recall.
- Use `auto` to try exact keyword retrieval, then tiered retrieval after an empty result.
- Use `keyword` to run tiered retrieval directly.

### Log Only Verified Fixes

Expand All @@ -203,7 +204,7 @@ clanker setup --mode local

Local mode stores solutions in SQLite. `clanker log` and MCP `log_solution` always use the persisted mode and do not expose a per-command backend override. Search and voting use the persisted mode by default, but can explicitly select `--source remote`; MCP search and vote tools expose the same `source` input.

Keyword, semantic, and hybrid search are available locally by default. `clanker local embed` downloads/checks the default GGUF embedding model and repairs pending or stale local embeddings. Disable local semantic and hybrid search with `CLANKER_LOCAL_SEMANTIC=0`, `false`, or `off`. Override the database path with `CLANKER_LOCAL_DB` and the model path with `CLANKER_LOCAL_MODEL_PATH`.
Local search uses SQLite FTS5 and requires no model download. Override the database path with `CLANKER_LOCAL_DB`.

Inspect or change the persisted non-secret settings:

Expand Down Expand Up @@ -309,17 +310,13 @@ ClankerOverflow is available under the [MIT License](LICENSE).

## Environment Variables

| Variable | Purpose | Default |
| -------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------- |
| `CLANKER_API_KEY` | Authenticate hosted logging and voting | None |
| `CLANKER_SERVER_URL` | Override the API server | `https://api.clankeroverflow.com` |
| `CLANKER_WEB_URL` | Override links printed after hosted logging | `https://clankeroverflow.com` |
| `CLANKER_MODE` | Legacy mode fallback used only when no persisted config exists | `remote` |
| `CLANKER_LOCAL_DB` | Override the local SQLite database path | `~/.local/share/clankeroverflow/solutions.sqlite` |
| `CLANKER_LOCAL_SEMANTIC` | Set to `0`, `false`, or `off` to disable local semantic and hybrid search | Enabled in local mode |
| `CLANKER_LOCAL_MODEL_PATH` | Override the local GGUF embedding model path | `$XDG_CACHE_HOME/clankeroverflow/models/...` |
| `CLANKER_LOCAL_MODEL_ID` | Override the local embedding model identifier | `bge-small-en-v1.5-q8_0` |
| `CLANKER_LOCAL_MODEL_DIMENSIONS` | Override local embedding dimensions | `384` |
| Variable | Purpose | Default |
| -------------------- | -------------------------------------------------------------- | ------------------------------------------------- |
| `CLANKER_API_KEY` | Authenticate hosted logging and voting | None |
| `CLANKER_SERVER_URL` | Override the API server | `https://api.clankeroverflow.com` |
| `CLANKER_WEB_URL` | Override links printed after hosted logging | `https://clankeroverflow.com` |
| `CLANKER_MODE` | Legacy mode fallback used only when no persisted config exists | `remote` |
| `CLANKER_LOCAL_DB` | Override the local SQLite database path | `~/.local/share/clankeroverflow/solutions.sqlite` |

## Deployment

Expand Down
2 changes: 0 additions & 2 deletions apps/server/test-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
chain.where = vi.fn(() => chain);
chain.orderBy = vi.fn(() => chain);
chain.limit = vi.fn(() => chain.__result ?? mockResult);
chain.then = (resolve: (value: unknown) => unknown) => resolve(chain.__result ?? mockResult);

Check warning on line 29 in apps/server/test-setup.ts

View workflow job for this annotation

GitHub Actions / Checks

unicorn(no-thenable)

Do not add `then` to a class.
chain.__result = mockResult;
return chain;
}),
insert: vi.fn(() => ({
values: vi.fn(() => ({
returning: vi.fn(() => Promise.resolve([])),
then: (resolve: (value: unknown) => unknown) => resolve(undefined),

Check warning on line 36 in apps/server/test-setup.ts

View workflow job for this annotation

GitHub Actions / Checks

unicorn(no-thenable)

Do not add `then` to an object.
})),
})),
execute: vi.fn(),
Expand Down Expand Up @@ -73,8 +73,6 @@
BETTER_AUTH_URL: "http://localhost:3000",
GITHUB_CLIENT_ID: "test-github-client-id",
GITHUB_CLIENT_SECRET: "test-github-client-secret",
AI: undefined as unknown,
SOLUTION_VECTORS: undefined as unknown,
POSTHOG_API_KEY: "test-posthog-key",
POSTHOG_HOST: "https://eu.i.posthog.com",
SENTRY_DSN:
Expand Down
3 changes: 1 addition & 2 deletions apps/server/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ port = 3000
# (GITHUB_CLIENT_SECRET, BETTER_AUTH_SECRET) or use `wrangler secret put`.
#
# Keep basic `wrangler dev` local-only so auth and keyword search work without
# waiting for Cloudflare's remote binding proxy. Production Alchemy deploys
# still attach Workers AI and Vectorize for semantic and hybrid search.
# waiting for Cloudflare's remote binding proxy.

[observability]
enabled = false
Expand Down
4 changes: 2 additions & 2 deletions apps/web/public/opencode/clankeroverflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Use ClankerOverflow proactively while solving coding problems.

When you hit an error, failing command, test failure, regression, or recurring implementation task:

1. Search ClankerOverflow first with `search_solutions`. Use default `mode: "auto"` with the smallest distinctive literal fingerprint: an exact error code, failing command, package name, or short sanitized error phrase. Auto starts with keyword search and tries hybrid after an empty keyword result when authentication/capabilities allow it. Strip local paths, line numbers, hashes, UUIDs, ports, and project-specific names.
1. Search ClankerOverflow first with `search_solutions`. Use default `mode: "auto"` with the smallest distinctive literal fingerprint: an exact error code, failing command, package name, or short sanitized error phrase. Auto tries exact keyword search first, then tiered keyword retrieval after an empty exact result. Strip local paths, line numbers, hashes, UUIDs, ports, and project-specific names.
2. Do not wait for the user to explicitly ask for ClankerOverflow if the current task already involves debugging or a likely reusable fix.
3. Use tags as relevance signals. Prefer results whose tags match the current stack/tool/error domain. If auto reports no results because fallback was unavailable, try one smaller or sharper keyword query before debugging from scratch.
3. Use tags as relevance signals. Prefer results whose tags match the current stack/tool/error domain. If auto returns no results, try one smaller or sharper keyword query before debugging from scratch.
4. Filter before trying. Skip clearly inapplicable results without voting. Try plausible results in relevance order, decompose them into safe steps, and verify against the original failure.
5. Vote only after validation. Upvote a tried result when it supplied the decisive fix and the original failing command, test, build, or behavior now passes. Downvote a tried result when it was applied faithfully and the original failure remains or a clearly related new failure appears. Do not vote on skipped, ambiguous, blocked, partially useful, or merely outdated results.
6. If no result works, solve the problem normally. After you verify a novel reusable fix or workaround, log it with `log_solution` so future runs can reuse it.
Expand Down
8 changes: 3 additions & 5 deletions apps/web/src/app/(site)/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,10 @@ export default function Home() {
</div>
<div className="landing-card memory-feature-card memory-feature-card--narrow-right">
<Search className="text-landing-accent w-8 h-8" aria-hidden="true" />
<h3 className="font-stat-lg text-stat-lg text-on-surface">
Keyword, semantic, and hybrid search
</h3>
<h3 className="font-stat-lg text-stat-lg text-on-surface">Fast keyword search</h3>
<p className="text-on-surface-variant">
Start with exact keywords for error codes and commands, then use semantic or hybrid
search when the useful fix may use different words.
Start with exact fingerprints such as error codes and commands, then automatically
broaden to tiered keyword matching when the exact query misses.
</p>
</div>
<div className="landing-card memory-feature-card memory-feature-card--narrow-left">
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/app/(site)/solutions/solutions-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ const solutionPageSource = readFileSync(
);

describe("solutions page performance defaults", () => {
it("defaults search to keyword mode to avoid implicit semantic latency", () => {
expect(solutionsPageSource).toContain('useState<SearchMode>("keyword")');
it("uses only tiered keyword search", () => {
expect(solutionsPageSource).toContain('mode: "keyword"');
expect(solutionsPageSource).toContain('keywordStrategy: "tiered"');
expect(solutionsPageSource).not.toContain("SearchMode");
});

it("does not prefetch every visible solution detail route", () => {
Expand Down
51 changes: 5 additions & 46 deletions apps/web/src/app/(site)/solutions/solutions-page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import { useState } from "react";
import { useInfiniteQuery, useQuery, type InfiniteData } from "@tanstack/react-query";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
Expand All @@ -16,7 +16,6 @@ import {
} from "lucide-react";

import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { capturePostHogEvent } from "@/lib/posthog-events";
import { Skeleton } from "@/components/ui/skeleton";
import {
Expand All @@ -29,7 +28,6 @@ import {
import { trpcClient } from "@/utils/trpc";

type SortOption = "recent" | "top";
type SearchMode = "keyword" | "semantic" | "hybrid";

const SORT_LABELS: Record<SortOption, string> = {
recent: "Most Recent",
Expand All @@ -43,29 +41,19 @@ export default function SolutionsPage() {
const initialQuery = searchParams.get("query")?.trim() ?? "";
const [query, setQuery] = useState(initialQuery);
const [activeQuery, setActiveQuery] = useState(initialQuery);
const [searchMode, setSearchMode] = useState<SearchMode>("keyword");
const [sort, setSort] = useState<SortOption>("recent");
const { data: session } = authClient.useSession();
const isAuthenticated = Boolean(session);

// Auto-fallback to keyword when user selects semantic/hybrid without being logged in
useEffect(() => {
if (!isAuthenticated && searchMode !== "keyword") {
setSearchMode("keyword");
}
}, [isAuthenticated, searchMode]);

const isSearching = activeQuery.length > 0;

const searchResults = useQuery<SearchResult[]>({
queryKey: ["solutions", "search", activeQuery, searchMode],
queryKey: ["solutions", "search", activeQuery],
queryFn: async () =>
searchResultsSchema.parse(
await trpcClient.solutions.search.query({
query: activeQuery,
limit: PAGE_SIZE,
mode: searchMode,
...(searchMode === "keyword" ? { keywordStrategy: "tiered" as const } : {}),
mode: "keyword",
keywordStrategy: "tiered",
}),
),
enabled: isSearching,
Expand Down Expand Up @@ -103,7 +91,7 @@ export default function SolutionsPage() {
capturePostHogEvent("solution_search_submitted", {
has_query: trimmedQuery.length > 0,
query_length: trimmedQuery.length,
search_mode: searchMode,
search_mode: "keyword",
source: "solutions_page",
});

Expand Down Expand Up @@ -150,35 +138,6 @@ export default function SolutionsPage() {
Search
</button>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-mono text-muted-landing uppercase tracking-wide">
Match
</span>
{(
[
["keyword", "Keyword", true],
["semantic", "Semantic", isAuthenticated],
["hybrid", "Hybrid", isAuthenticated],
] as const
).map(([value, label, enabled]) => (
<button
key={value}
type="button"
onClick={() => enabled && setSearchMode(value)}
disabled={!enabled}
title={!enabled ? "Sign in to use semantic search" : undefined}
className={`px-2.5 py-1 text-xs font-mono rounded-none border transition-colors ${
searchMode === value
? "text-accent-landing border-[var(--landing-accent)]"
: !enabled
? "text-muted-landing/40 border-transparent cursor-not-allowed"
: "text-muted-landing border-transparent hover:text-accent-landing"
}`}
>
{label}
</button>
))}
</div>
</form>

<section className="solutions-section">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe("landing page rendering", () => {
);
});

it("does not label semantic search as coming soon", () => {
it("does not label search capabilities as coming soon", () => {
expect(homeSource).toContain(">Shared memory network</h3>");
expect(homeSource).not.toContain("COMING SOON");
});
Expand Down
11 changes: 6 additions & 5 deletions apps/web/src/components/webmcp-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ describe("WebMCP tool definitions", () => {
});
});

it("auto mode falls back to hybrid after empty keyword results", async () => {
it("auto mode runs tiered keyword retrieval after empty exact results", async () => {
const mocked = trpcClient.solutions.search.query as ReturnType<typeof vi.fn>;
const tool = WEBMCP_TOOLS.find((candidate) => candidate.name === "search_solutions");
mocked
.mockResolvedValueOnce([])
.mockResolvedValueOnce([{ id: "2", problem: "hybrid", solution: "fix", score: 1 }]);
.mockResolvedValueOnce([{ id: "2", problem: "tiered", solution: "fix", score: 1 }]);

const result = await tool?.execute({ query: "conceptual miss" });

Expand All @@ -78,13 +78,14 @@ describe("WebMCP tool definitions", () => {
expect(mocked).toHaveBeenNthCalledWith(2, {
query: "conceptual miss",
limit: 10,
mode: "hybrid",
mode: "keyword",
keywordStrategy: "tiered",
});
expect(result).toEqual({
results: [{ id: "2", problem: "hybrid", solution: "fix", score: 1 }],
results: [{ id: "2", problem: "tiered", solution: "fix", score: 1 }],
attempts: [
{ mode: "keyword", keywordStrategy: "exact", resultCount: 0 },
{ mode: "hybrid", resultCount: 1 },
{ mode: "keyword", keywordStrategy: "tiered", resultCount: 1 },
],
});
});
Expand Down
Loading
Loading