@contextstream/mcp-server 0.4.63 → 0.4.65
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 +43 -1
- package/dist/hooks/auto-rules.js +103 -24
- package/dist/hooks/notification.js +162 -0
- package/dist/hooks/permission-request.js +174 -0
- package/dist/hooks/post-tool-use-failure.js +213 -0
- package/dist/hooks/pre-compact.js +1 -4
- package/dist/hooks/pre-tool-use.js +14 -5
- package/dist/hooks/runner.js +2306 -5164
- package/dist/hooks/session-init.js +33 -13
- package/dist/hooks/stop.js +171 -0
- package/dist/hooks/subagent-start.js +195 -0
- package/dist/hooks/subagent-stop.js +301 -0
- package/dist/hooks/task-completed.js +260 -0
- package/dist/hooks/teammate-idle.js +200 -0
- package/dist/hooks/user-prompt-submit.js +6 -4
- package/dist/index.js +3420 -1650
- package/dist/test-server.js +4 -2
- package/package.json +2 -2
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
var HOOK_SPECIFIC_OUTPUT_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit", "PostToolUse"]);
|
|
14
|
+
function readHookInput() {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function writeHookOutput(output) {
|
|
22
|
+
const eventName = output?.hookEventName || (typeof process.env.HOOK_EVENT_NAME === "string" ? process.env.HOOK_EVENT_NAME.trim() : "");
|
|
23
|
+
const canUseHookSpecificOutput = HOOK_SPECIFIC_OUTPUT_EVENTS.has(eventName);
|
|
24
|
+
const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
|
|
25
|
+
hookSpecificOutput: output.additionalContext && canUseHookSpecificOutput ? {
|
|
26
|
+
hookEventName: eventName,
|
|
27
|
+
additionalContext: output.additionalContext
|
|
28
|
+
} : void 0,
|
|
29
|
+
additionalContext: output.additionalContext,
|
|
30
|
+
blocked: output.blocked,
|
|
31
|
+
reason: output.reason
|
|
32
|
+
} : {};
|
|
33
|
+
console.log(JSON.stringify(payload));
|
|
34
|
+
}
|
|
35
|
+
function extractCwd(input) {
|
|
36
|
+
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
|
|
37
|
+
return cwd;
|
|
38
|
+
}
|
|
39
|
+
function loadHookConfig(cwd) {
|
|
40
|
+
let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
|
|
41
|
+
let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
42
|
+
let jwt = process.env.CONTEXTSTREAM_JWT || "";
|
|
43
|
+
let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
|
|
44
|
+
let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
|
|
45
|
+
let searchDir = path.resolve(cwd);
|
|
46
|
+
for (let i = 0; i < 6; i++) {
|
|
47
|
+
if (!apiKey && !jwt) {
|
|
48
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
49
|
+
if (fs.existsSync(mcpPath)) {
|
|
50
|
+
try {
|
|
51
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
52
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
53
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
54
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
55
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
56
|
+
if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
|
|
57
|
+
if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!workspaceId || !projectId) {
|
|
63
|
+
const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
64
|
+
if (fs.existsSync(localConfigPath)) {
|
|
65
|
+
try {
|
|
66
|
+
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
|
|
67
|
+
if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
|
|
68
|
+
if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const parentDir = path.dirname(searchDir);
|
|
74
|
+
if (parentDir === searchDir) break;
|
|
75
|
+
searchDir = parentDir;
|
|
76
|
+
}
|
|
77
|
+
if (!apiKey && !jwt) {
|
|
78
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
79
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
|
|
82
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
83
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
84
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
85
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { apiUrl, apiKey, jwt, workspaceId, projectId };
|
|
91
|
+
}
|
|
92
|
+
function isConfigured(config) {
|
|
93
|
+
return Boolean(config.apiKey || config.jwt);
|
|
94
|
+
}
|
|
95
|
+
function authHeaders(config) {
|
|
96
|
+
if (config.apiKey) {
|
|
97
|
+
return { "X-API-Key": config.apiKey };
|
|
98
|
+
}
|
|
99
|
+
if (config.jwt) {
|
|
100
|
+
return { Authorization: `Bearer ${config.jwt}` };
|
|
101
|
+
}
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
async function apiRequest(config, apiPath, init = {}) {
|
|
105
|
+
const response = await fetch(`${config.apiUrl}${apiPath}`, {
|
|
106
|
+
method: init.method || "GET",
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
...authHeaders(config)
|
|
110
|
+
},
|
|
111
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
115
|
+
}
|
|
116
|
+
const json = await response.json();
|
|
117
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
118
|
+
return json.data;
|
|
119
|
+
}
|
|
120
|
+
return json;
|
|
121
|
+
}
|
|
122
|
+
async function postMemoryEvent(config, title, content, tags, eventType = "operation") {
|
|
123
|
+
if (!isConfigured(config) || !config.workspaceId) return;
|
|
124
|
+
await apiRequest(config, "/memory/events", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
body: {
|
|
127
|
+
workspace_id: config.workspaceId,
|
|
128
|
+
project_id: config.projectId || void 0,
|
|
129
|
+
event_type: eventType,
|
|
130
|
+
title,
|
|
131
|
+
content: typeof content === "string" ? content : JSON.stringify(content),
|
|
132
|
+
metadata: {
|
|
133
|
+
tags,
|
|
134
|
+
source: "mcp_hook",
|
|
135
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/hooks/post-tool-use-failure.ts
|
|
142
|
+
var FAILURE_COUNTERS_FILE = path2.join(homedir2(), ".contextstream", "hook-failure-counts.json");
|
|
143
|
+
function extractErrorText(input) {
|
|
144
|
+
return typeof input.error === "string" && input.error || typeof input.tool_error === "string" && input.tool_error || typeof input.stderr === "string" && input.stderr || "Tool execution failed";
|
|
145
|
+
}
|
|
146
|
+
function failureFingerprint(toolName, errorText) {
|
|
147
|
+
const compact = errorText.split(/\s+/).slice(0, 18).join(" ").toLowerCase();
|
|
148
|
+
return `${toolName.toLowerCase()}:${compact}`;
|
|
149
|
+
}
|
|
150
|
+
function incrementFailureCounter(fingerprint) {
|
|
151
|
+
let counters = {};
|
|
152
|
+
try {
|
|
153
|
+
counters = JSON.parse(fs2.readFileSync(FAILURE_COUNTERS_FILE, "utf8"));
|
|
154
|
+
} catch {
|
|
155
|
+
counters = {};
|
|
156
|
+
}
|
|
157
|
+
counters[fingerprint] = (counters[fingerprint] || 0) + 1;
|
|
158
|
+
fs2.mkdirSync(path2.dirname(FAILURE_COUNTERS_FILE), { recursive: true });
|
|
159
|
+
fs2.writeFileSync(FAILURE_COUNTERS_FILE, JSON.stringify(counters, null, 2), "utf8");
|
|
160
|
+
return counters[fingerprint];
|
|
161
|
+
}
|
|
162
|
+
async function runPostToolUseFailureHook() {
|
|
163
|
+
const input = readHookInput();
|
|
164
|
+
const cwd = extractCwd(input);
|
|
165
|
+
const config = loadHookConfig(cwd);
|
|
166
|
+
const toolName = typeof input.tool_name === "string" && input.tool_name || typeof input.toolName === "string" && input.toolName || "unknown";
|
|
167
|
+
const errorText = extractErrorText(input);
|
|
168
|
+
const toolUseId = typeof input.tool_use_id === "string" && input.tool_use_id || typeof input.toolUseId === "string" && input.toolUseId || "";
|
|
169
|
+
const fingerprint = failureFingerprint(toolName, errorText);
|
|
170
|
+
const count = incrementFailureCounter(fingerprint);
|
|
171
|
+
if (isConfigured(config)) {
|
|
172
|
+
await postMemoryEvent(
|
|
173
|
+
config,
|
|
174
|
+
`Tool failure: ${toolName}`,
|
|
175
|
+
{
|
|
176
|
+
tool_name: toolName,
|
|
177
|
+
tool_use_id: toolUseId || null,
|
|
178
|
+
error: errorText,
|
|
179
|
+
fingerprint,
|
|
180
|
+
occurrence_count: count,
|
|
181
|
+
tool_input: input.tool_input || {},
|
|
182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
183
|
+
},
|
|
184
|
+
["hook", "post_tool_use_failure", "tool_error"]
|
|
185
|
+
).catch(() => {
|
|
186
|
+
});
|
|
187
|
+
const autoLessonEnabled = process.env.CONTEXTSTREAM_FAILURE_AUTO_LESSON !== "false";
|
|
188
|
+
if (autoLessonEnabled && count >= 3) {
|
|
189
|
+
await postMemoryEvent(
|
|
190
|
+
config,
|
|
191
|
+
`Recurring failure lesson: ${toolName}`,
|
|
192
|
+
{
|
|
193
|
+
title: `Recurring failure in ${toolName}`,
|
|
194
|
+
trigger: errorText,
|
|
195
|
+
prevention: "Add guardrails or alternate fallback path for this failure mode.",
|
|
196
|
+
occurrences: count,
|
|
197
|
+
fingerprint
|
|
198
|
+
},
|
|
199
|
+
["hook", "lesson", "recurring_failure"],
|
|
200
|
+
"lesson"
|
|
201
|
+
).catch(() => {
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
writeHookOutput();
|
|
206
|
+
}
|
|
207
|
+
var isDirectRun = process.argv[1]?.includes("post-tool-use-failure") || process.argv[2] === "post-tool-use-failure";
|
|
208
|
+
if (isDirectRun) {
|
|
209
|
+
runPostToolUseFailureHook().catch(() => process.exit(0));
|
|
210
|
+
}
|
|
211
|
+
export {
|
|
212
|
+
runPostToolUseFailureHook
|
|
213
|
+
};
|
|
@@ -303,10 +303,7 @@ After compaction, call session_init(is_post_compact=true) to restore context.${c
|
|
|
303
303
|
User instructions: ${customInstructions}` : ""}`;
|
|
304
304
|
console.log(
|
|
305
305
|
JSON.stringify({
|
|
306
|
-
|
|
307
|
-
hookEventName: "PreCompact",
|
|
308
|
-
additionalContext: context
|
|
309
|
-
}
|
|
306
|
+
additionalContext: context
|
|
310
307
|
})
|
|
311
308
|
);
|
|
312
309
|
process.exit(0);
|
|
@@ -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()
|
|
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
|
|
490
|
-
|
|
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
|
|
500
|
-
const msg = '
|
|
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") {
|