claude-session-skill 1.1.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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/SKILL.md +65 -0
- package/dist/mcp-server.js +15483 -0
- package/dist/session.js +983 -0
- package/lib/__tests__/fixtures/history-malformed.jsonl +4 -0
- package/lib/__tests__/fixtures/history.jsonl +3 -0
- package/lib/__tests__/fixtures/names.json +1 -0
- package/lib/__tests__/fixtures/session-file.jsonl +3 -0
- package/lib/__tests__/fixtures/summaries.json +1 -0
- package/lib/__tests__/format.test.ts +223 -0
- package/lib/__tests__/indexer-io.test.ts +644 -0
- package/lib/__tests__/indexer.test.ts +176 -0
- package/lib/__tests__/search.test.ts +135 -0
- package/lib/format.ts +177 -0
- package/lib/indexer.ts +732 -0
- package/lib/search.ts +81 -0
- package/mcp-server.ts +259 -0
- package/package.json +64 -0
- package/session.ts +171 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"sessionId": "valid001validid1", "display": "Valid entry one", "project": "/Users/tim/app", "timestamp": 1709000000000}
|
|
2
|
+
not valid json at all
|
|
3
|
+
{"sessionId": "valid002validid2", "display": "Another valid entry here", "project": "/Users/tim/app", "timestamp": 1709001000000}
|
|
4
|
+
{"broken": true
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"sessionId": "abc123def456abc1", "display": "Fix the login bug", "project": "/Users/tim/projects/app", "timestamp": 1709000000000}
|
|
2
|
+
{"sessionId": "abc123def456abc1", "display": "Added unit tests for auth module", "project": "/Users/tim/projects/app", "timestamp": 1709001000000}
|
|
3
|
+
{"sessionId": "xyz789abc012xyz7", "display": "Setup database migrations", "project": "/Users/tim/projects/db", "timestamp": 1708900000000}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"abc123def456abc1": "Login Fix Session"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"type": "user", "cwd": "/Users/tim/projects/app", "gitBranch": "feat/login", "message": {"role": "user", "content": "Fix the login bug please"}, "timestamp": "2024-03-01T12:00:00Z"}
|
|
2
|
+
{"type": "assistant", "message": {"role": "assistant", "content": [{"type": "text", "text": "I will fix the login bug by updating the auth handler to properly validate tokens."}]}, "timestamp": "2024-03-01T12:00:05Z"}
|
|
3
|
+
{"type": "user", "cwd": "/Users/tim/projects/app", "gitBranch": "feat/login", "message": {"role": "user", "content": "Can you also add unit tests for this?"}, "timestamp": "2024-03-01T12:01:00Z"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"abc123def456abc1": "- Fixed authentication bug in login flow\n- Added unit tests for auth module\n- Updated token validation logic", "garbage001garbag1": "I don't have access to the transcript"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
formatSessionList,
|
|
4
|
+
formatSearchResults,
|
|
5
|
+
formatSessionDetail,
|
|
6
|
+
formatStats,
|
|
7
|
+
} from "../format";
|
|
8
|
+
import type { SessionEntry } from "../indexer";
|
|
9
|
+
|
|
10
|
+
function makeSession(overrides: Partial<SessionEntry> = {}): SessionEntry {
|
|
11
|
+
return {
|
|
12
|
+
id: "abcdef1234567890",
|
|
13
|
+
name: "",
|
|
14
|
+
project: "my-project",
|
|
15
|
+
projectDir: "",
|
|
16
|
+
topic: "- Fixed authentication bug in login flow",
|
|
17
|
+
firstMessage: "Fix the auth bug",
|
|
18
|
+
lastMessage: "Done, all tests pass",
|
|
19
|
+
allMessages: "Fix the auth bug",
|
|
20
|
+
messageCount: 15,
|
|
21
|
+
firstTimestamp: 1709000000000,
|
|
22
|
+
lastTimestamp: 1709001000000,
|
|
23
|
+
cwd: "/Users/tim/projects/my-project",
|
|
24
|
+
gitBranch: "main",
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("formatSessionList", () => {
|
|
30
|
+
test("shows count header", () => {
|
|
31
|
+
const sessions = [makeSession()];
|
|
32
|
+
const output = formatSessionList(sessions, true);
|
|
33
|
+
expect(output).toContain("1 session(s)");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("truncates to 20 by default", () => {
|
|
37
|
+
const sessions = Array.from({ length: 25 }, (_, i) =>
|
|
38
|
+
makeSession({ id: `session-${i.toString().padStart(16, "0")}` })
|
|
39
|
+
);
|
|
40
|
+
const output = formatSessionList(sessions, false);
|
|
41
|
+
expect(output).toContain("25 sessions");
|
|
42
|
+
expect(output).toContain("--all");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("shows all when requested", () => {
|
|
46
|
+
const sessions = Array.from({ length: 25 }, (_, i) =>
|
|
47
|
+
makeSession({ id: `session-${i.toString().padStart(16, "0")}` })
|
|
48
|
+
);
|
|
49
|
+
const output = formatSessionList(sessions, true);
|
|
50
|
+
expect(output).toContain("25 session(s)");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("shows full session ID", () => {
|
|
54
|
+
const output = formatSessionList([makeSession()], true);
|
|
55
|
+
expect(output).toContain("abcdef1234567890");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("shows message count", () => {
|
|
59
|
+
const output = formatSessionList([makeSession()], true);
|
|
60
|
+
expect(output).toContain("15 msgs");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("shows name with summary when named", () => {
|
|
64
|
+
const output = formatSessionList([makeSession({ name: "Auth Fix" })], true);
|
|
65
|
+
expect(output).toContain("Auth Fix");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("formatSearchResults", () => {
|
|
70
|
+
test("shows no results message", () => {
|
|
71
|
+
const output = formatSearchResults([], "deploy");
|
|
72
|
+
expect(output).toContain('No sessions found matching "deploy"');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("shows result count", () => {
|
|
76
|
+
const sessions = [makeSession()];
|
|
77
|
+
const output = formatSearchResults(sessions, "auth");
|
|
78
|
+
expect(output).toContain('1 session(s) matching "auth"');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("shows top 15 max", () => {
|
|
82
|
+
const sessions = Array.from({ length: 20 }, () => makeSession());
|
|
83
|
+
const output = formatSearchResults(sessions, "test");
|
|
84
|
+
expect(output).toContain("showing 15");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("shows git branch when present", () => {
|
|
88
|
+
const output = formatSearchResults([makeSession({ gitBranch: "feat/login" })], "auth");
|
|
89
|
+
expect(output).toContain("feat/login");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("formatSessionDetail", () => {
|
|
94
|
+
test("shows full session ID", () => {
|
|
95
|
+
const output = formatSessionDetail(makeSession());
|
|
96
|
+
expect(output).toContain("abcdef1234567890");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("shows project", () => {
|
|
100
|
+
const output = formatSessionDetail(makeSession());
|
|
101
|
+
expect(output).toContain("my-project");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("shows cwd", () => {
|
|
105
|
+
const output = formatSessionDetail(makeSession());
|
|
106
|
+
expect(output).toContain("/Users/tim/projects/my-project");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("shows git branch", () => {
|
|
110
|
+
const output = formatSessionDetail(makeSession());
|
|
111
|
+
expect(output).toContain("main");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("shows message count", () => {
|
|
115
|
+
const output = formatSessionDetail(makeSession());
|
|
116
|
+
expect(output).toContain("15");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("shows name when present", () => {
|
|
120
|
+
const output = formatSessionDetail(makeSession({ name: "My Session" }));
|
|
121
|
+
expect(output).toContain("My Session");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("omits name line when empty", () => {
|
|
125
|
+
const output = formatSessionDetail(makeSession({ name: "" }));
|
|
126
|
+
expect(output).not.toContain("Name:");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("omits branch when empty", () => {
|
|
130
|
+
const output = formatSessionDetail(makeSession({ gitBranch: "" }));
|
|
131
|
+
expect(output).not.toContain("Branch:");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("formatStats", () => {
|
|
136
|
+
test("shows session and project count", () => {
|
|
137
|
+
const sessions = [
|
|
138
|
+
makeSession({ project: "app-a" }),
|
|
139
|
+
makeSession({ project: "app-a" }),
|
|
140
|
+
makeSession({ project: "app-b" }),
|
|
141
|
+
];
|
|
142
|
+
const output = formatStats(sessions);
|
|
143
|
+
expect(output).toContain("3 sessions across 2 projects");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("sorts by session count descending", () => {
|
|
147
|
+
const sessions = [
|
|
148
|
+
makeSession({ project: "small" }),
|
|
149
|
+
makeSession({ project: "big" }),
|
|
150
|
+
makeSession({ project: "big" }),
|
|
151
|
+
makeSession({ project: "big" }),
|
|
152
|
+
];
|
|
153
|
+
const output = formatStats(sessions);
|
|
154
|
+
const bigIdx = output.indexOf("big");
|
|
155
|
+
const smallIdx = output.indexOf("small");
|
|
156
|
+
expect(bigIdx).toBeLessThan(smallIdx);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── formatDate edge cases (via formatSessionList) ─────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe("formatDate edge cases (via formatSessionList)", () => {
|
|
163
|
+
test("ts=0 renders as 'unknown'", () => {
|
|
164
|
+
const session = makeSession({ lastTimestamp: 0, firstTimestamp: 0 });
|
|
165
|
+
const output = formatSessionList([session], true);
|
|
166
|
+
expect(output).toContain("unknown");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("ts=undefined-like (NaN) renders as 'unknown'", () => {
|
|
170
|
+
const session = makeSession({ lastTimestamp: NaN, firstTimestamp: NaN });
|
|
171
|
+
const output = formatSessionList([session], true);
|
|
172
|
+
expect(output).toContain("unknown");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("non-zero timestamp renders a real date", () => {
|
|
176
|
+
const session = makeSession({ lastTimestamp: 1709000000000 });
|
|
177
|
+
const output = formatSessionList([session], true);
|
|
178
|
+
// Should contain a month name, not "unknown"
|
|
179
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
180
|
+
expect(months.some((m) => output.includes(m))).toBe(true);
|
|
181
|
+
expect(output).not.toContain("unknown");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── truncate edge cases (via formatSessionList label) ─────────────────────────
|
|
186
|
+
|
|
187
|
+
describe("truncate edge cases (via formatSessionList)", () => {
|
|
188
|
+
test("emoji string truncated by code points, not bytes", () => {
|
|
189
|
+
// 4-byte emoji × 35 = 140 bytes but 35 code points → fits in max=100
|
|
190
|
+
const emojiStr = "🔥".repeat(35); // 35 chars (code points), 140 UTF-16 units
|
|
191
|
+
const session = makeSession({ topic: `- ${emojiStr}`, lastMessage: "" });
|
|
192
|
+
const output = formatSessionList([session], true);
|
|
193
|
+
// Should not crash and should contain emoji
|
|
194
|
+
expect(output).toContain("🔥");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("long string beyond 100 chars gets truncated with ellipsis", () => {
|
|
198
|
+
const longLabel = "a".repeat(120);
|
|
199
|
+
const session = makeSession({ topic: `- ${longLabel}`, lastMessage: "" });
|
|
200
|
+
const output = formatSessionList([session], true);
|
|
201
|
+
expect(output).toContain("...");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("CJK characters counted by code points", () => {
|
|
205
|
+
// CJK chars are BMP (single UTF-16 code unit) but Array.from still counts them correctly
|
|
206
|
+
// "- " (2 chars) + 101 CJK chars = 103 code points > 100 limit → truncated
|
|
207
|
+
const cjkStr = "中".repeat(101);
|
|
208
|
+
const session = makeSession({ topic: `- ${cjkStr}`, lastMessage: "" });
|
|
209
|
+
const output = formatSessionList([session], true);
|
|
210
|
+
expect(output).toContain("...");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("string exactly at max length (100) is not truncated", () => {
|
|
214
|
+
const exactStr = "a".repeat(98); // 98 chars + "- " prefix = 100 char label
|
|
215
|
+
const session = makeSession({ topic: `- ${exactStr}`, lastMessage: "" });
|
|
216
|
+
const output = formatSessionList([session], true);
|
|
217
|
+
// Should NOT add ellipsis since it's exactly at the limit
|
|
218
|
+
// The label is "- " + 98 chars = 100 chars, which should not be truncated
|
|
219
|
+
const lines = output.split("\n").filter((l) => l.startsWith(" "));
|
|
220
|
+
const labelLine = lines[0];
|
|
221
|
+
expect(labelLine).not.toContain("...");
|
|
222
|
+
});
|
|
223
|
+
});
|