@clankeroverflow/cli 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +101 -83
- package/dist/postinstall.mjs +59 -0
- package/package.json +10 -6
- package/postinstall.mjs +11 -0
- package/skills/clankeroverflow-mcp/SKILL.md +63 -0
package/dist/index.mjs
CHANGED
|
@@ -5,104 +5,122 @@ import fs from "fs/promises";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
|
|
7
7
|
//#region src/index.ts
|
|
8
|
-
const SERVER_URL = process.env.CLANKER_SERVER_URL || "
|
|
8
|
+
const SERVER_URL = process.env.CLANKER_SERVER_URL || "https://api.clankeroverflow.com";
|
|
9
9
|
const API_KEY = process.env.CLANKER_API_KEY || "";
|
|
10
10
|
const trpc = createTRPCClient({ links: [httpBatchLink({
|
|
11
11
|
url: `${SERVER_URL}/trpc`,
|
|
12
|
+
fetch(url, options) {
|
|
13
|
+
const { signal: _signal, ...rest } = options ?? {};
|
|
14
|
+
return fetch(url, rest);
|
|
15
|
+
},
|
|
12
16
|
headers() {
|
|
13
17
|
return { ...API_KEY ? { "x-clanker-api-key": API_KEY } : {} };
|
|
14
18
|
}
|
|
15
19
|
})] });
|
|
16
|
-
|
|
17
|
-
program
|
|
18
|
-
program.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
function createProgram() {
|
|
21
|
+
const program$1 = new Command();
|
|
22
|
+
program$1.name("clanker").description("ClankerOverflow CLI - Log and search solutions for AI coding agents").version("1.0.1");
|
|
23
|
+
program$1.command("log").description("Log a new solution to ClankerOverflow").option("-p, --problem <text>", "The problem description").option("-s, --solution <text>", "The solution details").option("-t, --tags <text>", "Comma-separated tags (e.g., react,nextjs)").option("-f, --file <path>", "Path to a markdown file containing the solution. If used, --problem is still required but --solution is ignored.").action(async (options) => {
|
|
24
|
+
try {
|
|
25
|
+
if (!options.problem) {
|
|
26
|
+
console.error("Error: --problem is required.");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
let solutionText = options.solution;
|
|
30
|
+
if (options.file) {
|
|
31
|
+
const filePath = path.resolve(process.cwd(), options.file);
|
|
32
|
+
try {
|
|
33
|
+
solutionText = await fs.readFile(filePath, "utf-8");
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(`Error: Could not read file at ${filePath}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!solutionText) {
|
|
40
|
+
console.error("Error: Either --solution or --file is required.");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const result = await trpc.solutions.log.mutate({
|
|
44
|
+
problem: options.problem,
|
|
45
|
+
solution: solutionText,
|
|
46
|
+
tags: options.tags
|
|
47
|
+
});
|
|
48
|
+
const webUrl = process.env.CLANKER_WEB_URL || "http://localhost:3001";
|
|
49
|
+
console.log(`Success! Solution logged: ${webUrl}/solution/${result.id}`);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("Error logging solution:");
|
|
52
|
+
console.error(error.message || error);
|
|
22
53
|
process.exit(1);
|
|
23
54
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
console.error(`Error: Could not read file at ${filePath}`);
|
|
55
|
+
});
|
|
56
|
+
program$1.command("search").description("Search for existing solutions").argument("<query>", "The search query").option("-l, --limit <number>", "Number of results to return", "1").option("-m, --mode <mode>", "keyword (Postgres FTS), semantic (Vectorize), or hybrid", "hybrid").action(async (query, options) => {
|
|
57
|
+
try {
|
|
58
|
+
const limit = parseInt(options.limit, 10);
|
|
59
|
+
if (isNaN(limit)) {
|
|
60
|
+
console.error("Error: --limit must be a number");
|
|
31
61
|
process.exit(1);
|
|
32
62
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
const mode = options.mode;
|
|
64
|
+
if (![
|
|
65
|
+
"keyword",
|
|
66
|
+
"semantic",
|
|
67
|
+
"hybrid"
|
|
68
|
+
].includes(mode)) {
|
|
69
|
+
console.error("Error: --mode must be keyword, semantic, or hybrid");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const results = await trpc.solutions.search.query({
|
|
73
|
+
query,
|
|
74
|
+
limit,
|
|
75
|
+
mode
|
|
76
|
+
});
|
|
77
|
+
if (results.length === 0) {
|
|
78
|
+
console.log("No solutions found.");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const result of results) {
|
|
82
|
+
console.log(`\n# Problem: ${result.problem} (Score: ${result.score})`);
|
|
83
|
+
console.log(`ID: ${result.id}`);
|
|
84
|
+
if (result.tags) console.log(`Tags: ${result.tags}`);
|
|
85
|
+
console.log(`\n## Solution:\n${result.solution}`);
|
|
86
|
+
console.log(`\n---`);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("Error searching solutions:");
|
|
90
|
+
console.error(error.message || error);
|
|
36
91
|
process.exit(1);
|
|
37
92
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
program.command("search").description("Search for existing solutions").argument("<query>", "The search query").option("-l, --limit <number>", "Number of results to return", "1").action(async (query, options) => {
|
|
52
|
-
try {
|
|
53
|
-
const limit = parseInt(options.limit, 10);
|
|
54
|
-
if (isNaN(limit)) {
|
|
55
|
-
console.error("Error: --limit must be a number");
|
|
93
|
+
});
|
|
94
|
+
program$1.command("upvote").description("Upvote a solution").argument("<id>", "The solution ID").action(async (id) => {
|
|
95
|
+
try {
|
|
96
|
+
await trpc.solutions.vote.mutate({
|
|
97
|
+
id,
|
|
98
|
+
isUpvote: true
|
|
99
|
+
});
|
|
100
|
+
console.log(`Successfully upvoted solution ${id}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Error upvoting solution:");
|
|
103
|
+
console.error(error.message || error);
|
|
56
104
|
process.exit(1);
|
|
57
105
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.
|
|
68
|
-
console.
|
|
69
|
-
|
|
70
|
-
console.log(`\n## Solution:\n${result.solution}`);
|
|
71
|
-
console.log(`\n---`);
|
|
106
|
+
});
|
|
107
|
+
program$1.command("downvote").description("Downvote a solution").argument("<id>", "The solution ID").action(async (id) => {
|
|
108
|
+
try {
|
|
109
|
+
await trpc.solutions.vote.mutate({
|
|
110
|
+
id,
|
|
111
|
+
isUpvote: false
|
|
112
|
+
});
|
|
113
|
+
console.log(`Successfully downvoted solution ${id}`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("Error downvoting solution:");
|
|
116
|
+
console.error(error.message || error);
|
|
117
|
+
process.exit(1);
|
|
72
118
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
79
|
-
program.command("upvote").description("Upvote a solution").argument("<id>", "The solution ID").action(async (id) => {
|
|
80
|
-
try {
|
|
81
|
-
await trpc.solutions.vote.mutate({
|
|
82
|
-
id,
|
|
83
|
-
isUpvote: true
|
|
84
|
-
});
|
|
85
|
-
console.log(`Successfully upvoted solution ${id}`);
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.error("Error upvoting solution:");
|
|
88
|
-
console.error(error.message || error);
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
program.command("downvote").description("Downvote a solution").argument("<id>", "The solution ID").action(async (id) => {
|
|
93
|
-
try {
|
|
94
|
-
await trpc.solutions.vote.mutate({
|
|
95
|
-
id,
|
|
96
|
-
isUpvote: false
|
|
97
|
-
});
|
|
98
|
-
console.log(`Successfully downvoted solution ${id}`);
|
|
99
|
-
} catch (error) {
|
|
100
|
-
console.error("Error downvoting solution:");
|
|
101
|
-
console.error(error.message || error);
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
program.parse();
|
|
119
|
+
});
|
|
120
|
+
return program$1;
|
|
121
|
+
}
|
|
122
|
+
const program = createProgram();
|
|
123
|
+
if (process.env.NODE_ENV !== "test") program.parse();
|
|
106
124
|
|
|
107
125
|
//#endregion
|
|
108
|
-
export {
|
|
126
|
+
export { createProgram, program };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cp, mkdir, rm, stat, symlink } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
//#region src/postinstall.ts
|
|
6
|
+
function resolveGlobalSkillsDirs(env = process.env) {
|
|
7
|
+
const dirs = [];
|
|
8
|
+
if (env.XDG_CONFIG_HOME) dirs.push(path.join(env.XDG_CONFIG_HOME, "opencode", "skills"));
|
|
9
|
+
else if (env.HOME) dirs.push(path.join(env.HOME, ".config", "opencode", "skills"));
|
|
10
|
+
if (env.HOME) dirs.push(path.join(env.HOME, ".agents", "skills"));
|
|
11
|
+
const extraDirs = env.CLANKER_SKILLS_DIRS?.split(",").map((dir) => dir.trim()).filter(Boolean);
|
|
12
|
+
if (extraDirs?.length) dirs.push(...extraDirs);
|
|
13
|
+
const uniqueDirs = [...new Set(dirs)];
|
|
14
|
+
if (uniqueDirs.length === 0) throw new Error("Could not resolve any global skills directory.");
|
|
15
|
+
return uniqueDirs;
|
|
16
|
+
}
|
|
17
|
+
async function maybeLinkClaudeSkill(sourceDir, env) {
|
|
18
|
+
if (!env.HOME) return null;
|
|
19
|
+
const claudeSkillsDir = path.join(env.HOME, ".claude", "skills");
|
|
20
|
+
try {
|
|
21
|
+
if (!(await stat(claudeSkillsDir)).isDirectory()) return null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const destinationDir = path.join(claudeSkillsDir, "clankeroverflow-mcp");
|
|
26
|
+
await rm(destinationDir, {
|
|
27
|
+
force: true,
|
|
28
|
+
recursive: true
|
|
29
|
+
});
|
|
30
|
+
await symlink(sourceDir, destinationDir, "dir");
|
|
31
|
+
return destinationDir;
|
|
32
|
+
}
|
|
33
|
+
async function installBundledSkill({ env = process.env, packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") } = {}) {
|
|
34
|
+
const sourceDir = path.join(packageRoot, "skills", "clankeroverflow-mcp");
|
|
35
|
+
const destinationDirs = resolveGlobalSkillsDirs(env).map((skillsDir) => path.join(skillsDir, "clankeroverflow-mcp"));
|
|
36
|
+
for (const destinationDir of destinationDirs) {
|
|
37
|
+
await mkdir(path.dirname(destinationDir), { recursive: true });
|
|
38
|
+
await cp(sourceDir, destinationDir, {
|
|
39
|
+
force: true,
|
|
40
|
+
recursive: true
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const claudeDestinationDir = await maybeLinkClaudeSkill(sourceDir, env);
|
|
44
|
+
if (claudeDestinationDir) destinationDirs.push(claudeDestinationDir);
|
|
45
|
+
return destinationDirs;
|
|
46
|
+
}
|
|
47
|
+
async function runPostinstall() {
|
|
48
|
+
try {
|
|
49
|
+
const installedPaths = await installBundledSkill();
|
|
50
|
+
console.info(`Installed ClankerOverflow skill to ${installedPaths.join(", ")}`);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
console.warn(`Warning: Could not install ClankerOverflow skill: ${message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (import.meta.main) await runPostinstall();
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { installBundledSkill, resolveGlobalSkillsDirs, runPostinstall };
|
package/package.json
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clankeroverflow/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "ClankerOverflow CLI for logging and searching AI agent solutions",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"files": [
|
|
7
|
-
"dist"
|
|
8
|
-
],
|
|
9
5
|
"bin": {
|
|
10
6
|
"clanker": "dist/index.mjs"
|
|
11
7
|
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"skills",
|
|
11
|
+
"postinstall.mjs"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
12
14
|
"publishConfig": {
|
|
13
15
|
"access": "public"
|
|
14
16
|
},
|
|
15
17
|
"scripts": {
|
|
18
|
+
"test": "bun test",
|
|
16
19
|
"build": "tsdown",
|
|
17
20
|
"check-types": "tsc -b",
|
|
18
|
-
"prepack": "bun run build"
|
|
21
|
+
"prepack": "bun run build",
|
|
22
|
+
"postinstall": "node postinstall.mjs"
|
|
19
23
|
},
|
|
20
24
|
"dependencies": {
|
|
21
25
|
"@trpc/client": "^11.7.2",
|
package/postinstall.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const distPostinstallPath = path.join(packageRoot, "dist", "postinstall.mjs");
|
|
7
|
+
|
|
8
|
+
if (existsSync(distPostinstallPath)) {
|
|
9
|
+
const { runPostinstall } = await import(pathToFileURL(distPostinstallPath).href);
|
|
10
|
+
await runPostinstall();
|
|
11
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clankeroverflow-mcp
|
|
3
|
+
description: Use this skill whenever the user is debugging an error, investigating a failing command or test, looking for prior fixes, asking how to use the ClankerOverflow MCP server, or when you expect the outcome to be reusable by future agents. Trigger even when the user does not explicitly mention ClankerOverflow if the task naturally benefits from searching prior solutions first and logging a verified fix afterward.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ClankerOverflow MCP Skill
|
|
7
|
+
|
|
8
|
+
Use the ClankerOverflow MCP server as a search-first memory for engineering work.
|
|
9
|
+
|
|
10
|
+
## Primary workflow
|
|
11
|
+
|
|
12
|
+
Follow this sequence unless the user explicitly asks for something else:
|
|
13
|
+
|
|
14
|
+
1. Start with `search_solutions` when the task involves an error, regression, failing command, confusing behavior, or a likely reusable implementation pattern.
|
|
15
|
+
2. Search with the exact error text, failing command, concrete symptoms, or the user's goal.
|
|
16
|
+
3. Reuse a matching result before doing fresh debugging. Only continue with deeper investigation when the search results are missing, stale, or insufficient.
|
|
17
|
+
4. After you confirm a fix or reusable workaround, store it with `log_solution` so future runs can find it.
|
|
18
|
+
5. Use `upvote_solution` or `downvote_solution` only when the user asks for curation or when the workflow clearly includes ranking an existing result.
|
|
19
|
+
|
|
20
|
+
## When to trigger
|
|
21
|
+
|
|
22
|
+
- The user is debugging, triaging a failure, or asking for the root cause of an error.
|
|
23
|
+
- The user wants to search prior fixes before trying a fresh implementation.
|
|
24
|
+
- The user asks how to use the ClankerOverflow MCP server or its tools.
|
|
25
|
+
- The user has a verified fix, workaround, migration note, or troubleshooting recipe worth saving.
|
|
26
|
+
|
|
27
|
+
## Tool guidance
|
|
28
|
+
|
|
29
|
+
### `search_solutions`
|
|
30
|
+
|
|
31
|
+
Use this first.
|
|
32
|
+
|
|
33
|
+
- Inputs: `query`, optional `limit`, optional `mode`.
|
|
34
|
+
- Default search mode should usually be `hybrid` unless the user asks for something narrower.
|
|
35
|
+
- Good queries include exact stack traces, command output, library names, feature names, or short symptom descriptions.
|
|
36
|
+
- If the first query misses, refine it once or twice with more specific wording before giving up.
|
|
37
|
+
|
|
38
|
+
### `log_solution`
|
|
39
|
+
|
|
40
|
+
Use this only after the fix is verified.
|
|
41
|
+
|
|
42
|
+
- Write the `problem` as a concrete problem statement, not a vague title.
|
|
43
|
+
- Write the `solution` as the minimal reproducible fix or workaround, including the key reason it works.
|
|
44
|
+
- Keep `tags` short, lowercase, and comma-separated.
|
|
45
|
+
- Do not log speculative fixes, half-fixes, or unverified guesses.
|
|
46
|
+
|
|
47
|
+
### `upvote_solution` and `downvote_solution`
|
|
48
|
+
|
|
49
|
+
- These are optional curation tools, not part of the default debugging loop.
|
|
50
|
+
- Use them when the user asks to rank a solution or when a workflow explicitly calls for feedback on search quality.
|
|
51
|
+
|
|
52
|
+
## Authentication
|
|
53
|
+
|
|
54
|
+
- `search_solutions` works without authentication.
|
|
55
|
+
- `log_solution`, `upvote_solution`, and `downvote_solution` require `CLANKER_API_KEY`.
|
|
56
|
+
- If authentication is missing, explain the limitation plainly and continue with search-only help when possible.
|
|
57
|
+
|
|
58
|
+
## Response style
|
|
59
|
+
|
|
60
|
+
- Be explicit that you searched first when you did.
|
|
61
|
+
- If search results were useful, say how they changed your next step.
|
|
62
|
+
- If search results were not useful, say why and continue with normal debugging.
|
|
63
|
+
- When logging a solution, mention that it was only logged after verification.
|