@contextstream/mcp-server 0.4.49 → 0.4.51

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,191 @@
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 API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
9
+ var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
10
+ var WORKSPACE_ID = null;
11
+ function loadConfigFromMcpJson(cwd) {
12
+ let searchDir = path.resolve(cwd);
13
+ for (let i = 0; i < 5; i++) {
14
+ if (!API_KEY) {
15
+ const mcpPath = path.join(searchDir, ".mcp.json");
16
+ if (fs.existsSync(mcpPath)) {
17
+ try {
18
+ const content = fs.readFileSync(mcpPath, "utf-8");
19
+ const config = JSON.parse(content);
20
+ const csEnv = config.mcpServers?.contextstream?.env;
21
+ if (csEnv?.CONTEXTSTREAM_API_KEY) {
22
+ API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
23
+ }
24
+ if (csEnv?.CONTEXTSTREAM_API_URL) {
25
+ API_URL = csEnv.CONTEXTSTREAM_API_URL;
26
+ }
27
+ } catch {
28
+ }
29
+ }
30
+ }
31
+ if (!WORKSPACE_ID) {
32
+ const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
33
+ if (fs.existsSync(csConfigPath)) {
34
+ try {
35
+ const content = fs.readFileSync(csConfigPath, "utf-8");
36
+ const csConfig = JSON.parse(content);
37
+ if (csConfig.workspace_id) {
38
+ WORKSPACE_ID = csConfig.workspace_id;
39
+ }
40
+ } catch {
41
+ }
42
+ }
43
+ }
44
+ const parentDir = path.dirname(searchDir);
45
+ if (parentDir === searchDir) break;
46
+ searchDir = parentDir;
47
+ }
48
+ if (!API_KEY) {
49
+ const homeMcpPath = path.join(homedir(), ".mcp.json");
50
+ if (fs.existsSync(homeMcpPath)) {
51
+ try {
52
+ const content = fs.readFileSync(homeMcpPath, "utf-8");
53
+ const config = JSON.parse(content);
54
+ const csEnv = config.mcpServers?.contextstream?.env;
55
+ if (csEnv?.CONTEXTSTREAM_API_KEY) {
56
+ API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
57
+ }
58
+ if (csEnv?.CONTEXTSTREAM_API_URL) {
59
+ API_URL = csEnv.CONTEXTSTREAM_API_URL;
60
+ }
61
+ } catch {
62
+ }
63
+ }
64
+ }
65
+ }
66
+ function parseTranscriptStats(transcriptPath) {
67
+ const stats = {
68
+ messageCount: 0,
69
+ toolCallCount: 0,
70
+ duration: 0,
71
+ filesModified: []
72
+ };
73
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) {
74
+ return stats;
75
+ }
76
+ try {
77
+ const content = fs.readFileSync(transcriptPath, "utf-8");
78
+ const lines = content.split("\n");
79
+ let firstTimestamp = null;
80
+ let lastTimestamp = null;
81
+ const modifiedFiles = /* @__PURE__ */ new Set();
82
+ for (const line of lines) {
83
+ if (!line.trim()) continue;
84
+ try {
85
+ const entry = JSON.parse(line);
86
+ if (entry.type === "user" || entry.type === "assistant") {
87
+ stats.messageCount++;
88
+ } else if (entry.type === "tool_use") {
89
+ stats.toolCallCount++;
90
+ if (["Write", "Edit", "NotebookEdit"].includes(entry.name || "")) {
91
+ const filePath = entry.input?.file_path;
92
+ if (filePath) {
93
+ modifiedFiles.add(filePath);
94
+ }
95
+ }
96
+ }
97
+ if (entry.timestamp) {
98
+ const ts = new Date(entry.timestamp);
99
+ if (!firstTimestamp || ts < firstTimestamp) {
100
+ firstTimestamp = ts;
101
+ }
102
+ if (!lastTimestamp || ts > lastTimestamp) {
103
+ lastTimestamp = ts;
104
+ }
105
+ }
106
+ } catch {
107
+ continue;
108
+ }
109
+ }
110
+ if (firstTimestamp && lastTimestamp) {
111
+ stats.duration = Math.round((lastTimestamp.getTime() - firstTimestamp.getTime()) / 1e3);
112
+ }
113
+ stats.filesModified = Array.from(modifiedFiles);
114
+ } catch {
115
+ }
116
+ return stats;
117
+ }
118
+ async function finalizeSession(sessionId, stats, reason) {
119
+ if (!API_KEY) return;
120
+ const payload = {
121
+ event_type: "session_end",
122
+ title: `Session Ended: ${reason}`,
123
+ content: JSON.stringify({
124
+ session_id: sessionId,
125
+ reason,
126
+ stats: {
127
+ messages: stats.messageCount,
128
+ tool_calls: stats.toolCallCount,
129
+ duration_seconds: stats.duration,
130
+ files_modified: stats.filesModified.length
131
+ },
132
+ files_modified: stats.filesModified.slice(0, 20),
133
+ ended_at: (/* @__PURE__ */ new Date()).toISOString()
134
+ }),
135
+ importance: "low",
136
+ tags: ["session", "end", reason],
137
+ source_type: "hook",
138
+ session_id: sessionId
139
+ };
140
+ if (WORKSPACE_ID) {
141
+ payload.workspace_id = WORKSPACE_ID;
142
+ }
143
+ try {
144
+ const controller = new AbortController();
145
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
146
+ await fetch(`${API_URL}/api/v1/memory/events`, {
147
+ method: "POST",
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ "X-API-Key": API_KEY
151
+ },
152
+ body: JSON.stringify(payload),
153
+ signal: controller.signal
154
+ });
155
+ clearTimeout(timeoutId);
156
+ } catch {
157
+ }
158
+ }
159
+ async function runSessionEndHook() {
160
+ if (!ENABLED) {
161
+ process.exit(0);
162
+ }
163
+ let inputData = "";
164
+ for await (const chunk of process.stdin) {
165
+ inputData += chunk;
166
+ }
167
+ if (!inputData.trim()) {
168
+ process.exit(0);
169
+ }
170
+ let input;
171
+ try {
172
+ input = JSON.parse(inputData);
173
+ } catch {
174
+ process.exit(0);
175
+ }
176
+ const cwd = input.cwd || process.cwd();
177
+ loadConfigFromMcpJson(cwd);
178
+ const sessionId = input.session_id || "unknown";
179
+ const transcriptPath = input.transcript_path || "";
180
+ const reason = input.reason || "user_exit";
181
+ const stats = parseTranscriptStats(transcriptPath);
182
+ await finalizeSession(sessionId, stats, reason);
183
+ process.exit(0);
184
+ }
185
+ var isDirectRun = process.argv[1]?.includes("session-end") || process.argv[2] === "session-end";
186
+ if (isDirectRun) {
187
+ runSessionEndHook().catch(() => process.exit(0));
188
+ }
189
+ export {
190
+ runSessionEndHook
191
+ };
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/session-init.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_INIT_ENABLED !== "false";
8
+ var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
9
+ var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
10
+ var WORKSPACE_ID = null;
11
+ var PROJECT_ID = null;
12
+ function loadConfigFromMcpJson(cwd) {
13
+ let searchDir = path.resolve(cwd);
14
+ for (let i = 0; i < 5; i++) {
15
+ if (!API_KEY) {
16
+ const mcpPath = path.join(searchDir, ".mcp.json");
17
+ if (fs.existsSync(mcpPath)) {
18
+ try {
19
+ const content = fs.readFileSync(mcpPath, "utf-8");
20
+ const config = JSON.parse(content);
21
+ const csEnv = config.mcpServers?.contextstream?.env;
22
+ if (csEnv?.CONTEXTSTREAM_API_KEY) {
23
+ API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
24
+ }
25
+ if (csEnv?.CONTEXTSTREAM_API_URL) {
26
+ API_URL = csEnv.CONTEXTSTREAM_API_URL;
27
+ }
28
+ if (csEnv?.CONTEXTSTREAM_WORKSPACE_ID) {
29
+ WORKSPACE_ID = csEnv.CONTEXTSTREAM_WORKSPACE_ID;
30
+ }
31
+ } catch {
32
+ }
33
+ }
34
+ }
35
+ if (!WORKSPACE_ID || !PROJECT_ID) {
36
+ const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
37
+ if (fs.existsSync(csConfigPath)) {
38
+ try {
39
+ const content = fs.readFileSync(csConfigPath, "utf-8");
40
+ const csConfig = JSON.parse(content);
41
+ if (csConfig.workspace_id && !WORKSPACE_ID) {
42
+ WORKSPACE_ID = csConfig.workspace_id;
43
+ }
44
+ if (csConfig.project_id && !PROJECT_ID) {
45
+ PROJECT_ID = csConfig.project_id;
46
+ }
47
+ } catch {
48
+ }
49
+ }
50
+ }
51
+ const parentDir = path.dirname(searchDir);
52
+ if (parentDir === searchDir) break;
53
+ searchDir = parentDir;
54
+ }
55
+ if (!API_KEY) {
56
+ const homeMcpPath = path.join(homedir(), ".mcp.json");
57
+ if (fs.existsSync(homeMcpPath)) {
58
+ try {
59
+ const content = fs.readFileSync(homeMcpPath, "utf-8");
60
+ const config = JSON.parse(content);
61
+ const csEnv = config.mcpServers?.contextstream?.env;
62
+ if (csEnv?.CONTEXTSTREAM_API_KEY) {
63
+ API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
64
+ }
65
+ if (csEnv?.CONTEXTSTREAM_API_URL) {
66
+ API_URL = csEnv.CONTEXTSTREAM_API_URL;
67
+ }
68
+ } catch {
69
+ }
70
+ }
71
+ }
72
+ }
73
+ async function fetchSessionContext() {
74
+ if (!API_KEY) return null;
75
+ try {
76
+ const controller = new AbortController();
77
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
78
+ const url = new URL(`${API_URL}/api/v1/context`);
79
+ if (WORKSPACE_ID) url.searchParams.set("workspace_id", WORKSPACE_ID);
80
+ if (PROJECT_ID) url.searchParams.set("project_id", PROJECT_ID);
81
+ url.searchParams.set("include_rules", "true");
82
+ url.searchParams.set("include_lessons", "true");
83
+ url.searchParams.set("include_decisions", "true");
84
+ url.searchParams.set("include_plans", "true");
85
+ url.searchParams.set("limit", "5");
86
+ const response = await fetch(url.toString(), {
87
+ method: "GET",
88
+ headers: {
89
+ "X-API-Key": API_KEY
90
+ },
91
+ signal: controller.signal
92
+ });
93
+ clearTimeout(timeoutId);
94
+ if (response.ok) {
95
+ return await response.json();
96
+ }
97
+ return null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ function formatContext(ctx) {
103
+ if (!ctx) {
104
+ return `[ContextStream Session Start]
105
+
106
+ No stored context found. Call \`mcp__contextstream__context(user_message="starting new session")\` to initialize.`;
107
+ }
108
+ const parts = ["[ContextStream Session Start]"];
109
+ if (ctx.lessons && ctx.lessons.length > 0) {
110
+ parts.push("\n## \u26A0\uFE0F Lessons from Past Mistakes");
111
+ for (const lesson of ctx.lessons.slice(0, 3)) {
112
+ parts.push(`- **${lesson.title}**: ${lesson.prevention}`);
113
+ }
114
+ }
115
+ if (ctx.active_plans && ctx.active_plans.length > 0) {
116
+ parts.push("\n## \u{1F4CB} Active Plans");
117
+ for (const plan of ctx.active_plans.slice(0, 3)) {
118
+ parts.push(`- ${plan.title} (${plan.status})`);
119
+ }
120
+ }
121
+ if (ctx.pending_tasks && ctx.pending_tasks.length > 0) {
122
+ parts.push("\n## \u2705 Pending Tasks");
123
+ for (const task of ctx.pending_tasks.slice(0, 5)) {
124
+ parts.push(`- ${task.title}`);
125
+ }
126
+ }
127
+ if (ctx.recent_decisions && ctx.recent_decisions.length > 0) {
128
+ parts.push("\n## \u{1F4DD} Recent Decisions");
129
+ for (const decision of ctx.recent_decisions.slice(0, 3)) {
130
+ parts.push(`- **${decision.title}**`);
131
+ }
132
+ }
133
+ parts.push("\n---");
134
+ parts.push('Call `mcp__contextstream__context(user_message="...")` for task-specific context.');
135
+ return parts.join("\n");
136
+ }
137
+ async function runSessionInitHook() {
138
+ if (!ENABLED) {
139
+ process.exit(0);
140
+ }
141
+ let inputData = "";
142
+ for await (const chunk of process.stdin) {
143
+ inputData += chunk;
144
+ }
145
+ if (!inputData.trim()) {
146
+ process.exit(0);
147
+ }
148
+ let input;
149
+ try {
150
+ input = JSON.parse(inputData);
151
+ } catch {
152
+ process.exit(0);
153
+ }
154
+ const cwd = input.cwd || process.cwd();
155
+ loadConfigFromMcpJson(cwd);
156
+ const context = await fetchSessionContext();
157
+ const formattedContext = formatContext(context);
158
+ console.log(
159
+ JSON.stringify({
160
+ hookSpecificOutput: {
161
+ hookEventName: "SessionStart",
162
+ additionalContext: formattedContext
163
+ }
164
+ })
165
+ );
166
+ process.exit(0);
167
+ }
168
+ var isDirectRun = process.argv[1]?.includes("session-init") || process.argv[2] === "session-init";
169
+ if (isDirectRun) {
170
+ runSessionInitHook().catch(() => process.exit(0));
171
+ }
172
+ export {
173
+ runSessionInitHook
174
+ };