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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }