skill-tree-ai 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.
@@ -152,20 +152,27 @@ export async function classifySession(client, sessionId, messages) {
152
152
  return classification;
153
153
  }
154
154
  export async function classifySessions(client, sessions, maxNew = 50, onProgress) {
155
- const results = [];
156
- let newClassified = 0;
157
- for (let i = 0; i < sessions.length; i++) {
158
- const session = sessions[i];
155
+ // Split into cached (instant) and uncached (needs API call)
156
+ const cachedResults = [];
157
+ const uncachedSessions = [];
158
+ for (const session of sessions) {
159
159
  const cached = getCachedClassification(session.sessionId);
160
160
  if (cached) {
161
- results.push(cached);
161
+ cachedResults.push(cached);
162
162
  }
163
- else if (newClassified < maxNew) {
164
- const classification = await classifySession(client, session.sessionId, session.messages);
165
- results.push(classification);
166
- newClassified++;
163
+ else {
164
+ uncachedSessions.push(session);
167
165
  }
168
- onProgress?.(i + 1, sessions.length);
169
166
  }
170
- return results;
167
+ // Classify uncached sessions in parallel batches (8 at a time)
168
+ const BATCH_SIZE = 8;
169
+ const uncachedResults = [];
170
+ const toClassify = uncachedSessions.slice(0, maxNew);
171
+ for (let i = 0; i < toClassify.length; i += BATCH_SIZE) {
172
+ const batch = toClassify.slice(i, i + BATCH_SIZE);
173
+ const batchResults = await Promise.all(batch.map((s) => classifySession(client, s.sessionId, s.messages)));
174
+ uncachedResults.push(...batchResults);
175
+ onProgress?.(cachedResults.length + uncachedResults.length, sessions.length);
176
+ }
177
+ return [...cachedResults, ...uncachedResults];
171
178
  }
package/dist/index.js CHANGED
File without changes
package/dist/remote.js CHANGED
@@ -1,70 +1,123 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * Remote MCP server for skill-tree.
4
+ *
5
+ * Deployed to Fly.io. Accepts conversation data from the client (Claude reads
6
+ * local files and sends them here), classifies with Haiku using the server's
7
+ * API key, and returns the profile. The user never needs an API key.
8
+ */
2
9
  import { createServer } from "node:http";
3
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
11
  import { StreamableHTTPServerTransport, } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
12
  import { z } from "zod";
6
13
  import Anthropic from "@anthropic-ai/sdk";
7
- import { findAllSessions } from "./core/extract.js";
8
14
  import { classifySessions } from "./core/classify.js";
9
- import { buildProfile } from "./core/profile.js";
15
+ import { buildProfile, ARCHETYPES } from "./core/profile.js";
10
16
  import { renderHTML } from "./core/render.js";
11
17
  import { MCP_INSTRUCTIONS } from "./shared.js";
12
18
  import { randomUUID } from "node:crypto";
13
19
  const PORT = parseInt(process.env.PORT || "3000", 10);
14
- // Per-session MCP servers
15
- const sessions = new Map();
16
- function createSessionServer() {
20
+ // Map from transport session ID → { server, transport }
21
+ const activeSessions = new Map();
22
+ function createMcpSession() {
17
23
  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 }) => {
24
+ server.tool("analyze", "Analyze conversation data and return a skill profile with archetype. " +
25
+ "The client (Claude) should read the user's local JSONL session files, " +
26
+ "extract user messages, and pass them here as sessions_json.", {
27
+ sessions_json: z.string().describe('JSON array of sessions: [{"id":"...","messages":["msg1","msg2",...]}]'),
28
+ }, async ({ sessions_json }) => {
25
29
  const apiKey = process.env.ANTHROPIC_API_KEY;
26
30
  if (!apiKey) {
27
- return {
28
- content: [{ type: "text", text: "Error: ANTHROPIC_API_KEY not configured on server." }],
29
- };
31
+ return { content: [{ type: "text", text: "Error: Server API key not configured." }] };
30
32
  }
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
- };
33
+ let parsed;
34
+ try {
35
+ parsed = JSON.parse(sessions_json);
36
+ }
37
+ catch {
38
+ return { content: [{ type: "text", text: "Error: Invalid JSON." }] };
39
+ }
40
+ if (!Array.isArray(parsed) || parsed.length === 0) {
41
+ return { content: [{ type: "text", text: "No sessions provided." }] };
39
42
  }
40
- const classifications = await classifySessions(client, sessions.map((s) => ({ sessionId: s.sessionId, messages: s.messages })), 20);
43
+ const client = new Anthropic({ apiKey });
44
+ const sessionsForClassifier = parsed.map((s) => ({
45
+ sessionId: s.id || randomUUID().slice(0, 8),
46
+ messages: (s.messages || []).map((text) => ({
47
+ text: String(text).slice(0, 2000),
48
+ rawLength: String(text).length,
49
+ cleanedLength: Math.min(String(text).length, 2000),
50
+ })),
51
+ }));
52
+ const classifications = await classifySessions(client, sessionsForClassifier, 50);
41
53
  const profile = buildProfile(classifications);
42
- return {
43
- content: [
44
- { type: "text", text: JSON.stringify(profile, null, 2) },
45
- ],
46
- };
54
+ return { content: [{ type: "text", text: JSON.stringify(profile) }] };
55
+ });
56
+ server.tool("visualize", "Generate HTML skill tree visualization from profile JSON.", { profile_json: z.string().describe("Profile JSON from analyze") }, async ({ profile_json }) => {
57
+ const html = renderHTML(JSON.parse(profile_json));
58
+ return { content: [{ type: "text", text: html }] };
59
+ });
60
+ server.tool("archetypes", "List all 7 archetypes.", {}, async () => {
61
+ const list = Object.entries(ARCHETYPES)
62
+ .map(([, a]) => `**${a.name}** — ${a.tagline}`)
63
+ .join("\n\n");
64
+ return { content: [{ type: "text", text: list }] };
47
65
  });
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
- };
66
+ const transport = new StreamableHTTPServerTransport({
67
+ sessionIdGenerator: () => randomUUID(),
57
68
  });
58
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
69
+ transport.onclose = () => {
70
+ const sid = transport.sessionId;
71
+ if (sid)
72
+ activeSessions.delete(sid);
73
+ };
59
74
  server.connect(transport);
60
75
  return { server, transport };
61
76
  }
