@contextstream/mcp-server 0.4.43 → 0.4.45
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 +92 -211
- package/dist/hooks/auto-rules.js +912 -0
- package/dist/hooks/media-aware.js +103 -0
- package/dist/hooks/post-write.js +341 -0
- package/dist/hooks/pre-compact.js +229 -0
- package/dist/hooks/pre-tool-use.js +236 -0
- package/dist/hooks/user-prompt-submit.js +69 -0
- package/dist/index.js +12164 -10247
- package/package.json +3 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/pre-tool-use.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_HOOK_ENABLED !== "false";
|
|
8
|
+
var INDEX_STATUS_FILE = path.join(homedir(), ".contextstream", "indexed-projects.json");
|
|
9
|
+
var STALE_THRESHOLD_DAYS = 7;
|
|
10
|
+
var DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"];
|
|
11
|
+
function isDiscoveryGlob(pattern) {
|
|
12
|
+
const patternLower = pattern.toLowerCase();
|
|
13
|
+
for (const p of DISCOVERY_PATTERNS) {
|
|
14
|
+
if (patternLower.includes(p)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (patternLower.startsWith("**/*.") || patternLower.startsWith("**/")) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (patternLower.includes("**") || patternLower.includes("*/")) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function isDiscoveryGrep(filePath) {
|
|
27
|
+
if (!filePath || filePath === "." || filePath === "./" || filePath === "*" || filePath === "**") {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (filePath.includes("*") || filePath.includes("**")) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function isProjectIndexed(cwd) {
|
|
36
|
+
if (!fs.existsSync(INDEX_STATUS_FILE)) {
|
|
37
|
+
return { isIndexed: false, isStale: false };
|
|
38
|
+
}
|
|
39
|
+
let data;
|
|
40
|
+
try {
|
|
41
|
+
const content = fs.readFileSync(INDEX_STATUS_FILE, "utf-8");
|
|
42
|
+
data = JSON.parse(content);
|
|
43
|
+
} catch {
|
|
44
|
+
return { isIndexed: false, isStale: false };
|
|
45
|
+
}
|
|
46
|
+
const projects = data.projects || {};
|
|
47
|
+
const cwdPath = path.resolve(cwd);
|
|
48
|
+
for (const [projectPath, info] of Object.entries(projects)) {
|
|
49
|
+
try {
|
|
50
|
+
const indexedPath = path.resolve(projectPath);
|
|
51
|
+
if (cwdPath === indexedPath || cwdPath.startsWith(indexedPath + path.sep)) {
|
|
52
|
+
const indexedAt = info.indexed_at;
|
|
53
|
+
if (indexedAt) {
|
|
54
|
+
try {
|
|
55
|
+
const indexedTime = new Date(indexedAt);
|
|
56
|
+
const now = /* @__PURE__ */ new Date();
|
|
57
|
+
const diffDays = (now.getTime() - indexedTime.getTime()) / (1e3 * 60 * 60 * 24);
|
|
58
|
+
if (diffDays > STALE_THRESHOLD_DAYS) {
|
|
59
|
+
return { isIndexed: true, isStale: true };
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { isIndexed: true, isStale: false };
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { isIndexed: false, isStale: false };
|
|
71
|
+
}
|
|
72
|
+
function extractCwd(input) {
|
|
73
|
+
if (input.cwd) return input.cwd;
|
|
74
|
+
if (input.workspace_roots?.length) return input.workspace_roots[0];
|
|
75
|
+
if (input.workspaceRoots?.length) return input.workspaceRoots[0];
|
|
76
|
+
return process.cwd();
|
|
77
|
+
}
|
|
78
|
+
function extractToolName(input) {
|
|
79
|
+
return input.tool_name || input.toolName || "";
|
|
80
|
+
}
|
|
81
|
+
function extractToolInput(input) {
|
|
82
|
+
return input.tool_input || input.parameters || input.toolParameters || {};
|
|
83
|
+
}
|
|
84
|
+
function blockClaudeCode(message) {
|
|
85
|
+
console.error(message);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
function outputClineBlock(errorMessage, contextMod) {
|
|
89
|
+
const result = {
|
|
90
|
+
cancel: true,
|
|
91
|
+
errorMessage
|
|
92
|
+
};
|
|
93
|
+
if (contextMod) {
|
|
94
|
+
result.contextModification = contextMod;
|
|
95
|
+
}
|
|
96
|
+
console.log(JSON.stringify(result));
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
function outputClineAllow() {
|
|
100
|
+
console.log(JSON.stringify({ cancel: false }));
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
function outputCursorBlock(reason) {
|
|
104
|
+
console.log(JSON.stringify({ decision: "deny", reason }));
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
function outputCursorAllow() {
|
|
108
|
+
console.log(JSON.stringify({ decision: "allow" }));
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
function detectEditorFormat(input) {
|
|
112
|
+
if (input.hookName !== void 0 || input.toolName !== void 0) {
|
|
113
|
+
return "cline";
|
|
114
|
+
}
|
|
115
|
+
if (input.hook_event_name !== void 0) {
|
|
116
|
+
return "cursor";
|
|
117
|
+
}
|
|
118
|
+
return "claude";
|
|
119
|
+
}
|
|
120
|
+
async function runPreToolUseHook() {
|
|
121
|
+
if (!ENABLED) {
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
let inputData = "";
|
|
125
|
+
for await (const chunk of process.stdin) {
|
|
126
|
+
inputData += chunk;
|
|
127
|
+
}
|
|
128
|
+
if (!inputData.trim()) {
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
let input;
|
|
132
|
+
try {
|
|
133
|
+
input = JSON.parse(inputData);
|
|
134
|
+
} catch {
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
const editorFormat = detectEditorFormat(input);
|
|
138
|
+
const cwd = extractCwd(input);
|
|
139
|
+
const tool = extractToolName(input);
|
|
140
|
+
const toolInput = extractToolInput(input);
|
|
141
|
+
const { isIndexed } = isProjectIndexed(cwd);
|
|
142
|
+
if (!isIndexed) {
|
|
143
|
+
if (editorFormat === "cline") {
|
|
144
|
+
outputClineAllow();
|
|
145
|
+
} else if (editorFormat === "cursor") {
|
|
146
|
+
outputCursorAllow();
|
|
147
|
+
}
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
if (tool === "Glob") {
|
|
151
|
+
const pattern = toolInput?.pattern || "";
|
|
152
|
+
if (isDiscoveryGlob(pattern)) {
|
|
153
|
+
const msg = `STOP: Use mcp__contextstream__search(mode="hybrid", query="${pattern}") instead of Glob.`;
|
|
154
|
+
if (editorFormat === "cline") {
|
|
155
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
156
|
+
} else if (editorFormat === "cursor") {
|
|
157
|
+
outputCursorBlock(msg);
|
|
158
|
+
}
|
|
159
|
+
blockClaudeCode(msg);
|
|
160
|
+
}
|
|
161
|
+
} else if (tool === "Grep" || tool === "Search") {
|
|
162
|
+
const pattern = toolInput?.pattern || "";
|
|
163
|
+
const filePath = toolInput?.path || "";
|
|
164
|
+
if (pattern) {
|
|
165
|
+
if (filePath && !isDiscoveryGrep(filePath)) {
|
|
166
|
+
const msg = `STOP: Use Read("${filePath}") to view file content, or mcp__contextstream__search(mode="keyword", query="${pattern}") for codebase search.`;
|
|
167
|
+
if (editorFormat === "cline") {
|
|
168
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
169
|
+
} else if (editorFormat === "cursor") {
|
|
170
|
+
outputCursorBlock(msg);
|
|
171
|
+
}
|
|
172
|
+
blockClaudeCode(msg);
|
|
173
|
+
} else {
|
|
174
|
+
const msg = `STOP: Use mcp__contextstream__search(mode="hybrid", query="${pattern}") instead of ${tool}.`;
|
|
175
|
+
if (editorFormat === "cline") {
|
|
176
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
177
|
+
} else if (editorFormat === "cursor") {
|
|
178
|
+
outputCursorBlock(msg);
|
|
179
|
+
}
|
|
180
|
+
blockClaudeCode(msg);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else if (tool === "Task") {
|
|
184
|
+
const subagentType = toolInput?.subagent_type?.toLowerCase() || "";
|
|
185
|
+
if (subagentType === "explore") {
|
|
186
|
+
const msg = 'STOP: Use mcp__contextstream__search(mode="hybrid") instead of Task(Explore).';
|
|
187
|
+
if (editorFormat === "cline") {
|
|
188
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
189
|
+
} else if (editorFormat === "cursor") {
|
|
190
|
+
outputCursorBlock(msg);
|
|
191
|
+
}
|
|
192
|
+
blockClaudeCode(msg);
|
|
193
|
+
}
|
|
194
|
+
if (subagentType === "plan") {
|
|
195
|
+
const msg = 'STOP: Use mcp__contextstream__session(action="capture_plan") for planning. ContextStream plans persist across sessions.';
|
|
196
|
+
if (editorFormat === "cline") {
|
|
197
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream plans for persistence.");
|
|
198
|
+
} else if (editorFormat === "cursor") {
|
|
199
|
+
outputCursorBlock(msg);
|
|
200
|
+
}
|
|
201
|
+
blockClaudeCode(msg);
|
|
202
|
+
}
|
|
203
|
+
} else if (tool === "EnterPlanMode") {
|
|
204
|
+
const msg = 'STOP: Use mcp__contextstream__session(action="capture_plan", title="...", steps=[...]) instead of EnterPlanMode. ContextStream plans persist across sessions and are searchable.';
|
|
205
|
+
if (editorFormat === "cline") {
|
|
206
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream plans for persistence.");
|
|
207
|
+
} else if (editorFormat === "cursor") {
|
|
208
|
+
outputCursorBlock(msg);
|
|
209
|
+
}
|
|
210
|
+
blockClaudeCode(msg);
|
|
211
|
+
}
|
|
212
|
+
if (tool === "list_files" || tool === "search_files") {
|
|
213
|
+
const pattern = toolInput?.path || toolInput?.regex || "";
|
|
214
|
+
if (isDiscoveryGlob(pattern) || isDiscoveryGrep(pattern)) {
|
|
215
|
+
const msg = `Use mcp__contextstream__search(mode="hybrid", query="${pattern}") instead of ${tool}. ContextStream search is indexed and faster.`;
|
|
216
|
+
if (editorFormat === "cline") {
|
|
217
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
218
|
+
} else if (editorFormat === "cursor") {
|
|
219
|
+
outputCursorBlock(msg);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (editorFormat === "cline") {
|
|
224
|
+
outputClineAllow();
|
|
225
|
+
} else if (editorFormat === "cursor") {
|
|
226
|
+
outputCursorAllow();
|
|
227
|
+
}
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
var isDirectRun = process.argv[1]?.includes("pre-tool-use") || process.argv[2] === "pre-tool-use";
|
|
231
|
+
if (isDirectRun) {
|
|
232
|
+
runPreToolUseHook().catch(() => process.exit(0));
|
|
233
|
+
}
|
|
234
|
+
export {
|
|
235
|
+
runPreToolUseHook
|
|
236
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/user-prompt-submit.ts
|
|
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]`;
|
|
10
|
+
function detectEditorFormat(input) {
|
|
11
|
+
if (input.hookName !== void 0) {
|
|
12
|
+
return "cline";
|
|
13
|
+
}
|
|
14
|
+
if (input.hook_event_name === "beforeSubmitPrompt") {
|
|
15
|
+
return "cursor";
|
|
16
|
+
}
|
|
17
|
+
return "claude";
|
|
18
|
+
}
|
|
19
|
+
async function runUserPromptSubmitHook() {
|
|
20
|
+
if (!ENABLED) {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
let inputData = "";
|
|
24
|
+
for await (const chunk of process.stdin) {
|
|
25
|
+
inputData += chunk;
|
|
26
|
+
}
|
|
27
|
+
if (!inputData.trim()) {
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
let input;
|
|
31
|
+
try {
|
|
32
|
+
input = JSON.parse(inputData);
|
|
33
|
+
} catch {
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
const editorFormat = detectEditorFormat(input);
|
|
37
|
+
if (editorFormat === "claude") {
|
|
38
|
+
console.log(
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
hookSpecificOutput: {
|
|
41
|
+
hookEventName: "UserPromptSubmit",
|
|
42
|
+
additionalContext: REMINDER
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
} else if (editorFormat === "cline") {
|
|
47
|
+
console.log(
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
cancel: false,
|
|
50
|
+
contextModification: REMINDER
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
} else if (editorFormat === "cursor") {
|
|
54
|
+
console.log(
|
|
55
|
+
JSON.stringify({
|
|
56
|
+
continue: true,
|
|
57
|
+
user_message: "[CONTEXTSTREAM] Search with mcp__contextstream__search before using Glob/Grep/Read"
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
var isDirectRun = process.argv[1]?.includes("user-prompt-submit") || process.argv[2] === "user-prompt-submit";
|
|
64
|
+
if (isDirectRun) {
|
|
65
|
+
runUserPromptSubmitHook().catch(() => process.exit(0));
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
runUserPromptSubmitHook
|
|
69
|
+
};
|