claude-session-skill 1.1.7 → 1.1.8

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,311 @@
1
+ import { readFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+
10
+ import { buildIndex, nameSession, clearSessionName, resolveSession } from "./indexer";
11
+ import { searchSessions } from "./search";
12
+ import {
13
+ formatSessionList,
14
+ formatSearchResults,
15
+ formatSessionDetail,
16
+ formatStats,
17
+ makeAutoSessionName,
18
+ } from "./format";
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
23
+
24
+ export function createServer(): Server {
25
+ const server = new Server(
26
+ { name: "claude-session", version: pkg.version },
27
+ { capabilities: { tools: {} } }
28
+ );
29
+
30
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
31
+ tools: [
32
+ {
33
+ name: "list_sessions",
34
+ description: "List recent Claude Code sessions with AI-generated summaries, sorted by last activity.",
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ limit: {
39
+ type: "number",
40
+ description: "Max sessions to return. Defaults to all sessions.",
41
+ },
42
+ },
43
+ },
44
+ },
45
+ {
46
+ name: "search_sessions",
47
+ description: "Search Claude Code sessions by keyword, project path, topic, or quoted phrase.",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ query: {
52
+ type: "string",
53
+ description: "Search query. Supports quoted phrases for exact matches, e.g. \"auth bug\".",
54
+ },
55
+ },
56
+ required: ["query"],
57
+ },
58
+ },
59
+ {
60
+ name: "show_session",
61
+ description: "Show detailed information about a specific session: project, branch, timestamps, message count, and AI summary.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ id: {
66
+ type: "string",
67
+ description: "Full or partial session ID (first 8+ characters are sufficient).",
68
+ },
69
+ },
70
+ required: ["id"],
71
+ },
72
+ },
73
+ {
74
+ name: "name_session",
75
+ description: "Give a session a memorable name for easy recall. Defaults to the most recent session if no ID is provided.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ id: {
80
+ type: "string",
81
+ description: "Session ID or prefix. If omitted, names the most recent session.",
82
+ },
83
+ name: {
84
+ type: "string",
85
+ description: "Name to assign (max 50 characters).",
86
+ },
87
+ },
88
+ required: ["name"],
89
+ },
90
+ },
91
+ {
92
+ name: "autoname_session",
93
+ description: "Generate a session title from its summary and prefix it with the session start time as dd/mm/yy HH:MM.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ id: {
98
+ type: "string",
99
+ description: "Session ID or prefix. If omitted, names the most recent session.",
100
+ },
101
+ },
102
+ },
103
+ },
104
+ {
105
+ name: "unname_session",
106
+ description: "Remove the name from a session. Defaults to the most recent session if no ID is provided.",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ id: {
111
+ type: "string",
112
+ description: "Session ID or prefix. If omitted, clears the name of the most recent session.",
113
+ },
114
+ },
115
+ required: ["id"],
116
+ },
117
+ },
118
+ {
119
+ name: "session_stats",
120
+ description: "Show session statistics broken down by project: session count, message count, and last activity.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {},
124
+ },
125
+ },
126
+ ],
127
+ }));
128
+
129
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
130
+ const { name, arguments: args } = request.params;
131
+
132
+ try {
133
+ switch (name) {
134
+ case "list_sessions": {
135
+ const sessions = await buildIndex();
136
+ const limit = typeof args?.limit === "number" ? args.limit : undefined;
137
+ const list = limit ? sessions.slice(0, limit) : sessions;
138
+ return {
139
+ content: [{ type: "text", text: formatSessionList(list, true) }],
140
+ };
141
+ }
142
+
143
+ case "search_sessions": {
144
+ const query = String(args?.query ?? "").trim();
145
+ if (!query) {
146
+ return {
147
+ content: [{ type: "text", text: "Error: query is required." }],
148
+ isError: true,
149
+ };
150
+ }
151
+ const sessions = await buildIndex();
152
+ const results = searchSessions(sessions, query);
153
+ return {
154
+ content: [{ type: "text", text: formatSearchResults(results, query) }],
155
+ };
156
+ }
157
+
158
+ case "show_session": {
159
+ const id = String(args?.id ?? "").trim();
160
+ if (!id) {
161
+ return {
162
+ content: [{ type: "text", text: "Error: id is required." }],
163
+ isError: true,
164
+ };
165
+ }
166
+ const sessions = await buildIndex();
167
+ const resolved = resolveSession(sessions, id);
168
+ if (!resolved.ok) {
169
+ return {
170
+ content: [{ type: "text", text: `Error: ${resolved.error}` }],
171
+ isError: true,
172
+ };
173
+ }
174
+ return {
175
+ content: [{ type: "text", text: formatSessionDetail(resolved.match) }],
176
+ };
177
+ }
178
+
179
+ case "name_session": {
180
+ const sessionName = String(args?.name ?? "").trim();
181
+ if (!sessionName) {
182
+ return {
183
+ content: [{ type: "text", text: "Error: name is required." }],
184
+ isError: true,
185
+ };
186
+ }
187
+
188
+ let sessionId: string;
189
+ if (args?.id) {
190
+ sessionId = String(args.id).trim();
191
+ } else {
192
+ // Default to most recent session
193
+ const sessions = await buildIndex();
194
+ if (sessions.length === 0) {
195
+ return {
196
+ content: [{ type: "text", text: "Error: No sessions found." }],
197
+ isError: true,
198
+ };
199
+ }
200
+ sessionId = sessions[0].id;
201
+ }
202
+
203
+ const result = await nameSession(sessionId, sessionName);
204
+ if (!result.ok) {
205
+ return {
206
+ content: [{ type: "text", text: `Error: ${result.error}` }],
207
+ isError: true,
208
+ };
209
+ }
210
+ return {
211
+ content: [
212
+ {
213
+ type: "text",
214
+ text: `Named session ${(result.fullId ?? "").slice(0, 8)}... → "${sessionName}"`,
215
+ },
216
+ ],
217
+ };
218
+ }
219
+
220
+ case "autoname_session": {
221
+ const sessions = await buildIndex();
222
+ if (sessions.length === 0) {
223
+ return {
224
+ content: [{ type: "text", text: "Error: No sessions found." }],
225
+ isError: true,
226
+ };
227
+ }
228
+
229
+ const sessionId = args?.id ? String(args.id).trim() : sessions[0].id;
230
+ const resolved = resolveSession(sessions, sessionId);
231
+ if (!resolved.ok) {
232
+ return {
233
+ content: [{ type: "text", text: `Error: ${resolved.error}` }],
234
+ isError: true,
235
+ };
236
+ }
237
+
238
+ const generatedName = makeAutoSessionName(resolved.match);
239
+ const result = await nameSession(resolved.match.id, generatedName);
240
+ if (!result.ok) {
241
+ return {
242
+ content: [{ type: "text", text: `Error: ${result.error}` }],
243
+ isError: true,
244
+ };
245
+ }
246
+ return {
247
+ content: [
248
+ {
249
+ type: "text",
250
+ text: `Named session ${(result.fullId ?? "").slice(0, 8)}... → "${generatedName}"`,
251
+ },
252
+ ],
253
+ };
254
+ }
255
+
256
+ case "unname_session": {
257
+ let sessionId: string;
258
+ if (args?.id) {
259
+ sessionId = String(args.id).trim();
260
+ } else {
261
+ const sessions = await buildIndex();
262
+ if (sessions.length === 0) {
263
+ return {
264
+ content: [{ type: "text", text: "Error: No sessions found." }],
265
+ isError: true,
266
+ };
267
+ }
268
+ sessionId = sessions[0].id;
269
+ }
270
+
271
+ const result = await clearSessionName(sessionId);
272
+ if (!result.ok) {
273
+ return {
274
+ content: [{ type: "text", text: `Error: ${result.error}` }],
275
+ isError: true,
276
+ };
277
+ }
278
+ return {
279
+ content: [
280
+ {
281
+ type: "text",
282
+ text: `Cleared name from session ${(result.fullId ?? "").slice(0, 8)}...`,
283
+ },
284
+ ],
285
+ };
286
+ }
287
+
288
+ case "session_stats": {
289
+ const sessions = await buildIndex();
290
+ return {
291
+ content: [{ type: "text", text: formatStats(sessions) }],
292
+ };
293
+ }
294
+
295
+ default:
296
+ return {
297
+ content: [{ type: "text", text: `Error: Unknown tool "${name}"` }],
298
+ isError: true,
299
+ };
300
+ }
301
+ } catch (err) {
302
+ const message = err instanceof Error ? err.message : String(err);
303
+ return {
304
+ content: [{ type: "text", text: `Error: ${message}` }],
305
+ isError: true,
306
+ };
307
+ }
308
+ });
309
+
310
+ return server;
311
+ }
package/lib/format.ts CHANGED
@@ -10,6 +10,10 @@ function truncate(s: string, max: number): string {
10
10
  return chars.slice(0, max - 3).join("") + "...";
11
11
  }
