@contextstream/mcp-server 0.4.48 → 0.4.50

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 CHANGED
@@ -157,6 +157,33 @@ claude mcp update contextstream -e CONTEXTSTREAM_API_KEY=your_key
157
157
 
158
158
  </details>
159
159
 
160
+ <details>
161
+ <summary><b>GitHub Copilot CLI</b></summary>
162
+
163
+ Use the Copilot CLI to interactively add the MCP server:
164
+
165
+ ```bash
166
+ /mcp add
167
+ ```
168
+
169
+ Or add to `~/.copilot/mcp-config.json`:
170
+
171
+ ```json
172
+ {
173
+ "mcpServers": {
174
+ "contextstream": {
175
+ "command": "npx",
176
+ "args": ["-y", "@contextstream/mcp-server"],
177
+ "env": { "CONTEXTSTREAM_API_KEY": "your_key" }
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ For more information, see the [GitHub Copilot CLI documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli).
184
+
185
+ </details>
186
+
160
187
  ---
161
188
 
162
189
  ## Links
@@ -105,7 +105,7 @@ function buildHooksConfig(options) {
105
105
  ],
106
106
  UserPromptSubmit: userPromptHooks
107
107
  };
108
- if (options?.includePreCompact) {
108
+ if (options?.includePreCompact !== false) {
109
109
  config.PreCompact = [
110
110
  {
111
111
  // Match both manual (/compact) and automatic compaction
@@ -157,7 +157,7 @@ async function installHookScripts(options) {
157
157
  preToolUse: "npx @contextstream/mcp-server hook pre-tool-use",
158
158
  userPrompt: "npx @contextstream/mcp-server hook user-prompt-submit"
159
159
  };
160
- if (options?.includePreCompact) {
160
+ if (options?.includePreCompact !== false) {
161
161
  result.preCompact = "npx @contextstream/mcp-server hook pre-compact";
162
162
  }
163
163
  if (options?.includeMediaAware !== false) {
@@ -203,7 +203,7 @@ async function installClaudeCodeHooks(options) {
203
203
  "npx @contextstream/mcp-server hook pre-tool-use",
204
204
  "npx @contextstream/mcp-server hook user-prompt-submit"
205
205
  );
206
- if (options.includePreCompact) {
206
+ if (options.includePreCompact !== false) {
207
207
  result.scripts.push("npx @contextstream/mcp-server hook pre-compact");
208
208
  }
209
209
  if (options.includeMediaAware !== false) {
@@ -270,12 +270,11 @@ When Media-Aware hook detects media patterns, it injects context about:
270
270
  - How to get clips for Remotion (with frame-based props)
271
271
  - How to index new media files
272
272
 
273
- ### PreCompact Hook (Optional)
273
+ ### PreCompact Hook
274
274
  - **Command:** \`npx @contextstream/mcp-server hook pre-compact\`
275
275
  - **Purpose:** Saves conversation state before context compaction
276
276
  - **Triggers:** Both manual (/compact) and automatic compaction
277
- - **Disable:** Set \`CONTEXTSTREAM_PRECOMPACT_ENABLED=false\` environment variable
278
- - **Note:** Enable with \`generate_rules(include_pre_compact=true)\` to activate
277
+ - **Installed:** By default (disable with \`CONTEXTSTREAM_HOOK_ENABLED=false\`)
279
278
 
280
279
  When PreCompact runs, it:
281
280
  1. Parses the transcript for active files and tool calls
File without changes
@@ -68,6 +68,9 @@ function parseTranscript(transcriptPath) {
68
68
  const activeFiles = /* @__PURE__ */ new Set();
69
69
  const recentMessages = [];
70
70
  const toolCalls = [];
71
+ const messages = [];
72
+ let startedAt = (/* @__PURE__ */ new Date()).toISOString();
73
+ let firstTimestamp = true;
71
74
  try {
72
75
  const content = fs.readFileSync(transcriptPath, "utf-8");
73
76
  const lines = content.split("\n");
@@ -76,6 +79,11 @@ function parseTranscript(transcriptPath) {
76
79
  try {
77
80
  const entry = JSON.parse(line);
78
81
  const msgType = entry.type || "";
82
+ const timestamp = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
83
+ if (firstTimestamp && entry.timestamp) {
84
+ startedAt = entry.timestamp;
85
+ firstTimestamp = false;
86
+ }
79
87
  if (msgType === "tool_use") {
80
88
  const toolName = entry.name || "";
81
89
  const toolInput = entry.input || {};
@@ -91,11 +99,40 @@ function parseTranscript(transcriptPath) {
91
99
  activeFiles.add(`[glob:${pattern}]`);
92
100
  }
93
101
  }
94
- }
95
- if (msgType === "assistant" && entry.content) {
96
- const content2 = entry.content;
97
- if (typeof content2 === "string" && content2.length > 50) {
98
- recentMessages.push(content2.slice(0, 500));
102
+ messages.push({
103
+ role: "assistant",
104
+ content: `[Tool: ${toolName}]`,
105
+ timestamp,
106
+ tool_calls: { name: toolName, input: toolInput }
107
+ });
108
+ } else if (msgType === "tool_result") {
109
+ const resultContent = typeof entry.content === "string" ? entry.content.slice(0, 2e3) : JSON.stringify(entry.content || {}).slice(0, 2e3);
110
+ messages.push({
111
+ role: "tool",
112
+ content: resultContent,
113
+ timestamp,
114
+ tool_results: { name: entry.name }
115
+ });
116
+ } else if (msgType === "user" || entry.role === "user") {
117
+ const userContent = typeof entry.content === "string" ? entry.content : "";
118
+ if (userContent) {
119
+ messages.push({
120
+ role: "user",
121
+ content: userContent,
122
+ timestamp
123
+ });
124
+ }
125
+ } else if (msgType === "assistant" || entry.role === "assistant") {
126
+ const assistantContent = typeof entry.content === "string" ? entry.content : "";
127
+ if (assistantContent) {
128
+ messages.push({
129
+ role: "assistant",
130
+ content: assistantContent,
131
+ timestamp
132
+ });
133
+ if (assistantContent.length > 50) {
134
+ recentMessages.push(assistantContent.slice(0, 500));
135
+ }
99
136
  }
100
137
  }
101
138
  } catch {
@@ -108,11 +145,57 @@ function parseTranscript(transcriptPath) {
108
145
  activeFiles: Array.from(activeFiles).slice(-20),
109
146
  // Last 20 files
110
147
  toolCallCount: toolCalls.length,
111
- messageCount: recentMessages.length,
112
- lastTools: toolCalls.slice(-10).map((t) => t.name)
148
+ messageCount: messages.length,
149
+ lastTools: toolCalls.slice(-10).map((t) => t.name),
113
150
  // Last 10 tool names
151
+ messages,
152
+ startedAt
114
153
  };
115
154
  }
155
+ async function saveFullTranscript(sessionId, transcriptData, trigger) {
156
+ if (!API_KEY) {
157
+ return { success: false, message: "No API key configured" };
158
+ }
159
+ if (transcriptData.messages.length === 0) {
160
+ return { success: false, message: "No messages to save" };
161
+ }
162
+ const payload = {
163
+ session_id: sessionId,
164
+ messages: transcriptData.messages,
165
+ started_at: transcriptData.startedAt,
166
+ source_type: "pre_compact",
167
+ title: `Pre-compaction save (${trigger})`,
168
+ metadata: {
169
+ trigger,
170
+ active_files: transcriptData.activeFiles,
171
+ tool_call_count: transcriptData.toolCallCount
172
+ },
173
+ tags: ["pre_compaction", trigger]
174
+ };
175
+ if (WORKSPACE_ID) {
176
+ payload.workspace_id = WORKSPACE_ID;
177
+ }
178
+ try {
179
+ const controller = new AbortController();
180
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
181
+ const response = await fetch(`${API_URL}/api/v1/transcripts`, {
182
+ method: "POST",
183
+ headers: {
184
+ "Content-Type": "application/json",
185
+ "X-API-Key": API_KEY
186
+ },
187
+ body: JSON.stringify(payload),
188
+ signal: controller.signal
189
+ });
190
+ clearTimeout(timeoutId);
191
+ if (response.ok) {
192
+ return { success: true, message: `Transcript saved (${transcriptData.messages.length} messages)` };
193
+ }
194
+ return { success: false, message: `API error: ${response.status}` };
195
+ } catch (error) {
196
+ return { success: false, message: String(error) };
197
+ }
198
+ }
116
199
  async function saveSnapshot(sessionId, transcriptData, trigger) {
117
200
  if (!API_KEY) {
118
201
  return { success: false, message: "No API key configured" };
@@ -193,13 +276,19 @@ async function runPreCompactHook() {
193
276
  }
194
277
  let autoSaveStatus = "";
195
278
  if (AUTO_SAVE && API_KEY) {
196
- const { success, message } = await saveSnapshot(sessionId, transcriptData, trigger);
197
- if (success) {
279
+ const transcriptResult = await saveFullTranscript(sessionId, transcriptData, trigger);
280
+ if (transcriptResult.success) {
198
281
  autoSaveStatus = `
