@contextstream/mcp-server 0.4.81 → 1.0.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.
@@ -1,285 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/hooks/session-end.ts
4
- import * as fs from "node:fs";
5
- import * as path from "node:path";
6
- import { homedir } from "node:os";
7
- var ENABLED = process.env.CONTEXTSTREAM_SESSION_END_ENABLED !== "false";
8
- var SAVE_TRANSCRIPT = process.env.CONTEXTSTREAM_SESSION_END_SAVE_TRANSCRIPT !== "false";
9
- var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
10
- var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
11
- var WORKSPACE_ID = null;
12
- var PROJECT_ID = null;
13
- function loadConfigFromMcpJson(cwd) {
14
- let searchDir = path.resolve(cwd);
15
- for (let i = 0; i < 5; i++) {
16
- if (!API_KEY) {
17
- const mcpPath = path.join(searchDir, ".mcp.json");
18
- if (fs.existsSync(mcpPath)) {
19
- try {
20
- const content = fs.readFileSync(mcpPath, "utf-8");
21
- const config = JSON.parse(content);
22
- const csEnv = config.mcpServers?.contextstream?.env;
23
- if (csEnv?.CONTEXTSTREAM_API_KEY) {
24
- API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
25
- }
26
- if (csEnv?.CONTEXTSTREAM_API_URL) {
27
- API_URL = csEnv.CONTEXTSTREAM_API_URL;
28
- }
29
- } catch {
30
- }
31
- }
32
- }
33
- if (!WORKSPACE_ID || !PROJECT_ID) {
34
- const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
35
- if (fs.existsSync(csConfigPath)) {
36
- try {
37
- const content = fs.readFileSync(csConfigPath, "utf-8");
38
- const csConfig = JSON.parse(content);
39
- if (csConfig.workspace_id && !WORKSPACE_ID) {
40
- WORKSPACE_ID = csConfig.workspace_id;
41
- }
42
- if (csConfig.project_id && !PROJECT_ID) {
43
- PROJECT_ID = csConfig.project_id;
44
- }
45
- } catch {
46
- }
47
- }
48
- }
49
- const parentDir = path.dirname(searchDir);
50
- if (parentDir === searchDir) break;
51
- searchDir = parentDir;
52
- }
53
- if (!API_KEY) {
54
- const homeMcpPath = path.join(homedir(), ".mcp.json");
55
- if (fs.existsSync(homeMcpPath)) {
56
- try {
57
- const content = fs.readFileSync(homeMcpPath, "utf-8");
58
- const config = JSON.parse(content);
59
- const csEnv = config.mcpServers?.contextstream?.env;
60
- if (csEnv?.CONTEXTSTREAM_API_KEY) {
61
- API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
62
- }
63
- if (csEnv?.CONTEXTSTREAM_API_URL) {
64
- API_URL = csEnv.CONTEXTSTREAM_API_URL;
65
- }
66
- } catch {
67
- }
68
- }
69
- }
70
- }
71
- function parseTranscriptStats(transcriptPath) {
72
- const stats = {
73
- messageCount: 0,
74
- toolCallCount: 0,
75
- duration: 0,
76
- filesModified: [],
77
- messages: [],
78
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
79
- };
80
- if (!transcriptPath || !fs.existsSync(transcriptPath)) {
81
- return stats;
82
- }
83
- try {
84
- const content = fs.readFileSync(transcriptPath, "utf-8");
85
- const lines = content.split("\n");
86
- let firstTimestamp = null;
87
- let lastTimestamp = null;
88
- const modifiedFiles = /* @__PURE__ */ new Set();
89
- for (const line of lines) {
90
- if (!line.trim()) continue;
91
- try {
92
- const entry = JSON.parse(line);
93
- const msgType = entry.type || "";
94
- const timestamp = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
95
- if (entry.timestamp) {
96
- const ts = new Date(entry.timestamp);
97
- if (!firstTimestamp || ts < firstTimestamp) {
98
- firstTimestamp = ts;
99
- stats.startedAt = entry.timestamp;
100
- }
101
- if (!lastTimestamp || ts > lastTimestamp) {
102
- lastTimestamp = ts;
103
- }
104
- }
105
- if (msgType === "user" || entry.role === "user") {
106
- stats.messageCount++;
107
- const userContent = typeof entry.content === "string" ? entry.content : "";
108
- if (userContent) {
109
- stats.messages.push({
110
- role: "user",
111
- content: userContent,
112
- timestamp
113
- });
114
- }
115
- } else if (msgType === "assistant" || entry.role === "assistant") {
116
- stats.messageCount++;
117
- const assistantContent = typeof entry.content === "string" ? entry.content : "";
118
- if (assistantContent) {
119
- stats.messages.push({
120
- role: "assistant",
121
- content: assistantContent,
122
- timestamp
123
- });
124
- }
125
- } else if (msgType === "tool_use") {
126
- stats.toolCallCount++;
127
- const toolName = entry.name || "";
128
- const toolInput = entry.input || {};
129
- if (["Write", "Edit", "NotebookEdit"].includes(toolName)) {
130
- const filePath = toolInput.file_path || toolInput.notebook_path;
131
- if (filePath) {
132
- modifiedFiles.add(filePath);
133
- }
134
- }
135
- stats.messages.push({
136
- role: "assistant",
137
- content: `[Tool: ${toolName}]`,
138
- timestamp,
139
- tool_calls: { name: toolName, input: toolInput }
140
- });
141
- } else if (msgType === "tool_result") {
142
- const resultContent = typeof entry.content === "string" ? entry.content.slice(0, 2e3) : JSON.stringify(entry.content || {}).slice(0, 2e3);
143
- stats.messages.push({
144
- role: "tool",
145
- content: resultContent,
146
- timestamp,
147
- tool_results: { name: entry.name }
148
- });
149
- }
150
- } catch {
151
- continue;
152
- }
153
- }
154
- if (firstTimestamp && lastTimestamp) {
155
- stats.duration = Math.round((lastTimestamp.getTime() - firstTimestamp.getTime()) / 1e3);
156
- }
157
- stats.filesModified = Array.from(modifiedFiles);
158
- } catch {
159
- }
160
- return stats;
161
- }
162
- async function saveFullTranscript(sessionId, stats, reason) {
163
- if (!API_KEY) {
164
- return { success: false, message: "No API key configured" };
165
- }
166
- if (stats.messages.length === 0) {
167
- return { success: false, message: "No messages to save" };
168
- }
169
- const payload = {
170
- session_id: sessionId,
171
- messages: stats.messages,
172
- started_at: stats.startedAt,
173
- source_type: "session_end",
174
- title: `Session transcript (${reason})`,
175
- metadata: {
176
- reason,
177
- tool_call_count: stats.toolCallCount,
178
- files_modified: stats.filesModified.slice(0, 20),
179
- duration_seconds: stats.duration
180
- },
181
- tags: ["session_end", reason]
182
- };
183
- if (WORKSPACE_ID) {
184
- payload.workspace_id = WORKSPACE_ID;
185
- }
186
- if (PROJECT_ID) {
187
- payload.project_id = PROJECT_ID;
188
- }
189
- try {
190
- const controller = new AbortController();
191
- const timeoutId = setTimeout(() => controller.abort(), 15e3);
192
- const response = await fetch(`${API_URL}/api/v1/transcripts`, {
193
- method: "POST",
194
- headers: {
195
- "Content-Type": "application/json",
196
- "X-API-Key": API_KEY
197
- },
198
- body: JSON.stringify(payload),
199
- signal: controller.signal
200
- });
201
- clearTimeout(timeoutId);
202
- if (response.ok) {
203
- return { success: true, message: `Transcript saved (${stats.messages.length} messages)` };
204
- }
205
- return { success: false, message: `API error: ${response.status}` };
206
- } catch (error) {
207
- return { success: false, message: String(error) };
208
- }
209
- }
210
- async function finalizeSession(sessionId, stats, reason) {
211
- if (!API_KEY) return;
212
- if (SAVE_TRANSCRIPT && stats.messages.length > 0) {
213
- await saveFullTranscript(sessionId, stats, reason);
214
- }
215
- const payload = {
216
- event_type: "manual_note",
217
- title: `Session Ended: ${reason}`,
218
- content: JSON.stringify({
219
- session_id: sessionId,
220
- reason,
221
- stats: {
222
- messages: stats.messageCount,
223
- tool_calls: stats.toolCallCount,
224
- duration_seconds: stats.duration,
225
- files_modified: stats.filesModified.length
226
- },
227
- files_modified: stats.filesModified.slice(0, 20),
228
- ended_at: (/* @__PURE__ */ new Date()).toISOString()
229
- }),
230
- importance: "low",
231
- tags: ["session", "end", reason],
232
- source_type: "hook"
233
- };
234
- if (WORKSPACE_ID) {
235
- payload.workspace_id = WORKSPACE_ID;
236
- }
237
- try {
238
- const controller = new AbortController();
239
- const timeoutId = setTimeout(() => controller.abort(), 5e3);
240
- await fetch(`${API_URL}/api/v1/memory/events`, {
241
- method: "POST",
242
- headers: {
243
- "Content-Type": "application/json",
244
- "X-API-Key": API_KEY
245
- },
246
- body: JSON.stringify(payload),
247
- signal: controller.signal
248
- });
249
- clearTimeout(timeoutId);
250
- } catch {
251
- }
252
- }
253
- async function runSessionEndHook() {
254
- if (!ENABLED) {
255
- process.exit(0);
256
- }
257
- let inputData = "";
258
- for await (const chunk of process.stdin) {
259
- inputData += chunk;
260
- }
261
- if (!inputData.trim()) {
262
- process.exit(0);
263
- }
264
- let input;
265
- try {
266
- input = JSON.parse(inputData);
267
- } catch {
268
- process.exit(0);
269
- }
270
- const cwd = input.cwd || process.cwd();
271
- loadConfigFromMcpJson(cwd);
272
- const sessionId = input.session_id || "unknown";
273
- const transcriptPath = input.transcript_path || "";
274
- const reason = input.reason || "user_exit";
275
- const stats = parseTranscriptStats(transcriptPath);
276
- await finalizeSession(sessionId, stats, reason);
277
- process.exit(0);
278
- }
279
- var isDirectRun = process.argv[1]?.includes("session-end") || process.argv[2] === "session-end";
280
- if (isDirectRun) {
281
- runSessionEndHook().catch(() => process.exit(0));
282
- }
283
- export {
284
- runSessionEndHook
285
- };