12
12
 
13
+ function normalizeWhitespace(value: string): string {
14
+ return value.replace(/\s+/g, " ").trim();
15
+ }
16
+
13
17
  function formatDate(ts: number): string {
14
18
  if (!ts) return "unknown";
15
19
  const d = new Date(ts);
@@ -23,8 +27,19 @@ function formatDate(ts: number): string {
23
27
  return `${month} ${day}, ${h}:${mins} ${ampm}`;
24
28
  }
25
29
 
30
+ function formatNameTimestamp(ts: number): string {
31
+ if (!ts) return "unknown";
32
+ const d = new Date(ts);
33
+ const day = d.getDate().toString().padStart(2, "0");
34
+ const month = (d.getMonth() + 1).toString().padStart(2, "0");
35
+ const year = (d.getFullYear() % 100).toString().padStart(2, "0");
36
+ const hours = d.getHours().toString().padStart(2, "0");
37
+ const minutes = d.getMinutes().toString().padStart(2, "0");
38
+ return `${day}/${month}/${year} ${hours}:${minutes}`;
39
+ }
40
+
26
41
  // Get single-line display for list view — first bullet or last message
27
- function displayLine(s: SessionEntry): string {
42
+ export function displayLine(s: SessionEntry): string {
28
43
  if (s.topic && !isGarbageSummary(s.topic)) {
29
44
  // If topic has bullets, return just the first one
30
45
  const lines = s.topic.split("\n").filter((l: string) => l.trim().length > 0);
@@ -37,6 +52,21 @@ function displayLine(s: SessionEntry): string {
37
52
  return s.firstMessage || "(no messages)";
38
53
  }
39
54
 
55
+ export function makeAutoSessionName(s: SessionEntry, maxLength = 50): string {
56
+ const prefix = formatNameTimestamp(s.firstTimestamp);
57
+ const summary = displayLine(s);
58
+ const cleanSummary = normalizeWhitespace(
59
+ summary.startsWith("- ") ? summary.slice(2) : summary
60
+ ).replace(/^["']+|["']+$/g, "");
61
+
62
+ if (!cleanSummary) return prefix;
63
+
64
+ const available = maxLength - prefix.length - 1;
65
+ if (available <= 0) return prefix.slice(0, maxLength);
66
+
67
+ return `${prefix} ${truncate(cleanSummary, available)}`;
68
+ }
69
+
40
70
  // Combine name + summary for list/search views
41
71
  function makeLabel(s: SessionEntry, summary: string): string {
42
72
  if (!s.name) return summary;
package/mcp-server.ts CHANGED
@@ -1,259 +1,11 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- } from "@modelcontextprotocol/sdk/types.js";
9
-
10
- import { buildIndex, nameSession, clearSessionName, resolveSession } from "./lib/indexer";
11
- import { searchSessions } from "./lib/search";
12
- import {
13
- formatSessionList,
14
- formatSearchResults,
15
- formatSessionDetail,
16
- formatStats,
17
- } from "./lib/format";
4
+ import { createServer } from "./lib/create-server";
18
5
 
19
6
  // NEVER use console.log() — corrupts JSON-RPC stdio stream.
20
7
  // Only process.stderr (debug) is safe.
21
8
 
22
- const server = new Server(
23
- { name: "claude-session", version: "1.1.0" },
24
- { capabilities: { tools: {} } }
25
- );
26
-
27
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
28
- tools: [
29
- {
30
- name: "list_sessions",
31
- description: "List recent Claude Code sessions with AI-generated summaries, sorted by last activity.",
32
- inputSchema: {
33
- type: "object",
34
- properties: {
35
- limit: {
36
- type: "number",
37
- description: "Max sessions to return. Defaults to all sessions.",
38
- },
39
- },
40
- },
41
- },
42
- {
43
- name: "search_sessions",
44
- description: "Search Claude Code sessions by keyword, project path, topic, or quoted phrase.",
45
- inputSchema: {
46
- type: "object",
47
- properties: {
48
- query: {
49
- type: "string",
50
- description: "Search query. Supports quoted phrases for exact matches, e.g. \"auth bug\".",
51
- },
52
- },
53
- required: ["query"],
54
- },
55
- },
56
- {
57
- name: "show_session",
58
- description: "Show detailed information about a specific session: project, branch, timestamps, message count, and AI summary.",
59
- inputSchema: {
60
- type: "object",
61
- properties: {
62
- id: {
63
- type: "string",
64
- description: "Full or partial session ID (first 8+ characters are sufficient).",
65
- },
66
- },
67
- required: ["id"],
68
- },
69
- },
70
- {
71
- name: "name_session",
72
- description: "Give a session a memorable name for easy recall. Defaults to the most recent session if no ID is provided.",
73
- inputSchema: {
74
- type: "object",
75
- properties: {
76
- id: {
77
- type: "string",
78
- description: "Session ID or prefix. If omitted, names the most recent session.",
79
- },
80
- name: {
81
- type: "string",
82
- description: "Name to assign (max 50 characters).",
83
- },
84
- },
85
- required: ["name"],
86
- },
87
- },
88
- {
89
- name: "unname_session",
90
- description: "Remove the name from a session. Defaults to the most recent session if no ID is provided.",
91
- inputSchema: {
92
- type: "object",
93
- properties: {
94
- id: {
95
- type: "string",
96
- description: "Session ID or prefix. If omitted, clears the name of the most recent session.",
97
- },
98
- },
99
- required: ["id"],
100
- },
101
- },
102
- {
103
- name: "session_stats",
104
- description: "Show session statistics broken down by project: session count, message count, and last activity.",
105
- inputSchema: {
106
- type: "object",
107
- properties: {},
108
- },
109
- },
110
- ],
111
- }));
112
-
113
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
114
- const { name, arguments: args } = request.params;
115
-
116
- try {
117
- switch (name) {
118
- case "list_sessions": {
119
- const sessions = await buildIndex();
120
- const limit = typeof args?.limit === "number" ? args.limit : undefined;
121
- const list = limit ? sessions.slice(0, limit) : sessions;
122
- return {
123
- content: [{ type: "text", text: formatSessionList(list, true) }],
124
- };
125
- }
126
-
127
- case "search_sessions": {
128
- const query = String(args?.query ?? "").trim();
129
- if (!query) {
130
- return {
131
- content: [{ type: "text", text: "Error: query is required." }],
132
- isError: true,
133
- };
134
- }
135
- const sessions = await buildIndex();
136
- const results = searchSessions(sessions, query);
137
- return {
138
- content: [{ type: "text", text: formatSearchResults(results, query) }],
139
- };
140
- }
141
-
142
- case "show_session": {
143
- const id = String(args?.id ?? "").trim();
144
- if (!id) {
145
- return {
146
- content: [{ type: "text", text: "Error: id is required." }],
147
- isError: true,
148
- };
149
- }
150
- const sessions = await buildIndex();
151
- const resolved = resolveSession(sessions, id);
152
- if (!resolved.ok) {
153
- return {
154
- content: [{ type: "text", text: `Error: ${resolved.error}` }],
155
- isError: true,
156
- };
157
- }
158
- return {
159
- content: [{ type: "text", text: formatSessionDetail(resolved.match) }],
160
- };
161
- }
162
-
163
- case "name_session": {
164
- const sessionName = String(args?.name ?? "").trim();
165
- if (!sessionName) {
166
- return {
167
- content: [{ type: "text", text: "Error: name is required." }],
168
- isError: true,
169
- };
170
- }
171
-
172
- let sessionId: string;
173
- if (args?.id) {
174
- sessionId = String(args.id).trim();
175
- } else {
176
- // Default to most recent session
177
- const sessions = await buildIndex();
178
- if (sessions.length === 0) {
179
- return {
180
- content: [{ type: "text", text: "Error: No sessions found." }],
181
- isError: true,
182
- };
183
- }
184
- sessionId = sessions[0].id;
185
- }
186
-
187
- const result = await nameSession(sessionId, sessionName);
188
- if (!result.ok) {
189
- return {
190
- content: [{ type: "text", text: `Error: ${result.error}` }],
191
- isError: true,
192
- };
193
- }
194
- return {
195
- content: [
196
- {
197
- type: "text",
198
- text: `Named session ${(result.fullId ?? "").slice(0, 8)}... → "${sessionName}"`,
199
- },
200
- ],
201
- };
202
- }
203
-
204
- case "unname_session": {
205
- let sessionId: string;
206
- if (args?.id) {
207
- sessionId = String(args.id).trim();
208
- } else {
209
- const sessions = await buildIndex();
210
- if (sessions.length === 0) {
211
- return {
212
- content: [{ type: "text", text: "Error: No sessions found." }],
213
- isError: true,
214
- };
215
- }
216
- sessionId = sessions[0].id;
217
- }
218
-
219
- const result = await clearSessionName(sessionId);
220
- if (!result.ok) {
221
- return {
222
- content: [{ type: "text", text: `Error: ${result.error}` }],
223
- isError: true,
224
- };
225
- }
226
- return {
227
- content: [
228
- {
229
- type: "text",
230
- text: `Cleared name from session ${(result.fullId ?? "").slice(0, 8)}...`,
231
- },
232
- ],
233
- };
234
- }
235
-
236
- case "session_stats": {
237
- const sessions = await buildIndex();
238
- return {
239
- content: [{ type: "text", text: formatStats(sessions) }],
240
- };
241
- }
242
-
243
- default:
244
- return {
245
- content: [{ type: "text", text: `Error: Unknown tool "${name}"` }],
246
- isError: true,
247
- };
248
- }
249
- } catch (err) {
250
- const message = err instanceof Error ? err.message : String(err);
251
- return {
252
- content: [{ type: "text", text: `Error: ${message}` }],
253
- isError: true,
254
- };
255
- }
256
- });
257
-
9
+ const server = createServer();
258
10
  const transport = new StdioServerTransport();
259
11
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-skill",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Search, browse, and resume past Claude Code sessions with AI-generated summaries — plus an MCP server for any MCP-compatible client",
5
5
  "author": "ITeachYouAI <engineering@iteachyouai.com>",
6
6
  "license": "MIT",
package/session.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  formatSearchResults,
8
8
  formatSessionDetail,
9
9
  formatStats,
10
+ makeAutoSessionName,
10
11
  } from "./lib/format";
