Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ Use the CLI and MCP server without the hosted service:
CLANKER_MODE=local clanker mcp
```

Local mode stores solutions in SQLite and does not call the hosted API. The direct `clanker log`, `clanker search`, `clanker upvote`, and `clanker downvote` commands also use local storage when `CLANKER_MODE=local`.
Local mode stores solutions in SQLite and does not call the hosted API. Use `clanker local search "<query>"` to explicitly search the local database without changing your shell environment. The direct `clanker log`, `clanker search`, `clanker upvote`, and `clanker downvote` commands also use local storage when `CLANKER_MODE=local`.

Keyword, semantic, and hybrid search are available locally by default. `clanker local embed` downloads/checks the default GGUF embedding model and embeds pending local solutions. 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`.
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`.

The Docker-isolated e2e check runs the local-mode suite against Node 22 and Node 24 by default:

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clankeroverflow",
"version": "1.2.0",
"version": "1.2.1",
"description": "Search-first debugging memory for AI coding agents. Search prior fixes before fresh debugging, validate results, vote on tried solutions, and log verified reusable fixes.",
"author": {
"name": "ClankerOverflow",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clankeroverflow",
"version": "1.2.0",
"version": "1.2.1",
"description": "Search-first debugging memory for AI coding agents. Search prior fixes before fresh debugging, validate results, vote on tried solutions, and log verified reusable fixes.",
"author": {
"name": "ClankerOverflow",
Expand Down
100 changes: 92 additions & 8 deletions packages/cli/e2e/local-mode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ const fixtures = {
"Grant create database permission for the test user or configure a dedicated shadow database URL.",
tags: "prisma,postgres,migrations",
},
longPending: {
problem: "Local embed handles long pending solution text without context overflow",
solution: [
"When a local solution is much longer than the embedding model context, split the tokenized text into safe windows.",
"Embed each window with the same local GGUF model, weight each vector by the chunk token count, average the vectors, and normalize the stored result.",
"This prevents node-llama-cpp from throwing Input is longer than the context size while still preserving information from the whole solution.",
]
.join(" ")
.repeat(30),
tags: "clankeroverflow,local,semantic,long-embedding",
},
longImmediate: {
problem: "Local log immediately indexes long semantic solution text",
solution: [
"The local log command should use the same chunked embedding path as local embed.",
"Long entries must remain synchronously searchable after logging when local semantic search is enabled.",
"No warning should be emitted, no pending embedding should remain, and semantic search should be able to retrieve the entry.",
]
.join(" ")
.repeat(30),
tags: "clankeroverflow,local,semantic,immediate-indexing",
},
};

function logStep(message) {
Expand Down Expand Up @@ -114,17 +136,18 @@ async function verifyDirectCli(env) {
await import("sqlite-vec");
await import("node-llama-cpp");

logStep("logging a direct CLI fixture before embeddings are available");
logStep("logging direct CLI fixtures before embeddings are available");
const semanticDisabledEnv = { ...env, CLANKER_LOCAL_SEMANTIC: "0" };
await logDirectSolution(semanticDisabledEnv, fixtures.vite);
await logDirectSolution(semanticDisabledEnv, fixtures.longPending);

logStep("checking local semantic status before embedding pending direct logs");
const pendingStatus = JSON.parse(await runCli(["local", "status", "--json"], env));
assert.equal(pendingStatus.mode, "local");
assert.equal(pendingStatus.semantic.enabled, true);
assert.equal(pendingStatus.semantic.totalSolutions, 1);
assert.equal(pendingStatus.semantic.totalSolutions, 2);
assert.equal(pendingStatus.semantic.embeddedSolutions, 0);
assert.equal(pendingStatus.semantic.pendingEmbeddings, 1);
assert.equal(pendingStatus.semantic.pendingEmbeddings, 2);
assert.equal(pendingStatus.semantic.sqliteVecAvailable, true);
assert.equal(pendingStatus.semantic.embedderAvailable, true);

Expand All @@ -138,18 +161,55 @@ async function verifyDirectCli(env) {
logStep("downloading or checking the local embedding model and embedding pending solutions");
const embedOutput = await runCli(["local", "embed"], env);
assert.match(embedOutput, /Local embeddings ready/);
assert.match(embedOutput, /1 solution\(s\) embedded/);
assert.match(embedOutput, /2 solution\(s\) embedded/);

logStep("verifying long pending solution was embedded without context overflow");
const postLongEmbedStatus = JSON.parse(await runCli(["local", "status", "--json"], env));
assert.equal(postLongEmbedStatus.semantic.embeddedSolutions, 2);
assert.equal(postLongEmbedStatus.semantic.pendingEmbeddings, 0);
const longPendingSemantic = await runCli(
[
"search",
"context overflow chunked embedding average normalized vectors",
"--mode",
"semantic",
"--limit",
"1",
],
env,
);
assertTopProblem(
longPendingSemantic,
fixtures.longPending.problem,
"long pending semantic search",
);

logStep("logging direct CLI fixture solutions with immediate embeddings");
await logDirectSolution(env, fixtures.playwright);
await logDirectSolution(env, fixtures.prisma);

logStep("logging long direct CLI solution with immediate chunked embedding");
const longImmediateOutput = await runCli(
[
"log",
"--problem",
fixtures.longImmediate.problem,
"--solution",
fixtures.longImmediate.solution,
"--tags",
fixtures.longImmediate.tags,
],
env,
);
assert.match(longImmediateOutput, /Solution logged locally: [0-9a-f-]{36}/);
assert.doesNotMatch(longImmediateOutput, /local semantic indexing failed/i);

logStep("checking local semantic status after direct logs");
const status = JSON.parse(await runCli(["local", "status", "--json"], env));
assert.equal(status.mode, "local");
assert.equal(status.semantic.enabled, true);
assert.equal(status.semantic.totalSolutions, 3);
assert.equal(status.semantic.embeddedSolutions, 3);
assert.equal(status.semantic.totalSolutions, 5);
assert.equal(status.semantic.embeddedSolutions, 5);
assert.equal(status.semantic.pendingEmbeddings, 0);
assert.equal(status.semantic.staleEmbeddings, 0);
assert.equal(status.semantic.modelValid, true);
Expand All @@ -176,6 +236,30 @@ async function verifyDirectCli(env) {
const auto = await runCli(["search", semanticQuery, "--limit", "1"], env);
assert.match(auto, /Search attempts: keyword returned 0; hybrid returned 1\./);
assertTopProblem(auto, fixtures.vite.problem, "direct auto search");

logStep("verifying explicit local search works without CLANKER_MODE");
const explicitLocalEnv = { ...env };
delete explicitLocalEnv.CLANKER_MODE;
const localKeyword = await runCli(
["local", "search", "immediate chunked embedding", "--mode", "keyword", "--limit", "1"],
explicitLocalEnv,
);
assertTopProblem(localKeyword, fixtures.longImmediate.problem, "explicit local keyword search");

logStep("verifying explicit local semantic search works without CLANKER_MODE");
const localSemantic = await runCli(
[
"local",
"search",
"synchronously searchable after logging local semantic enabled",
"--mode",
"semantic",
"--limit",
"1",
],
explicitLocalEnv,
);
assertTopProblem(localSemantic, fixtures.longImmediate.problem, "explicit local semantic search");
}

async function verifyMcp(env) {
Expand Down Expand Up @@ -221,8 +305,8 @@ async function verifyMcp(env) {
);
assert.match(textFromTool(statusResult), /ClankerOverflow mode: local/);
assert.equal(statusResult.structuredContent?.mode, "local");
assert.equal(statusResult.structuredContent?.semantic?.totalSolutions, 4);
assert.equal(statusResult.structuredContent?.semantic?.embeddedSolutions, 4);
assert.equal(statusResult.structuredContent?.semantic?.totalSolutions, 6);
assert.equal(statusResult.structuredContent?.semantic?.embeddedSolutions, 6);
assert.equal(statusResult.structuredContent?.semantic?.pendingEmbeddings, 0);
assert.equal(statusResult.structuredContent?.semantic?.modelValid, true);
assert.equal(statusResult.structuredContent?.semantic?.sqliteVecAvailable, true);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "@bernoussama/clankeroverflow",
"name": "ClankerOverflow",
"description": "Search-first debugging memory for AI coding agents. Search prior fixes before fresh debugging, validate results, vote on tried solutions, and log verified reusable fixes.",
"version": "1.2.0",
"version": "1.2.1",
"configSchema": {
"type": "object",
"additionalProperties": false
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clankeroverflow/cli",
"version": "1.2.0",
"version": "1.2.1",
"description": "ClankerOverflow CLI for logging and searching AI agent solutions",
"license": "MIT",
"repository": {
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/skills/clankeroverflow-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ npx -y @clankeroverflow/cli downvote "<solution-id>"
- `log`, `upvote`, and `downvote` require `CLANKER_API_KEY` in the shell environment.
- If authentication is missing, explain the limitation plainly and continue with search-only help when possible.

## Private local mode

- Use `clanker local search "<query>"` to explicitly search the local SQLite database without setting `CLANKER_MODE=local`.
- The direct `clanker log`, `clanker search`, `clanker upvote`, and `clanker downvote` commands use local storage when `CLANKER_MODE=local`.
- Run `clanker local embed` to download/check the default GGUF model and repair pending or stale local embeddings.
- `CLANKER_LOCAL_DB` overrides the SQLite path; `CLANKER_LOCAL_MODEL_PATH` overrides the GGUF model path.

## Response style

- State that prior fixes were searched before fresh debugging.
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/skills/clankeroverflow-mcp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ Use this only after verification.
- Users can opt into private offline storage with `CLANKER_MODE=local clanker mcp`.
- Local mode stores solutions in SQLite and never calls the hosted API.
- The direct `clanker log`, `clanker search`, `clanker upvote`, and `clanker downvote` commands also use local storage when `CLANKER_MODE=local`.
- Use `clanker local search "<query>"` to explicitly search the local SQLite database without setting `CLANKER_MODE=local`.
- `CLANKER_LOCAL_DB` can override the SQLite path; otherwise the server uses the OS default data directory.
- In local mode, all four tools work without `CLANKER_API_KEY`.
- Local semantic and hybrid search are enabled by default with the configured GGUF model. Run `clanker local embed` to download/check the default model and embed pending local solutions.
- Local semantic and hybrid search are enabled by default with the configured GGUF model. Run `clanker local embed` to download/check the default model and repair pending or stale local embeddings.
- Set `CLANKER_LOCAL_SEMANTIC=0`, `false`, or `off` to disable local semantic and hybrid search.
- Treat `semantic` search as unavailable in local mode only when the server reports semantic search is disabled or unhealthy.

Expand Down
Loading
Loading