claude-session-skill 1.1.6 → 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.
- package/README.md +10 -1
- package/SKILL.md +12 -0
- package/dist/mcp-server.js +527 -1261
- package/dist/session.js +49 -1
- package/lib/__tests__/format.test.ts +33 -0
- package/lib/__tests__/mcp-server.test.ts +63 -0
- package/lib/__tests__/mcp-tools.test.ts +989 -0
- package/lib/__tests__/search.test.ts +27 -14
- package/lib/create-server.ts +311 -0
- package/lib/format.ts +31 -1
- package/lib/search.ts +2 -2
- package/mcp-server.ts +2 -250
- package/package.json +1 -1
- package/session.ts +27 -0
|
@@ -47,40 +47,44 @@ describe("searchSessions", () => {
|
|
|
47
47
|
expect(results[0].id).toBe("a");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
test("
|
|
50
|
+
test("name match is included even without topic match", () => {
|
|
51
51
|
const sessions = [
|
|
52
52
|
makeSession({ id: "a", name: "", topic: "deploy pipeline" }),
|
|
53
53
|
makeSession({ id: "b", name: "Deploy Fix", topic: "unrelated" }),
|
|
54
54
|
];
|
|
55
55
|
const results = searchSessions(sessions, "deploy");
|
|
56
|
-
expect(results
|
|
56
|
+
expect(results.map((r) => r.id)).toContain("a");
|
|
57
|
+
expect(results.map((r) => r.id)).toContain("b");
|
|
57
58
|
});
|
|
58
59
|
|
|
59
|
-
test("
|
|
60
|
+
test("topic match is included", () => {
|
|
60
61
|
const sessions = [
|
|
61
62
|
makeSession({ id: "a", topic: "unrelated", allMessages: "deploy fix" }),
|
|
62
63
|
makeSession({ id: "b", topic: "deploy pipeline", allMessages: "other stuff" }),
|
|
63
64
|
];
|
|
64
65
|
const results = searchSessions(sessions, "deploy");
|
|
65
|
-
expect(results
|
|
66
|
+
expect(results.map((r) => r.id)).toContain("a");
|
|
67
|
+
expect(results.map((r) => r.id)).toContain("b");
|
|
66
68
|
});
|
|
67
69
|
|
|
68
|
-
test("
|
|
70
|
+
test("firstMessage and lastMessage matches are both included", () => {
|
|
69
71
|
const sessions = [
|
|
70
72
|
makeSession({ id: "a", firstMessage: "unrelated", lastMessage: "fixed the deploy" }),
|
|
71
73
|
makeSession({ id: "b", firstMessage: "fix the deploy", lastMessage: "unrelated" }),
|
|
72
74
|
];
|
|
73
75
|
const results = searchSessions(sessions, "deploy");
|
|
74
|
-
expect(results
|
|
76
|
+
expect(results.map((r) => r.id)).toContain("a");
|
|
77
|
+
expect(results.map((r) => r.id)).toContain("b");
|
|
75
78
|
});
|
|
76
79
|
|
|
77
|
-
test("
|
|
80
|
+
test("quoted phrase filters to exact matches only", () => {
|
|
78
81
|
const sessions = [
|
|
79
82
|
makeSession({ id: "a", topic: "deploy fix" }),
|
|
80
83
|
makeSession({ id: "b", topic: "fix deploy issue" }),
|
|
81
84
|
];
|
|
82
85
|
const results = searchSessions(sessions, '"deploy fix"');
|
|
83
|
-
expect(results
|
|
86
|
+
expect(results.map((r) => r.id)).toContain("a");
|
|
87
|
+
expect(results.map((r) => r.id)).not.toContain("b");
|
|
84
88
|
});
|
|
85
89
|
|
|
86
90
|
test("matches on project path", () => {
|
|
@@ -103,7 +107,7 @@ describe("searchSessions", () => {
|
|
|
103
107
|
expect(results[0].id).toBe("a");
|
|
104
108
|
});
|
|
105
109
|
|
|
106
|
-
test("
|
|
110
|
+
test("results are ordered most recent first", () => {
|
|
107
111
|
const now = Date.now();
|
|
108
112
|
const sessions = [
|
|
109
113
|
makeSession({ id: "old", topic: "deploy", lastTimestamp: now - 86400000 * 30 }),
|
|
@@ -111,19 +115,27 @@ describe("searchSessions", () => {
|
|
|
111
115
|
];
|
|
112
116
|
const results = searchSessions(sessions, "deploy");
|
|
113
117
|
expect(results[0].id).toBe("recent");
|
|
118
|
+
expect(results[1].id).toBe("old");
|
|
114
119
|
});
|
|
115
120
|
|
|
116
|
-
test("handles multiple search tokens", () => {
|
|
121
|
+
test("handles multiple search tokens — all matching sessions included", () => {
|
|
122
|
+
const now = Date.now();
|
|
117
123
|
const sessions = [
|
|
118
|
-
makeSession({ id: "a", topic: "fix login bug in auth" }),
|
|
119
|
-
makeSession({ id: "b", topic: "fix deploy pipeline" }),
|
|
120
|
-
makeSession({ id: "c", topic: "login page redesign" }),
|
|
124
|
+
makeSession({ id: "a", topic: "fix login bug in auth", lastTimestamp: now - 1000 }),
|
|
125
|
+
makeSession({ id: "b", topic: "fix deploy pipeline", lastTimestamp: now - 2000 }),
|
|
126
|
+
makeSession({ id: "c", topic: "login page redesign", lastTimestamp: now - 3000 }),
|
|
121
127
|
];
|
|
122
128
|
const results = searchSessions(sessions, "fix login");
|
|
129
|
+
// a and c both match ("fix login" shares tokens); b matches "fix"
|
|
130
|
+
const ids = results.map((r) => r.id);
|
|
131
|
+
expect(ids).toContain("a");
|
|
132
|
+
expect(ids).toContain("b");
|
|
133
|
+
expect(ids).toContain("c");
|
|
134
|
+
// All ordered most recent first
|
|
123
135
|
expect(results[0].id).toBe("a");
|
|
124
136
|
});
|
|
125
137
|
|
|
126
|
-
test("returns results sorted by
|
|
138
|
+
test("returns results sorted by most recent first regardless of score", () => {
|
|
127
139
|
const now = Date.now();
|
|
128
140
|
const sessions = [
|
|
129
141
|
makeSession({ id: "a", topic: "deploy", lastTimestamp: now - 86400000 * 10 }),
|
|
@@ -131,5 +143,6 @@ describe("searchSessions", () => {
|
|
|
131
143
|
];
|
|
132
144
|
const results = searchSessions(sessions, "deploy");
|
|
133
145
|
expect(results[0].id).toBe("b");
|
|
146
|
+
expect(results[1].id).toBe("a");
|
|
134
147
|
});
|
|
135
148
|
});
|
|
@@ -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/lib/search.ts
CHANGED
|
@@ -74,8 +74,8 @@ export function searchSessions(
|
|
|
74
74
|
scored.push({ session, score });
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// Sort by
|
|
78
|
-
scored.sort((a, b) => b.
|
|
77
|
+
// Sort by most recent first (timestamp desc). Score only determines inclusion, not order.
|
|
78
|
+
scored.sort((a, b) => b.session.lastTimestamp - a.session.lastTimestamp);
|
|
79
79
|
|
|
80
80
|
return scored.map((s) => s.session);
|
|
81
81
|
}
|