11
12
 
12
13
  const args = process.argv.slice(2);
@@ -88,6 +89,31 @@ async function main() {
88
89
  break;
89
90
  }
90
91
 
92
+ case "autoname": {
93
+ const rest = args.slice(1);
94
+ const sessions = await buildIndex();
95
+ if (sessions.length === 0) {
96
+ console.error("No sessions found.");
97
+ process.exit(1);
98
+ }
99
+
100
+ const sessionId = rest[0] ?? sessions[0].id;
101
+ const resolved = resolveSession(sessions, sessionId);
102
+ if (!resolved.ok) {
103
+ console.error(resolved.error);
104
+ process.exit(1);
105
+ }
106
+
107
+ const generatedName = makeAutoSessionName(resolved.match);
108
+ const result = await nameSession(resolved.match.id, generatedName);
109
+ if (!result.ok) {
110
+ console.error(result.error);
111
+ process.exit(1);
112
+ }
113
+ console.log(`Named session ${(result.fullId ?? "").slice(0, 8)}... → "${generatedName}"`);
114
+ break;
115
+ }
116
+
91
117
  case "unname":
92
118
  case "clear-name": {
93
119
  const rest = args.slice(1);
@@ -155,6 +181,7 @@ async function main() {
155
181
  session show <id> Show session details (partial ID ok)
156
182
  session name <name> Name the most recent session
157
183
  session name <id> <name> Name a specific session (partial ID ok)
184
+ session autoname [<id>] Name a session from its summary + start time
158
185
  session unname [<id>] Clear a session's name
159
186
  session search <query> Search sessions by keyword
160
187
  session rebuild Force rebuild the index