qbsm-init 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/dist/init.d.ts +2 -0
- package/dist/init.js +224 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# qbsm-init
|
|
2
|
+
|
|
3
|
+
One-shot CLI that wires **qb-skill-manager** (skills auto-extraction for Claude Code) into any project. After running this, Claude Code in that project can call the qbsm MCP server (SSE, hosted on Fly.io) to record skills demonstrated in the conversation.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. Sign up + issue an API token at https://qb-skill-manager.fly.dev/settings/tokens
|
|
9
|
+
# 2. Run qbsm-init in your project directory:
|
|
10
|
+
npx qbsm-init --api-key qbsm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That writes three files in your project (all merged idempotently if they already exist):
|
|
14
|
+
|
|
15
|
+
- `.mcp.json` — registers `qbsm` as a remote SSE MCP server
|
|
16
|
+
- `CLAUDE.md` — appends a guidance section so Claude Code knows when to call the tools
|
|
17
|
+
- `.claude/settings.json` — hooks that nudge the agent to call `register_skill` after package installs
|
|
18
|
+
|
|
19
|
+
Then start a Claude Code session as usual; the `qbsm` server appears in `claude mcp list`.
|
|
20
|
+
|
|
21
|
+
## Options
|
|
22
|
+
|
|
23
|
+
| Flag | Description |
|
|
24
|
+
|------|-------------|
|
|
25
|
+
| `--api-key <key>` | Required (or interactive prompt). Get at `https://qb-skill-manager.fly.dev/settings/tokens` |
|
|
26
|
+
| `--server-url <url>` | Override server (default `https://qb-skill-manager.fly.dev`) |
|
|
27
|
+
| `-y, --yes` | Non-interactive mode; `--api-key` becomes required |
|
|
28
|
+
| `--no-hooks` | Skip `.claude/settings.json` hook registration |
|
|
29
|
+
| `--overwrite` | Replace existing `<qbsm:start>…<qbsm:end>` section in CLAUDE.md |
|
|
30
|
+
| `-h, --help` | Show help |
|
|
31
|
+
|
|
32
|
+
Positional `[target-path]` (default `.`) lets you point at a sibling directory.
|
|
33
|
+
|
|
34
|
+
## Generated `.mcp.json` entry
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"qbsm": {
|
|
40
|
+
"type": "sse",
|
|
41
|
+
"url": "https://qb-skill-manager.fly.dev/api/mcp/sse",
|
|
42
|
+
"headers": { "x-api-key": "qbsm_..." }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`.mcp.json` is gitignored by default in Next.js / many templates. **Do not commit your API key**.
|
|
49
|
+
|
|
50
|
+
## Tools exposed
|
|
51
|
+
|
|
52
|
+
| Tool | Purpose |
|
|
53
|
+
|------|---------|
|
|
54
|
+
| `auth_status` | Confirm which user the API key resolves to |
|
|
55
|
+
| `register_skill` | Record a demonstrated skill (idempotent on `category + name`) |
|
|
56
|
+
| `update_skill` | Adjust proficiency / reason / tags on an existing skill |
|
|
57
|
+
| `list_my_skills` | List the user's recent skills |
|
|
58
|
+
|
|
59
|
+
## Tokens
|
|
60
|
+
|
|
61
|
+
If a token is leaked, revoke it at `https://qb-skill-manager.fly.dev/settings/tokens` and re-run `qbsm-init --api-key <new>` to overwrite `.mcp.json`.
|
package/dist/init.d.ts
ADDED
package/dist/init.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
const DEFAULT_SERVER_URL = "https://qb-skill-manager.fly.dev";
|
|
6
|
+
const SERVER_KEY = "qbsm";
|
|
7
|
+
const CLAUDE_MD_SECTION_START = "<!-- qbsm:start -->";
|
|
8
|
+
const CLAUDE_MD_SECTION_END = "<!-- qbsm:end -->";
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const args = argv.slice(2);
|
|
11
|
+
const flags = {
|
|
12
|
+
targetPath: ".",
|
|
13
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
14
|
+
yes: false,
|
|
15
|
+
noHooks: false,
|
|
16
|
+
overwrite: false,
|
|
17
|
+
};
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
const a = args[i];
|
|
20
|
+
if (a === "--help" || a === "-h") {
|
|
21
|
+
printHelp();
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
else if (a === "--yes" || a === "-y") {
|
|
25
|
+
flags.yes = true;
|
|
26
|
+
}
|
|
27
|
+
else if (a === "--no-hooks") {
|
|
28
|
+
flags.noHooks = true;
|
|
29
|
+
}
|
|
30
|
+
else if (a === "--overwrite") {
|
|
31
|
+
flags.overwrite = true;
|
|
32
|
+
}
|
|
33
|
+
else if (a === "--api-key") {
|
|
34
|
+
flags.apiKey = args[++i];
|
|
35
|
+
}
|
|
36
|
+
else if (a === "--server-url") {
|
|
37
|
+
flags.serverUrl = args[++i];
|
|
38
|
+
}
|
|
39
|
+
else if (!a.startsWith("-")) {
|
|
40
|
+
flags.targetPath = a;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return flags;
|
|
44
|
+
}
|
|
45
|
+
function printHelp() {
|
|
46
|
+
console.log(`
|
|
47
|
+
qbsm-init — set up qb-skill-manager MCP integration for a project
|
|
48
|
+
|
|
49
|
+
Usage:
|
|
50
|
+
npx qbsm-init [target-path] --api-key <qbsm_...> [options]
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--api-key <key> Required. API token from ${DEFAULT_SERVER_URL}/settings/tokens
|
|
54
|
+
--server-url <url> Override server URL (default: ${DEFAULT_SERVER_URL})
|
|
55
|
+
-y, --yes Skip all prompts; use defaults
|
|
56
|
+
--no-hooks Skip .claude/settings.json hooks registration
|
|
57
|
+
--overwrite Overwrite .mcp.json / CLAUDE.md section if present
|
|
58
|
+
-h, --help Show this help
|
|
59
|
+
|
|
60
|
+
What it does (idempotent merge):
|
|
61
|
+
- .mcp.json adds an "${SERVER_KEY}" entry pointing to the SSE endpoint
|
|
62
|
+
- CLAUDE.md appends a guidance section between qbsm:start/end markers
|
|
63
|
+
- .claude/settings.json
|
|
64
|
+
registers PostToolUse (npm/pip/go/cargo install detector)
|
|
65
|
+
and Stop (session-stop hint) hooks unless --no-hooks
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
function ask(question, defaultValue) {
|
|
69
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
71
|
+
return new Promise((resolveAns) => {
|
|
72
|
+
rl.question(`${question}${suffix}: `, (a) => {
|
|
73
|
+
rl.close();
|
|
74
|
+
resolveAns(a.trim() || defaultValue || "");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function readJson(path) {
|
|
79
|
+
if (!existsSync(path))
|
|
80
|
+
return null;
|
|
81
|
+
try {
|
|
82
|
+
const raw = readFileSync(path, "utf-8");
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function writeJson(path, value, mode = 0o644) {
|
|
94
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + "\n", { mode });
|
|
95
|
+
}
|
|
96
|
+
function writeMcpJson(target, apiKey, serverUrl, overwrite) {
|
|
97
|
+
const path = join(target, ".mcp.json");
|
|
98
|
+
const entry = {
|
|
99
|
+
type: "sse",
|
|
100
|
+
url: `${serverUrl.replace(/\/$/, "")}/api/mcp/sse`,
|
|
101
|
+
headers: { "x-api-key": apiKey },
|
|
102
|
+
};
|
|
103
|
+
let config = readJson(path) ?? {};
|
|
104
|
+
const servers = (config.mcpServers && typeof config.mcpServers === "object"
|
|
105
|
+
? config.mcpServers
|
|
106
|
+
: {});
|
|
107
|
+
if (servers[SERVER_KEY] && !overwrite) {
|
|
108
|
+
// upsert: replace existing qbsm entry (always — overwrite flag is for non-qbsm collisions)
|
|
109
|
+
}
|
|
110
|
+
servers[SERVER_KEY] = entry;
|
|
111
|
+
config.mcpServers = servers;
|
|
112
|
+
writeJson(path, config);
|
|
113
|
+
return path;
|
|
114
|
+
}
|
|
115
|
+
const CLAUDE_MD_BODY = `## qbsm (qb-skill-manager) usage
|
|
116
|
+
|
|
117
|
+
You have access to the **qbsm** MCP server (SSE, remote). Use it to record skills you observe in the conversation.
|
|
118
|
+
|
|
119
|
+
When to call:
|
|
120
|
+
- After installing a package (npm/pip/go/cargo), call \`register_skill\` with \`category: "technical"\`, \`type: "tool"\` and the package name.
|
|
121
|
+
- When the user demonstrates a non-obvious soft / domain skill, call \`register_skill\` with appropriate category.
|
|
122
|
+
- Use \`list_my_skills\` to recall what's already recorded before suggesting a skill.
|
|
123
|
+
- Duplicates (same \`category + name\`) are deduplicated server-side; calling \`register_skill\` again only bumps the occurrence count.
|
|
124
|
+
|
|
125
|
+
Tool reference:
|
|
126
|
+
- \`auth_status\` — confirm which user the API key resolves to.
|
|
127
|
+
- \`register_skill(category, type, name, proficiency, reason, [confidence], [tags], [excerpt])\` — record a skill.
|
|
128
|
+
- \`update_skill(skillId, [proficiency], [reason], [tags])\` — adjust an existing skill.
|
|
129
|
+
- \`list_my_skills([limit], [category])\` — list the user's recent skills.
|
|
130
|
+
`;
|
|
131
|
+
function writeClaudeMd(target, overwrite) {
|
|
132
|
+
const path = join(target, "CLAUDE.md");
|
|
133
|
+
const block = `${CLAUDE_MD_SECTION_START}\n${CLAUDE_MD_BODY}${CLAUDE_MD_SECTION_END}\n`;
|
|
134
|
+
if (!existsSync(path)) {
|
|
135
|
+
writeFileSync(path, block, { mode: 0o644 });
|
|
136
|
+
return { path, action: "created" };
|
|
137
|
+
}
|
|
138
|
+
const current = readFileSync(path, "utf-8");
|
|
139
|
+
const startIdx = current.indexOf(CLAUDE_MD_SECTION_START);
|
|
140
|
+
const endIdx = current.indexOf(CLAUDE_MD_SECTION_END);
|
|
141
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
142
|
+
if (!overwrite)
|
|
143
|
+
return { path, action: "skipped" };
|
|
144
|
+
const before = current.slice(0, startIdx);
|
|
145
|
+
const after = current.slice(endIdx + CLAUDE_MD_SECTION_END.length);
|
|
146
|
+
writeFileSync(path, `${before}${block.trimEnd()}\n${after.startsWith("\n") ? after.slice(1) : after}`, { mode: 0o644 });
|
|
147
|
+
return { path, action: "replaced" };
|
|
148
|
+
}
|
|
149
|
+
const sep = current.endsWith("\n") ? "" : "\n";
|
|
150
|
+
writeFileSync(path, `${current}${sep}\n${block}`, { mode: 0o644 });
|
|
151
|
+
return { path, action: "appended" };
|
|
152
|
+
}
|
|
153
|
+
const POST_TOOL_USE_INLINE = `bash -c 'INPUT=$(cat); CMD=$(echo "$INPUT" | jq -r ".tool_input.command // empty"); TOOL=$(echo "$INPUT" | jq -r ".tool_name // empty"); if [ "$TOOL" = "Bash" ] && echo "$CMD" | grep -qE "^[[:space:]]*(npm|pnpm|yarn|bun)[[:space:]]+(install|add|i)[[:space:]]+|^[[:space:]]*(pip|pip3|uv pip)[[:space:]]+install[[:space:]]+|^[[:space:]]*go[[:space:]]+(get|install)[[:space:]]+|^[[:space:]]*cargo[[:space:]]+(add|install)[[:space:]]+"; then echo "[qbsm] Detected a package install — consider calling MCP tool register_skill for each package actually used."; fi'`;
|
|
154
|
+
const SESSION_STOP_INLINE = `bash -c 'echo "[qbsm] Session ended — if this session demonstrated new skills, call register_skill via the qbsm MCP server before exiting."'`;
|
|
155
|
+
function writeClaudeSettings(target) {
|
|
156
|
+
const dir = join(target, ".claude");
|
|
157
|
+
mkdirSync(dir, { recursive: true });
|
|
158
|
+
const path = join(dir, "settings.json");
|
|
159
|
+
const config = readJson(path) ?? {};
|
|
160
|
+
const hooks = (config.hooks && typeof config.hooks === "object"
|
|
161
|
+
? config.hooks
|
|
162
|
+
: {});
|
|
163
|
+
function upsertHook(event, matcher, command) {
|
|
164
|
+
const list = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
165
|
+
const already = list.some((g) => g.hooks?.some((h) => h.type === "command" && h.command === command));
|
|
166
|
+
if (already)
|
|
167
|
+
return;
|
|
168
|
+
list.push({ ...(matcher ? { matcher } : {}), hooks: [{ type: "command", command }] });
|
|
169
|
+
hooks[event] = list;
|
|
170
|
+
}
|
|
171
|
+
upsertHook("PostToolUse", "Bash", POST_TOOL_USE_INLINE);
|
|
172
|
+
upsertHook("Stop", undefined, SESSION_STOP_INLINE);
|
|
173
|
+
config.hooks = hooks;
|
|
174
|
+
const action = existsSync(path) ? "merged" : "created";
|
|
175
|
+
writeJson(path, config);
|
|
176
|
+
return { path, action };
|
|
177
|
+
}
|
|
178
|
+
async function main() {
|
|
179
|
+
const flags = parseArgs(process.argv);
|
|
180
|
+
const target = resolve(flags.targetPath);
|
|
181
|
+
console.log("\n🔧 qbsm-init — qb-skill-manager setup\n");
|
|
182
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
183
|
+
console.error(`✗ Target path is not a directory: ${target}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
let apiKey = flags.apiKey;
|
|
187
|
+
if (!apiKey) {
|
|
188
|
+
if (flags.yes) {
|
|
189
|
+
console.error(`✗ --api-key is required when --yes is set. Get one at ${flags.serverUrl}/settings/tokens`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
apiKey = await ask(`API key from ${flags.serverUrl}/settings/tokens`);
|
|
193
|
+
if (!apiKey) {
|
|
194
|
+
console.error("✗ No api-key provided. Aborting.");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!apiKey.startsWith("qbsm_")) {
|
|
199
|
+
console.error("✗ API key must start with 'qbsm_'");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const mcpPath = writeMcpJson(target, apiKey, flags.serverUrl, flags.overwrite);
|
|
203
|
+
console.log(`✓ .mcp.json ${mcpPath}`);
|
|
204
|
+
const claude = writeClaudeMd(target, flags.overwrite);
|
|
205
|
+
console.log(`✓ CLAUDE.md ${claude.path} (${claude.action})`);
|
|
206
|
+
if (flags.noHooks) {
|
|
207
|
+
console.log("→ Skipped .claude/settings.json (--no-hooks)");
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const settings = writeClaudeSettings(target);
|
|
211
|
+
console.log(`✓ .claude/settings.json ${settings.path} (${settings.action})`);
|
|
212
|
+
}
|
|
213
|
+
console.log(`
|
|
214
|
+
Done. Start a Claude Code session in ${target} and the qbsm MCP server will be
|
|
215
|
+
reachable at ${flags.serverUrl}/api/mcp/sse with your API key.
|
|
216
|
+
|
|
217
|
+
To verify quickly:
|
|
218
|
+
cd ${target} && claude mcp list # should include "qbsm"
|
|
219
|
+
`);
|
|
220
|
+
}
|
|
221
|
+
main().catch((e) => {
|
|
222
|
+
console.error(`✗ ${e.message}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qbsm-init",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-shot CLI to wire qb-skill-manager MCP into a project (.mcp.json + CLAUDE.md + hooks)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"qbsm-init": "dist/init.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"claude",
|
|
20
|
+
"qb-skill-manager",
|
|
21
|
+
"qbsm"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/QuestBoard-inc/qb-skill-manager.git",
|
|
29
|
+
"directory": "cli"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/QuestBoard-inc/qb-skill-manager#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/QuestBoard-inc/qb-skill-manager/issues"
|
|
34
|
+
},
|
|
35
|
+
"license": "UNLICENSED",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"typescript": "^5.5.0"
|
|
39
|
+
}
|
|
40
|
+
}
|