199
- [ContextStream: Auto-saved snapshot with ${transcriptData.activeFiles.length} active files]`;
282
+ [ContextStream: ${transcriptResult.message}]`;
200
283
  } else {
201
- autoSaveStatus = `
284
+ const { success, message } = await saveSnapshot(sessionId, transcriptData, trigger);
285
+ if (success) {
286
+ autoSaveStatus = `
287
+ [ContextStream: Auto-saved snapshot with ${transcriptData.activeFiles.length} active files (transcript save failed: ${transcriptResult.message})]`;
288
+ } else {
289
+ autoSaveStatus = `
202
290
  [ContextStream: Auto-save failed - ${message}]`;
291
+ }
203
292
  }
204
293
  }
205
294
  const filesList = transcriptData.activeFiles.slice(0, 5).join(", ") || "none detected";
@@ -2,11 +2,16 @@
2
2
 
3
3
  // src/hooks/user-prompt-submit.ts
4
4
  var ENABLED = process.env.CONTEXTSTREAM_REMINDER_ENABLED !== "false";
5
- var REMINDER = `[CONTEXTSTREAM RULES]
6
- 1. BEFORE Glob/Grep/Read/Search: mcp__contextstream__search(mode="hybrid") FIRST
7
- 2. Call context_smart at start of EVERY response
8
- 3. Local tools ONLY if ContextStream returns 0 results
9
- [END RULES]`;
5
+ var REMINDER = `[CONTEXTSTREAM - DO THIS FIRST]
6
+ mcp__contextstream__context(user_message="<user's message>") - CALL THIS BEFORE ANY OTHER TOOL
7
+
8
+ Why: Delivers task-specific rules, lessons from past mistakes, and relevant context.
9
+ Skip it = repeat past mistakes, miss relevant decisions, fly blind.
10
+
11
+ BEFORE Glob/Grep/Read/Search/Explore:
12
+ mcp__contextstream__search(mode="hybrid", query="...") FIRST
13
+ Local tools ONLY if search returns 0 results.
14
+ [END]`;
10
15
  function detectEditorFormat(input) {
11
16
  if (input.hookName !== void 0) {
12
17
  return "cline";