skill-tree-ai 1.0.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.
@@ -0,0 +1,58 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { homedir } from "node:os";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ const execFileAsync = promisify(execFile);
8
+ function getTemplateDir() {
9
+ // Try relative to this file first (works in dev and dist)
10
+ const thisDir = dirname(fileURLToPath(import.meta.url));
11
+ // In dist/core/render.js → templates is at ../../templates
12
+ const distPath = join(thisDir, "..", "..", "templates");
13
+ if (existsSync(distPath))
14
+ return distPath;
15
+ // In src/core/render.ts → templates is at ../../templates
16
+ const srcPath = join(thisDir, "..", "..", "templates");
17
+ if (existsSync(srcPath))
18
+ return srcPath;
19
+ // Fallback: look in cwd
20
+ const cwdPath = join(process.cwd(), "templates");
21
+ if (existsSync(cwdPath))
22
+ return cwdPath;
23
+ throw new Error("Cannot find templates directory");
24
+ }
25
+ function getOutputDir() {
26
+ const dir = join(homedir(), ".skill-tree");
27
+ return dir;
28
+ }
29
+ export function renderHTML(profile) {
30
+ const templatePath = join(getTemplateDir(), "skill-tree.html");
31
+ const template = readFileSync(templatePath, "utf-8");
32
+ // Escape </script> in JSON to prevent HTML parser breaking
33
+ const jsonStr = JSON.stringify(profile).replace(/<\/script>/gi, "<\\/script>");
34
+ return template.replace("__PROFILE_DATA__", jsonStr);
35
+ }
36
+ export function writeAndOpen(profile) {
37
+ const html = renderHTML(profile);
38
+ const outputPath = join(getOutputDir(), "report.html");
39
+ writeFileSync(outputPath, html);
40
+ return outputPath;
41
+ }
42
+ export async function openInBrowser(filePath) {
43
+ const platform = process.platform;
44
+ try {
45
+ if (platform === "darwin") {
46
+ await execFileAsync("open", [filePath]);
47
+ }
48
+ else if (platform === "linux") {
49
+ await execFileAsync("xdg-open", [filePath]);
50
+ }
51
+ else if (platform === "win32") {
52
+ await execFileAsync("cmd", ["/c", "start", filePath]);
53
+ }
54
+ }
55
+ catch {
56
+ // Silent — caller has the path
57
+ }
58
+ }
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ const isRemote = process.argv.includes("--remote") ||
3
+ process.env.SKILL_TREE_REMOTE === "1";
4
+ if (isRemote) {
5
+ await import("./remote.js");
6
+ }
7
+ else {
8
+ await import("./local.js");
9
+ }
10
+ export {};
package/dist/local.js ADDED
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import Anthropic from "@anthropic-ai/sdk";
6
+ import { findAllSessions } from "./core/extract.js";
7
+ import { classifySessions } from "./core/classify.js";
8
+ import { buildProfile } from "./core/profile.js";
9
+ import { writeAndOpen, openInBrowser } from "./core/render.js";
10
+ import { MCP_INSTRUCTIONS } from "./shared.js";
11
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { homedir } from "node:os";
14
+ // --- State & Persistence ---
15
+ let cachedProfile = null;
16
+ function getSkillTreeDir() {
17
+ const dir = join(homedir(), ".skill-tree");
18
+ if (!existsSync(dir))
19
+ mkdirSync(dir, { recursive: true });
20
+ return dir;
21
+ }
22
+ function getProfilePath() {
23
+ return join(getSkillTreeDir(), "profile.json");
24
+ }
25
+ function getHistoryDir() {
26
+ const dir = join(getSkillTreeDir(), "history");
27
+ if (!existsSync(dir))
28
+ mkdirSync(dir, { recursive: true });
29
+ return dir;
30
+ }
31
+ function loadCachedProfile() {
32
+ const path = getProfilePath();
33
+ if (!existsSync(path))
34
+ return null;
35
+ try {
36
+ return JSON.parse(readFileSync(path, "utf-8"));
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ function saveProfile(profile) {
43
+ const dir = getSkillTreeDir();
44
+ // Save current profile
45
+ writeFileSync(join(dir, "profile.json"), JSON.stringify(profile, null, 2));
46
+ // Save timestamped snapshot for progress tracking
47
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
48
+ writeFileSync(join(getHistoryDir(), `profile-${ts}.json`), JSON.stringify(profile, null, 2));
49
+ // Write growth-quest.txt for SessionStart hook
50
+ writeFileSync(join(dir, "growth-quest.txt"), profile.archetype.growth_quest);
51
+ }
52
+ // --- MCP Server ---
53
+ const server = new McpServer({ name: "skill-tree-ai", version: "1.0.0" }, { instructions: MCP_INSTRUCTIONS });
54
+ server.tool("analyze", "Analyze your Claude conversation history. Scans Claude Code and Cowork sessions, classifies 11 AI collaboration behaviors, builds a skill profile with character archetype and growth recommendation. Returns the full profile.", {
55
+ max_sessions: z
56
+ .number()
57
+ .default(100)
58
+ .describe("Maximum number of sessions to analyze (most recent first)"),
59
+ force_refresh: z
60
+ .boolean()
61
+ .default(false)
62
+ .describe("Force re-analysis even if cached results exist"),
63
+ }, async ({ max_sessions, force_refresh }) => {
64
+ // Check cache unless forced
65
+ if (!force_refresh) {
66
+ const cached = loadCachedProfile();
67
+ if (cached) {
68
+ cachedProfile = cached;
69
+ return {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: formatProfileSummary(cached),
74
+ },
75
+ ],
76
+ };
77
+ }
78
+ }
79
+ const apiKey = process.env.ANTHROPIC_API_KEY;
80
+ if (!apiKey) {
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: "Error: ANTHROPIC_API_KEY not set. Set it in your environment to enable skill tree analysis.",
86
+ },
87
+ ],
88
+ };
89
+ }
90
+ const client = new Anthropic({ apiKey });
91
+ // Extract sessions
92
+ const sessions = findAllSessions(max_sessions);
93
+ if (sessions.length === 0) {
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: "No conversation sessions found. Make sure you have Claude Code or Cowork conversations on this machine.",
99
+ },
100
+ ],
101
+ };
102
+ }
103
+ // Classify (uses per-session cache — only new sessions hit the API)
104
+ const classifications = await classifySessions(client, sessions.map((s) => ({
105
+ sessionId: s.sessionId,
106
+ messages: s.messages,
107
+ })), 50);
108
+ // Build profile (deterministic — no LLM call)
109
+ const previousProfile = loadCachedProfile();
110
+ const profile = buildProfile(classifications, previousProfile);
111
+ cachedProfile = profile;
112
+ saveProfile(profile);
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: formatProfileSummary(profile),
118
+ },
119
+ ],
120
+ };
121
+ });
122
+ server.tool("visualize", "Generate a beautiful HTML skill tree visualization and open it in your browser. Run 'analyze' first to generate the profile data.", {}, async () => {
123
+ const profile = cachedProfile || loadCachedProfile();
124
+ if (!profile) {
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: "No profile data available. Run the 'analyze' tool first.",
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ const outputPath = writeAndOpen(profile);
135
+ await openInBrowser(outputPath);
136
+ return {
137
+ content: [
138
+ {
139
+ type: "text",
140
+ text: `Skill tree visualization generated and opened in browser.\nFile: ${outputPath}`,
141
+ },
142
+ ],
143
+ };
144
+ });
145
+ server.tool("growth_quest", "Get your current growth recommendation — one specific thing to try in your next session, based on your archetype's growth path.", {}, async () => {
146
+ const profile = cachedProfile || loadCachedProfile();
147
+ if (!profile) {
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: "No profile data available. Run the 'analyze' tool first to get personalized recommendations.",
153
+ },
154
+ ],
155
+ };
156
+ }
157
+ const a = profile.archetype;
158
+ const ge = profile.growth_edge;
159
+ const userPct = Math.round(ge.rate * 100);
160
+ const basePct = Math.round(ge.baseline * 100);
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: [
166
+ `${a.name} — ${a.tagline}`,
167
+ ``,
168
+ `Superpower: ${a.superpower}`,
169
+ ``,
170
+ `Next Unlock: ${a.growth_unlock}`,
171
+ ``,
172
+ `Quest: ${a.growth_quest}`,
173
+ ``,
174
+ `Growth edge: ${ge.label} — you're at ${userPct}%, population average is ${basePct}%`,
175
+ ].join("\n"),
176
+ },
177
+ ],
178
+ };
179
+ });
180
+ // --- Format helpers ---
181
+ function formatProfileSummary(profile) {
182
+ const a = profile.archetype;
183
+ const lines = [
184
+ `╔══════════════════════════════════════╗`,
185
+ `║ ${a.name.padStart(18).padEnd(36)}║`,
186
+ `╚══════════════════════════════════════╝`,
187
+ `"${a.tagline}"`,
188
+ ``,
189
+ `Superpower: ${a.superpower}`,
190
+ ``,
191
+ `─── Skill Profile (${profile.total_sessions} sessions) ───`,
192
+ ``,
193
+ ];
194
+ for (const [axisName, axis] of Object.entries(profile.branches)) {
195
+ const indicator = axis.above_baseline ? "+" : "-";
196
+ lines.push(`${axisName} (${indicator} vs avg):`);
197
+ for (const key of axis.behaviors) {
198
+ const b = profile.behaviors[key];
199
+ if (!b)
200
+ continue;
201
+ const pct = Math.round(b.rate * 100);
202
+ const basePct = Math.round(b.baseline * 100);
203
+ const arrow = b.above_baseline ? "↑" : "↓";
204
+ const bar = "■".repeat(Math.round(pct / 10)) +
205
+ "□".repeat(10 - Math.round(pct / 10));
206
+ lines.push(` ${bar} ${pct}% ${b.label} (avg: ${basePct}%) ${arrow}`);
207
+ }
208
+ lines.push(``);
209
+ }
210
+ const ge = profile.growth_edge;
211
+ lines.push(`Next Unlock: ${a.growth_unlock}`);
212
+ lines.push(`Quest: ${a.growth_quest}`);
213
+ if (profile.previous_archetype && profile.previous_archetype !== a.key) {
214
+ lines.push(`\nProgress: Archetype changed from ${profile.previous_archetype} → ${a.key}`);
215
+ }
216
+ return lines.join("\n");
217
+ }
218
+ // --- Start ---
219
+ const transport = new StdioServerTransport();
220
+ await server.connect(transport);
package/dist/remote.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from "node:http";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StreamableHTTPServerTransport, } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { z } from "zod";
6
+ import Anthropic from "@anthropic-ai/sdk";
7
+ import { findAllSessions } from "./core/extract.js";
8
+ import { classifySessions } from "./core/classify.js";
9
+ import { buildProfile } from "./core/profile.js";
10
+ import { renderHTML } from "./core/render.js";
11
+ import { MCP_INSTRUCTIONS } from "./shared.js";
12
+ import { randomUUID } from "node:crypto";
13
+ const PORT = parseInt(process.env.PORT || "3000", 10);
14
+ // Per-session MCP servers
15
+ const sessions = new Map();
16
+ function createSessionServer() {
17
+ const server = new McpServer({ name: "skill-tree-ai", version: "1.0.0" }, { instructions: MCP_INSTRUCTIONS });
18
+ // analyze tool (simplified for remote — operates on provided data or sample)
19
+ server.tool("analyze", "Analyze conversation history and return skill profile.", {
20
+ conversation_json: z
21
+ .string()
22
+ .optional()
23
+ .describe("Optional: paste conversation JSONL content to analyze"),
24
+ }, async ({ conversation_json }) => {
25
+ const apiKey = process.env.ANTHROPIC_API_KEY;
26
+ if (!apiKey) {
27
+ return {
28
+ content: [{ type: "text", text: "Error: ANTHROPIC_API_KEY not configured on server." }],
29
+ };
30
+ }
31
+ const client = new Anthropic({ apiKey });
32
+ // If conversation_json provided, analyze it directly
33
+ // Otherwise, analyze local sessions (server-side)
34
+ const sessions = findAllSessions(50);
35
+ if (sessions.length === 0) {
36
+ return {
37
+ content: [{ type: "text", text: "No sessions found on this machine." }],
38
+ };
39
+ }
40
+ const classifications = await classifySessions(client, sessions.map((s) => ({ sessionId: s.sessionId, messages: s.messages })), 20);
41
+ const profile = buildProfile(classifications);
42
+ return {
43
+ content: [
44
+ { type: "text", text: JSON.stringify(profile, null, 2) },
45
+ ],
46
+ };
47
+ });
48
+ // visualize tool — returns HTML string
49
+ server.tool("visualize", "Generate HTML visualization from a profile.", {
50
+ profile_json: z.string().describe("Profile JSON from the analyze tool"),
51
+ }, async ({ profile_json }) => {
52
+ const profile = JSON.parse(profile_json);
53
+ const html = renderHTML(profile);
54
+ return {
55
+ content: [{ type: "text", text: html }],
56
+ };
57
+ });
58
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
59
+ server.connect(transport);
60
+ return { server, transport };
61
+ }
62
+ const httpServer = createServer(async (req, res) => {
63
+ const url = new URL(req.url || "/", `http://localhost:${PORT}`);
64
+ // CORS
65
+ res.setHeader("Access-Control-Allow-Origin", "*");
66
+ res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
67
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
68
+ if (req.method === "OPTIONS") {
69
+ res.writeHead(204);
70
+ res.end();
71
+ return;
72
+ }
73
+ if (url.pathname === "/health") {
74
+ res.writeHead(200, { "Content-Type": "application/json" });
75
+ res.end(JSON.stringify({ status: "ok", sessions: sessions.size }));
76
+ return;
77
+ }
78
+ if (url.pathname === "/mcp" && req.method === "POST") {
79
+ const sessionId = req.headers["mcp-session-id"];
80
+ let session;
81
+ if (sessionId && sessions.has(sessionId)) {
82
+ session = sessions.get(sessionId);
83
+ }
84
+ else {
85
+ session = createSessionServer();
86
+ const newId = randomUUID();
87
+ sessions.set(newId, session);
88
+ res.setHeader("mcp-session-id", newId);
89
+ }
90
+ // Forward request to transport
91
+ await session.transport.handleRequest(req, res, await readBody(req));
92
+ return;
93
+ }
94
+ res.writeHead(404);
95
+ res.end("Not found");
96
+ });
97
+ function readBody(req) {
98
+ return new Promise((resolve) => {
99
+ let body = "";
100
+ req.on("data", (chunk) => (body += chunk));
101
+ req.on("end", () => {
102
+ try {
103
+ resolve(JSON.parse(body));
104
+ }
105
+ catch {
106
+ resolve(body);
107
+ }
108
+ });
109
+ });
110
+ }
111
+ httpServer.listen(PORT, () => {
112
+ console.error(`Skill Tree MCP server running on http://localhost:${PORT}/mcp`);
113
+ });
package/dist/shared.js ADDED
@@ -0,0 +1,16 @@
1
+ export const MCP_INSTRUCTIONS = `Skill Tree analyzes your Claude conversation history to reveal your AI collaboration style.
2
+
3
+ It classifies 11 observable behaviors from the AI Fluency Framework across your sessions, builds a skill profile, and generates a character archetype card with a personalized growth recommendation.
4
+
5
+ ## Tools
6
+
7
+ - **analyze**: Scans your conversation history (Claude Code + Cowork), classifies behaviors, and returns your full skill profile with archetype and growth edge. Run this first.
8
+ - **visualize**: Generates a beautiful HTML skill tree visualization and opens it in your browser. Requires a profile from analyze.
9
+ - **growth_quest**: Returns your current recommended challenge — one specific thing to try in your next session.
10
+
11
+ ## When to use
12
+
13
+ Use \`analyze\` when the user asks about their Claude usage style, wants to see their skill tree, or runs /skill-tree.
14
+ Use \`visualize\` after analyze to show the results.
15
+ Use \`growth_quest\` when the user wants a quick recommendation without the full analysis.
16
+ `;
@@ -0,0 +1,15 @@
1
+ {
2
+ "description": "Skill tree growth quest — injects your active challenge into each session",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh\""
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # Inject the active growth quest into the session context.
3
+ # Reads from ~/.skill-tree/growth-quest.txt (written by profile.py after /skill-tree runs).
4
+
5
+ QUEST_FILE="$HOME/.skill-tree/growth-quest.txt"
6
+
7
+ if [ -f "$QUEST_FILE" ] && [ -s "$QUEST_FILE" ]; then
8
+ QUEST=$(cat "$QUEST_FILE")
9
+ cat << EOF
10
+ {
11
+ "hookSpecificOutput": {
12
+ "hookEventName": "SessionStart",
13
+ "additionalContext": "Skill Tree growth quest active: ${QUEST} — If a natural opportunity arises during this session, gently encourage the user to practice this behavior. Do not force it or mention this unless relevant."
14
+ }
15
+ }
16
+ EOF
17
+ else
18
+ # No quest yet — user hasn't run /skill-tree
19
+ cat << 'EOF'
20
+ {
21
+ "hookSpecificOutput": {
22
+ "hookEventName": "SessionStart",
23
+ "additionalContext": ""
24
+ }
25
+ }
26
+ EOF
27
+ fi
28
+
29
+ exit 0
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "skill-tree-ai",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Your AI collaboration style — skill tree visualization with character archetype cards and growth recommendations, grounded in the AI Fluency Framework. MCP server for Claude Code.",
6
+ "bin": {
7
+ "skill-tree-ai": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates",
12
+ "skills",
13
+ ".claude-plugin",
14
+ ".mcp.json",
15
+ "hooks",
16
+ "hooks-handlers"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "start": "node dist/index.js",
21
+ "start:remote": "SKILL_TREE_REMOTE=1 node dist/index.js",
22
+ "dev": "tsx src/index.ts",
23
+ "dev:remote": "SKILL_TREE_REMOTE=1 tsx src/index.ts",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "claude-code",
29
+ "skill-tree",
30
+ "ai-fluency",
31
+ "mcp-server",
32
+ "education"
33
+ ],
34
+ "license": "MIT",
35
+ "author": "Robert Nowell",
36
+ "dependencies": {
37
+ "@anthropic-ai/sdk": "^0.39.0",
38
+ "@modelcontextprotocol/sdk": "^1.12.0",
39
+ "zod": "^3.23.8"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.0.0",
43
+ "tsx": "^4.19.2",
44
+ "typescript": "^5.6.3"
45
+ }
46
+ }
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: skill-tree
3
+ description: Analyze your Claude collaboration style and generate a skill tree visualization with character archetype card. Use when the user says "skill tree", "show my skills", "analyze my style", or wants to see their AI fluency profile.
4
+ ---
5
+
6
+ # Skill Tree
7
+
8
+ Generate a personalized AI fluency profile by analyzing the user's Claude conversation history.
9
+
10
+ ## Workflow
11
+
12
+ Use the MCP tools in this order:
13
+
14
+ 1. Call the **`analyze`** tool — this scans your Claude Code and Cowork conversation history, classifies 11 AI collaboration behaviors using Claude Haiku, and returns your full skill profile with character archetype.
15
+
16
+ 2. Call the **`visualize`** tool — this generates a beautiful HTML skill tree visualization and opens it in your browser.
17
+
18
+ 3. Present the results to the user — show the archetype card, key strengths, and growth quest.
19
+
20
+ If the user just wants a quick recommendation without the full analysis, use the **`growth_quest`** tool.
21
+
22
+ ## Requirements
23
+
24
+ - `ANTHROPIC_API_KEY` environment variable must be set (for behavioral classification via Claude Haiku)
25
+ - If not set, the tools will return an error with setup instructions
26
+
27
+ ## What It Shows
28
+
29
+ - **Character Card**: Your archetype name, tagline, signature strengths, and vibe
30
+ - **Skill Tree**: 11 behaviors across 4 branches (Planning, Craft, Judgment, Rigor) with animated bars and population baseline markers
31
+ - **Growth Quest**: Your "next unlock" — one specific challenge for your next session
32
+
33
+ ## The 11 Behaviors (from AI Fluency Framework)
34
+
35
+ | Branch | Behavior | Population Avg |
36
+ |--------|----------|---------------|
37
+ | Planning | Clarifies goals upfront | 51% |
38
+ | Planning | Discusses approach first | 10% |
39
+ | Craft | Iterates on outputs | 86% |
40
+ | Craft | Provides examples | 41% |
41
+ | Craft | Specifies format | 30% |
42
+ | Craft | Sets interaction style | 30% |
43
+ | Craft | Expresses tone preferences | 23% |
44
+ | Craft | Defines audience | 18% |
45
+ | Judgment | Flags context gaps | 20% |
46
+ | Judgment | Questions Claude's logic | 16% |
47
+ | Rigor | Verifies facts | 9% |
48
+
49
+ Baselines from [Anthropic's AI Fluency Index](https://www.anthropic.com/research/AI-fluency-index) (Feb 2026, N=9,830 conversations).