77
+ async function handleMcp(req, res) {
78
+ const sessionId = req.headers["mcp-session-id"];
79
+ if (sessionId && activeSessions.has(sessionId)) {
80
+ // Existing session
81
+ const { transport } = activeSessions.get(sessionId);
82
+ await transport.handleRequest(req, res, await readBody(req));
83
+ }
84
+ else if (!sessionId) {
85
+ // New session — create server + transport, let transport assign session ID
86
+ const session = createMcpSession();
87
+ // Handle the request — transport will set mcp-session-id header in response
88
+ await session.transport.handleRequest(req, res, await readBody(req));
89
+ // After handling, get the session ID the transport assigned
90
+ const newSid = session.transport.sessionId;
91
+ if (newSid) {
92
+ activeSessions.set(newSid, session);
93
+ }
94
+ }
95
+ else {
96
+ // Session ID provided but not found — stale session
97
+ res.writeHead(400, { "Content-Type": "application/json" });
98
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Session expired" }, id: null }));
99
+ }
100
+ }
101
+ function readBody(req) {
102
+ return new Promise((resolve) => {
103
+ let body = "";
104
+ req.on("data", (chunk) => (body += chunk));
105
+ req.on("end", () => {
106
+ try {
107
+ resolve(JSON.parse(body));
108
+ }
109
+ catch {
110
+ resolve(body);
111
+ }
112
+ });
113
+ });
114
+ }
62
115
  const httpServer = createServer(async (req, res) => {
63
116
  const url = new URL(req.url || "/", `http://localhost:${PORT}`);
64
- // CORS
65
117
  res.setHeader("Access-Control-Allow-Origin", "*");
66
118
  res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
67
119
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
120
+ res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
68
121
  if (req.method === "OPTIONS") {
69
122
  res.writeHead(204);
70
123
  res.end();
@@ -72,42 +125,16 @@ const httpServer = createServer(async (req, res) => {
72
125
  }
73
126
  if (url.pathname === "/health") {
74
127
  res.writeHead(200, { "Content-Type": "application/json" });
75
- res.end(JSON.stringify({ status: "ok", sessions: sessions.size }));
128
+ res.end(JSON.stringify({ status: "ok", sessions: activeSessions.size }));
76
129
  return;
77
130
  }
78
131
  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));
132
+ await handleMcp(req, res);
92
133
  return;
93
134
  }
94
135
  res.writeHead(404);
95
136
  res.end("Not found");
96
137
  });
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
138
  httpServer.listen(PORT, () => {
112
139
  console.error(`Skill Tree MCP server running on http://localhost:${PORT}/mcp`);
113
140
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-tree-ai",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
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
6
  "bin": {