@contextstream/mcp-server 0.4.62 → 0.4.64

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,210 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/post-tool-use-failure.ts
4
+ import * as fs2 from "node:fs";
5
+ import * as path2 from "node:path";
6
+ import { homedir as homedir2 } from "node:os";
7
+
8
+ // src/hooks/common.ts
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { homedir } from "node:os";
12
+ var DEFAULT_API_URL = "https://api.contextstream.io";
13
+ function readHookInput() {
14
+ try {
15
+ return JSON.parse(fs.readFileSync(0, "utf8"));
16
+ } catch {
17
+ return {};
18
+ }
19
+ }
20
+ function writeHookOutput(output) {
21
+ const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
22
+ hookSpecificOutput: output.additionalContext ? {
23
+ hookEventName: output.hookEventName,
24
+ additionalContext: output.additionalContext
25
+ } : void 0,
26
+ additionalContext: output.additionalContext,
27
+ blocked: output.blocked,
28
+ reason: output.reason
29
+ } : {};
30
+ console.log(JSON.stringify(payload));
31
+ }
32
+ function extractCwd(input) {
33
+ const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
34
+ return cwd;
35
+ }
36
+ function loadHookConfig(cwd) {
37
+ let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
38
+ let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
39
+ let jwt = process.env.CONTEXTSTREAM_JWT || "";
40
+ let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
41
+ let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
42
+ let searchDir = path.resolve(cwd);
43
+ for (let i = 0; i < 6; i++) {
44
+ if (!apiKey && !jwt) {
45
+ const mcpPath = path.join(searchDir, ".mcp.json");
46
+ if (fs.existsSync(mcpPath)) {
47
+ try {
48
+ const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
49
+ const env = config.mcpServers?.contextstream?.env;
50
+ if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
51
+ if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
52
+ if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
53
+ if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
54
+ if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
55
+ } catch {
56
+ }
57
+ }
58
+ }
59
+ if (!workspaceId || !projectId) {
60
+ const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
61
+ if (fs.existsSync(localConfigPath)) {
62
+ try {
63
+ const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
64
+ if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
65
+ if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
66
+ } catch {
67
+ }
68
+ }
69
+ }
70
+ const parentDir = path.dirname(searchDir);
71
+ if (parentDir === searchDir) break;
72
+ searchDir = parentDir;
73
+ }
74
+ if (!apiKey && !jwt) {
75
+ const homeMcpPath = path.join(homedir(), ".mcp.json");
76
+ if (fs.existsSync(homeMcpPath)) {
77
+ try {
78
+ const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
79
+ const env = config.mcpServers?.contextstream?.env;
80
+ if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
81
+ if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
82
+ if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
83
+ } catch {
84
+ }
85
+ }
86
+ }
87
+ return { apiUrl, apiKey, jwt, workspaceId, projectId };
88
+ }
89
+ function isConfigured(config) {
90
+ return Boolean(config.apiKey || config.jwt);
91
+ }
92
+ function authHeaders(config) {
93
+ if (config.apiKey) {
94
+ return { "X-API-Key": config.apiKey };
95
+ }
96
+ if (config.jwt) {
97
+ return { Authorization: `Bearer ${config.jwt}` };
98
+ }
99
+ return {};
100
+ }
101
+ async function apiRequest(config, apiPath, init = {}) {
102
+ const response = await fetch(`${config.apiUrl}${apiPath}`, {
103
+ method: init.method || "GET",
104
+ headers: {
105
+ "Content-Type": "application/json",
106
+ ...authHeaders(config)
107
+ },
108
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
109
+ });
110
+ if (!response.ok) {
111
+ throw new Error(`${response.status} ${response.statusText}`);
112
+ }
113
+ const json = await response.json();
114
+ if (json && typeof json === "object" && "data" in json) {
115
+ return json.data;
116
+ }
117
+ return json;
118
+ }
119
+ async function postMemoryEvent(config, title, content, tags, eventType = "operation") {
120
+ if (!isConfigured(config) || !config.workspaceId) return;
121
+ await apiRequest(config, "/memory/events", {
122
+ method: "POST",
123
+ body: {
124
+ workspace_id: config.workspaceId,
125
+ project_id: config.projectId || void 0,
126
+ event_type: eventType,
127
+ title,
128
+ content: typeof content === "string" ? content : JSON.stringify(content),
129
+ metadata: {
130
+ tags,
131
+ source: "mcp_hook",
132
+ captured_at: (/* @__PURE__ */ new Date()).toISOString()
133
+ }
134
+ }
135
+ });
136
+ }
137
+
138
+ // src/hooks/post-tool-use-failure.ts
139
+ var FAILURE_COUNTERS_FILE = path2.join(homedir2(), ".contextstream", "hook-failure-counts.json");
140
+ function extractErrorText(input) {
141
+ return typeof input.error === "string" && input.error || typeof input.tool_error === "string" && input.tool_error || typeof input.stderr === "string" && input.stderr || "Tool execution failed";
142
+ }
143
+ function failureFingerprint(toolName, errorText) {
144
+ const compact = errorText.split(/\s+/).slice(0, 18).join(" ").toLowerCase();
145
+ return `${toolName.toLowerCase()}:${compact}`;
146
+ }
147
+ function incrementFailureCounter(fingerprint) {
148
+ let counters = {};
149
+ try {
150
+ counters = JSON.parse(fs2.readFileSync(FAILURE_COUNTERS_FILE, "utf8"));
151
+ } catch {
152
+ counters = {};
153
+ }
154
+ counters[fingerprint] = (counters[fingerprint] || 0) + 1;
155
+ fs2.mkdirSync(path2.dirname(FAILURE_COUNTERS_FILE), { recursive: true });
156
+ fs2.writeFileSync(FAILURE_COUNTERS_FILE, JSON.stringify(counters, null, 2), "utf8");
157
+ return counters[fingerprint];
158
+ }
159
+ async function runPostToolUseFailureHook() {
160
+ const input = readHookInput();
161
+ const cwd = extractCwd(input);
162
+ const config = loadHookConfig(cwd);
163
+ const toolName = typeof input.tool_name === "string" && input.tool_name || typeof input.toolName === "string" && input.toolName || "unknown";
164
+ const errorText = extractErrorText(input);
165
+ const toolUseId = typeof input.tool_use_id === "string" && input.tool_use_id || typeof input.toolUseId === "string" && input.toolUseId || "";
166
+ const fingerprint = failureFingerprint(toolName, errorText);
167
+ const count = incrementFailureCounter(fingerprint);
168
+ if (isConfigured(config)) {
169
+ await postMemoryEvent(
170
+ config,
171
+ `Tool failure: ${toolName}`,
172
+ {
173
+ tool_name: toolName,
174
+ tool_use_id: toolUseId || null,
175
+ error: errorText,
176
+ fingerprint,
177
+ occurrence_count: count,
178
+ tool_input: input.tool_input || {},
179
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
180
+ },
181
+ ["hook", "post_tool_use_failure", "tool_error"]
182
+ ).catch(() => {
183
+ });
184
+ const autoLessonEnabled = process.env.CONTEXTSTREAM_FAILURE_AUTO_LESSON !== "false";
185
+ if (autoLessonEnabled && count >= 3) {
186
+ await postMemoryEvent(
187
+ config,
188
+ `Recurring failure lesson: ${toolName}`,
189
+ {
190
+ title: `Recurring failure in ${toolName}`,
191
+ trigger: errorText,
192
+ prevention: "Add guardrails or alternate fallback path for this failure mode.",
193
+ occurrences: count,
194
+ fingerprint
195
+ },
196
+ ["hook", "lesson", "recurring_failure"],
197
+ "lesson"
198
+ ).catch(() => {
199
+ });
200
+ }
201
+ }
202
+ writeHookOutput();
203
+ }
204
+ var isDirectRun = process.argv[1]?.includes("post-tool-use-failure") || process.argv[2] === "post-tool-use-failure";
205
+ if (isDirectRun) {
206
+ runPostToolUseFailureHook().catch(() => process.exit(0));
207
+ }
208
+ export {
209
+ runPostToolUseFailureHook
210
+ };
@@ -141,7 +141,7 @@ function isContextFreshAndClean(cwd, maxAgeSeconds) {
141
141
  if (ageSeconds < 0 || ageSeconds > maxAgeSeconds) return false;
142
142
  if (entry.last_state_change_at) {
143
143
  const changedAt = new Date(entry.last_state_change_at);
144
- if (!Number.isNaN(changedAt.getTime()) && changedAt.getTime() > contextAt.getTime()) {
144
+ if (!Number.isNaN(changedAt.getTime()) && changedAt.getTime() >= contextAt.getTime()) {
145
145
  return false;
146
146
  }
147
147
  }
@@ -485,9 +485,18 @@ async function runPreToolUseHook() {
485
485
  blockClaudeCode(msg);
486
486
  }
487
487
  }
488
+ } else if (tool === "Explore") {
489
+ const msg = 'Project index is current. Use mcp__contextstream__search(mode="auto", output_format="paths") instead of Explore for broad discovery.';
490
+ if (editorFormat === "cline") {
491
+ outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
492
+ } else if (editorFormat === "cursor") {
493
+ outputCursorBlock(msg);
494
+ }
495
+ blockClaudeCode(msg);
488
496
  } else if (tool === "Task") {
489
- const subagentType = toolInput?.subagent_type?.toLowerCase() || "";
490
- if (subagentType === "explore") {
497
+ const subagentTypeRaw = toolInput?.subagent_type || toolInput?.subagentType || "";
498
+ const subagentType = subagentTypeRaw.toLowerCase();
499
+ if (subagentType.includes("explore")) {
491
500
  const msg = 'Project index is current. Use mcp__contextstream__search(mode="auto") instead of Task(Explore) for broad discovery.';
492
501
  if (editorFormat === "cline") {
493
502
  outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
@@ -496,8 +505,8 @@ async function runPreToolUseHook() {
496
505
  }
497
506
  blockClaudeCode(msg);
498
507
  }
499
- if (subagentType === "plan") {
500
- const msg = 'After your plan is ready, save it with mcp__contextstream__session(action="capture_plan"). Then create tasks with mcp__contextstream__memory(action="create_task", title="...", plan_id="...").';
508
+ if (subagentType.includes("plan")) {
509
+ const msg = 'For planning, use mcp__contextstream__search(mode="auto", output_format="paths") for discovery, then save your plan with mcp__contextstream__session(action="capture_plan"). Then create tasks with mcp__contextstream__memory(action="create_task", title="...", plan_id="...").';
501
510
  if (editorFormat === "cline") {
502
511
  outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream plans for persistence.");
503
512
  } else if (editorFormat === "cursor") {