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.
- package/dist/core/classify.js +18 -11
- package/dist/index.js +0 -0
- package/dist/remote.js +95 -68
- package/package.json +1 -1
package/dist/core/classify.js
CHANGED
|
@@ -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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
+
cachedResults.push(cached);
|
|
162
162
|
}
|
|
163
|
-
else
|
|
164
|
-
|
|
165
|
-
results.push(classification);
|
|
166
|
-
newClassified++;
|
|
163
|
+
else {
|
|
164
|
+
uncachedSessions.push(session);
|
|
167
165
|
}
|
|
168
|
-
onProgress?.(i + 1, sessions.length);
|
|
169
166
|
}
|
|
170
|
-
|
|
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
|
-
//
|
|
15
|
-
const
|
|
16
|
-
function
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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": {
|