@contextstream/mcp-server 0.4.82 → 1.0.1
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/NOTICE +25 -0
- package/README.md +174 -290
- package/assets/contextstream-logo.png +0 -0
- package/npm/bin/contextstream-hook.mjs +9 -0
- package/npm/bin/contextstream-mcp.mjs +9 -0
- package/npm/bin/mcp-server.mjs +9 -0
- package/npm/launcher.mjs +390 -0
- package/package.json +28 -72
- package/server.json +43 -0
- package/dist/hooks/auto-rules.js +0 -3529
- package/dist/hooks/media-aware.js +0 -103
- package/dist/hooks/notification.js +0 -162
- package/dist/hooks/on-bash.js +0 -190
- package/dist/hooks/on-read.js +0 -321
- package/dist/hooks/on-save-intent.js +0 -132
- package/dist/hooks/on-task.js +0 -139
- package/dist/hooks/on-web.js +0 -155
- package/dist/hooks/permission-request.js +0 -174
- package/dist/hooks/post-compact.js +0 -172
- package/dist/hooks/post-tool-use-failure.js +0 -213
- package/dist/hooks/post-write.js +0 -1023
- package/dist/hooks/pre-compact.js +0 -317
- package/dist/hooks/pre-tool-use.js +0 -553
- package/dist/hooks/runner.js +0 -5826
- package/dist/hooks/session-end.js +0 -285
- package/dist/hooks/session-init.js +0 -1880
- package/dist/hooks/stop.js +0 -171
- package/dist/hooks/subagent-start.js +0 -195
- package/dist/hooks/subagent-stop.js +0 -301
- package/dist/hooks/task-completed.js +0 -260
- package/dist/hooks/teammate-idle.js +0 -200
- package/dist/hooks/user-prompt-submit.js +0 -866
- package/dist/index.js +0 -41985
- package/dist/test-server.js +0 -4363
- package/scripts/postinstall.js +0 -56
- package/scripts/registration-smoke.mts +0 -45
|
@@ -1,553 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/hooks/pre-tool-use.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/prompt-state.ts
|
|
9
|
-
import * as fs from "node:fs";
|
|
10
|
-
import * as path from "node:path";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
var STATE_PATH = path.join(homedir(), ".contextstream", "prompt-state.json");
|
|
13
|
-
function defaultState() {
|
|
14
|
-
return { workspaces: {} };
|
|
15
|
-
}
|
|
16
|
-
function nowIso() {
|
|
17
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
18
|
-
}
|
|
19
|
-
function ensureStateDir() {
|
|
20
|
-
try {
|
|
21
|
-
fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
|
|
22
|
-
} catch {
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function normalizePath(input) {
|
|
26
|
-
try {
|
|
27
|
-
return path.resolve(input);
|
|
28
|
-
} catch {
|
|
29
|
-
return input;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function workspacePathsMatch(a, b) {
|
|
33
|
-
const left = normalizePath(a);
|
|
34
|
-
const right = normalizePath(b);
|
|
35
|
-
return left === right || left.startsWith(`${right}${path.sep}`) || right.startsWith(`${left}${path.sep}`);
|
|
36
|
-
}
|
|
37
|
-
function readState() {
|
|
38
|
-
try {
|
|
39
|
-
const content = fs.readFileSync(STATE_PATH, "utf8");
|
|
40
|
-
const parsed = JSON.parse(content);
|
|
41
|
-
if (!parsed || typeof parsed !== "object" || !parsed.workspaces) {
|
|
42
|
-
return defaultState();
|
|
43
|
-
}
|
|
44
|
-
return parsed;
|
|
45
|
-
} catch {
|
|
46
|
-
return defaultState();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function writeState(state) {
|
|
50
|
-
try {
|
|
51
|
-
ensureStateDir();
|
|
52
|
-
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf8");
|
|
53
|
-
} catch {
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function getOrCreateEntry(state, cwd) {
|
|
57
|
-
if (!cwd.trim()) return null;
|
|
58
|
-
const exact = state.workspaces[cwd];
|
|
59
|
-
if (exact) return { key: cwd, entry: exact };
|
|
60
|
-
for (const [trackedCwd, trackedEntry] of Object.entries(state.workspaces)) {
|
|
61
|
-
if (workspacePathsMatch(trackedCwd, cwd)) {
|
|
62
|
-
return { key: trackedCwd, entry: trackedEntry };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const created = {
|
|
66
|
-
require_context: false,
|
|
67
|
-
require_init: false,
|
|
68
|
-
last_context_at: void 0,
|
|
69
|
-
last_state_change_at: void 0,
|
|
70
|
-
index_wait_started_at: void 0,
|
|
71
|
-
index_wait_until: void 0,
|
|
72
|
-
updated_at: nowIso()
|
|
73
|
-
};
|
|
74
|
-
state.workspaces[cwd] = created;
|
|
75
|
-
return { key: cwd, entry: created };
|
|
76
|
-
}
|
|
77
|
-
function cleanupStale(maxAgeSeconds) {
|
|
78
|
-
const state = readState();
|
|
79
|
-
const now = Date.now();
|
|
80
|
-
let changed = false;
|
|
81
|
-
for (const [cwd, entry] of Object.entries(state.workspaces)) {
|
|
82
|
-
const updated = new Date(entry.updated_at);
|
|
83
|
-
if (Number.isNaN(updated.getTime())) continue;
|
|
84
|
-
const ageSeconds = (now - updated.getTime()) / 1e3;
|
|
85
|
-
if (ageSeconds > maxAgeSeconds) {
|
|
86
|
-
delete state.workspaces[cwd];
|
|
87
|
-
changed = true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (changed) {
|
|
91
|
-
writeState(state);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function clearContextRequired(cwd) {
|
|
95
|
-
if (!cwd.trim()) return;
|
|
96
|
-
const state = readState();
|
|
97
|
-
const target = getOrCreateEntry(state, cwd);
|
|
98
|
-
if (!target) return;
|
|
99
|
-
target.entry.require_context = false;
|
|
100
|
-
target.entry.last_context_at = nowIso();
|
|
101
|
-
target.entry.index_wait_started_at = void 0;
|
|
102
|
-
target.entry.index_wait_until = void 0;
|
|
103
|
-
target.entry.updated_at = nowIso();
|
|
104
|
-
writeState(state);
|
|
105
|
-
}
|
|
106
|
-
function isContextRequired(cwd) {
|
|
107
|
-
if (!cwd.trim()) return false;
|
|
108
|
-
const state = readState();
|
|
109
|
-
const target = getOrCreateEntry(state, cwd);
|
|
110
|
-
return Boolean(target?.entry.require_context);
|
|
111
|
-
}
|
|
112
|
-
function clearInitRequired(cwd) {
|
|
113
|
-
if (!cwd.trim()) return;
|
|
114
|
-
const state = readState();
|
|
115
|
-
const target = getOrCreateEntry(state, cwd);
|
|
116
|
-
if (!target) return;
|
|
117
|
-
target.entry.require_init = false;
|
|
118
|
-
target.entry.updated_at = nowIso();
|
|
119
|
-
writeState(state);
|
|
120
|
-
}
|
|
121
|
-
function isInitRequired(cwd) {
|
|
122
|
-
if (!cwd.trim()) return false;
|
|
123
|
-
const state = readState();
|
|
124
|
-
const target = getOrCreateEntry(state, cwd);
|
|
125
|
-
return Boolean(target?.entry.require_init);
|
|
126
|
-
}
|
|
127
|
-
function markStateChanged(cwd) {
|
|
128
|
-
if (!cwd.trim()) return;
|
|
129
|
-
const state = readState();
|
|
130
|
-
const target = getOrCreateEntry(state, cwd);
|
|
131
|
-
if (!target) return;
|
|
132
|
-
target.entry.last_state_change_at = nowIso();
|
|
133
|
-
target.entry.updated_at = nowIso();
|
|
134
|
-
writeState(state);
|
|
135
|
-
}
|
|
136
|
-
function isContextFreshAndClean(cwd, maxAgeSeconds) {
|
|
137
|
-
if (!cwd.trim()) return false;
|
|
138
|
-
const state = readState();
|
|
139
|
-
const target = getOrCreateEntry(state, cwd);
|
|
140
|
-
const entry = target?.entry;
|
|
141
|
-
if (!entry?.last_context_at) return false;
|
|
142
|
-
const contextAt = new Date(entry.last_context_at);
|
|
143
|
-
if (Number.isNaN(contextAt.getTime())) return false;
|
|
144
|
-
const ageSeconds = (Date.now() - contextAt.getTime()) / 1e3;
|
|
145
|
-
if (ageSeconds < 0 || ageSeconds > maxAgeSeconds) return false;
|
|
146
|
-
if (entry.last_state_change_at) {
|
|
147
|
-
const changedAt = new Date(entry.last_state_change_at);
|
|
148
|
-
if (!Number.isNaN(changedAt.getTime()) && changedAt.getTime() >= contextAt.getTime()) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// src/hooks/pre-tool-use.ts
|
|
156
|
-
var ENABLED = process.env.CONTEXTSTREAM_HOOK_ENABLED !== "false";
|
|
157
|
-
var INDEX_STATUS_FILE = path2.join(homedir2(), ".contextstream", "indexed-projects.json");
|
|
158
|
-
var DEBUG_FILE = "/tmp/pretooluse-hook-debug.log";
|
|
159
|
-
var STALE_THRESHOLD_DAYS = 7;
|
|
160
|
-
var CONTEXT_FRESHNESS_SECONDS = 120;
|
|
161
|
-
var DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"];
|
|
162
|
-
function isDiscoveryGlob(pattern) {
|
|
163
|
-
const patternLower = pattern.toLowerCase();
|
|
164
|
-
for (const p of DISCOVERY_PATTERNS) {
|
|
165
|
-
if (patternLower.includes(p)) {
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (patternLower.startsWith("**/*.") || patternLower.startsWith("**/")) {
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
if (patternLower.includes("**") || patternLower.includes("*/")) {
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
function isDiscoveryGrep(filePath) {
|
|
178
|
-
if (!filePath || filePath === "." || filePath === "./" || filePath === "*" || filePath === "**") {
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
if (filePath.includes("*") || filePath.includes("**")) {
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
function isProjectIndexed(cwd) {
|
|
187
|
-
if (!fs2.existsSync(INDEX_STATUS_FILE)) {
|
|
188
|
-
return { isIndexed: false, isStale: false };
|
|
189
|
-
}
|
|
190
|
-
let data;
|
|
191
|
-
try {
|
|
192
|
-
const content = fs2.readFileSync(INDEX_STATUS_FILE, "utf-8");
|
|
193
|
-
data = JSON.parse(content);
|
|
194
|
-
} catch {
|
|
195
|
-
return { isIndexed: false, isStale: false };
|
|
196
|
-
}
|
|
197
|
-
const projects = data.projects || {};
|
|
198
|
-
const cwdPath = path2.resolve(cwd);
|
|
199
|
-
for (const [projectPath, info] of Object.entries(projects)) {
|
|
200
|
-
try {
|
|
201
|
-
const indexedPath = path2.resolve(projectPath);
|
|
202
|
-
if (cwdPath === indexedPath || cwdPath.startsWith(indexedPath + path2.sep)) {
|
|
203
|
-
const indexedAt = info.indexed_at;
|
|
204
|
-
if (indexedAt) {
|
|
205
|
-
try {
|
|
206
|
-
const indexedTime = new Date(indexedAt);
|
|
207
|
-
const now = /* @__PURE__ */ new Date();
|
|
208
|
-
const diffDays = (now.getTime() - indexedTime.getTime()) / (1e3 * 60 * 60 * 24);
|
|
209
|
-
if (diffDays > STALE_THRESHOLD_DAYS) {
|
|
210
|
-
return { isIndexed: true, isStale: true };
|
|
211
|
-
}
|
|
212
|
-
} catch {
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return { isIndexed: true, isStale: false };
|
|
216
|
-
}
|
|
217
|
-
} catch {
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return { isIndexed: false, isStale: false };
|
|
222
|
-
}
|
|
223
|
-
function extractCwd(input) {
|
|
224
|
-
if (input.cwd) return input.cwd;
|
|
225
|
-
if (input.workspace_roots?.length) return input.workspace_roots[0];
|
|
226
|
-
if (input.workspaceRoots?.length) return input.workspaceRoots[0];
|
|
227
|
-
return process.cwd();
|
|
228
|
-
}
|
|
229
|
-
function extractToolName(input) {
|
|
230
|
-
return input.tool_name || input.toolName || "";
|
|
231
|
-
}
|
|
232
|
-
function extractToolInput(input) {
|
|
233
|
-
return input.tool_input || input.parameters || input.toolParameters || {};
|
|
234
|
-
}
|
|
235
|
-
function normalizeContextstreamToolName(toolName) {
|
|
236
|
-
const trimmed = toolName.trim();
|
|
237
|
-
if (!trimmed) return null;
|
|
238
|
-
const lower = trimmed.toLowerCase();
|
|
239
|
-
const prefixed = "mcp__contextstream__";
|
|
240
|
-
if (lower.startsWith(prefixed)) {
|
|
241
|
-
return lower.slice(prefixed.length);
|
|
242
|
-
}
|
|
243
|
-
if (lower.startsWith("contextstream__")) {
|
|
244
|
-
return lower.slice("contextstream__".length);
|
|
245
|
-
}
|
|
246
|
-
if (lower === "init" || lower === "context") {
|
|
247
|
-
return lower;
|
|
248
|
-
}
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
function actionFromToolInput(toolInput) {
|
|
252
|
-
const maybeAction = toolInput?.action;
|
|
253
|
-
return typeof maybeAction === "string" ? maybeAction.trim().toLowerCase() : "";
|
|
254
|
-
}
|
|
255
|
-
function isContextstreamReadOnlyOperation(toolName, toolInput) {
|
|
256
|
-
const action = actionFromToolInput(toolInput);
|
|
257
|
-
switch (toolName) {
|
|
258
|
-
case "workspace":
|
|
259
|
-
return action === "list" || action === "get";
|
|
260
|
-
case "memory":
|
|
261
|
-
return action === "list_docs" || action === "list_events" || action === "list_todos" || action === "list_tasks" || action === "list_transcripts" || action === "list_nodes" || action === "decisions" || action === "get_doc" || action === "get_event" || action === "get_task" || action === "get_todo" || action === "get_transcript";
|
|
262
|
-
case "session":
|
|
263
|
-
return action === "get_lessons" || action === "get_plan" || action === "list_plans" || action === "recall";
|
|
264
|
-
case "help":
|
|
265
|
-
return action === "version" || action === "tools" || action === "auth";
|
|
266
|
-
case "project":
|
|
267
|
-
return action === "list" || action === "get" || action === "index_status";
|
|
268
|
-
case "reminder":
|
|
269
|
-
return action === "list" || action === "active";
|
|
270
|
-
case "context":
|
|
271
|
-
case "init":
|
|
272
|
-
return true;
|
|
273
|
-
default:
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
function isLikelyStateChangingTool(toolLower, toolInput, isContextstreamCall, normalizedContextstreamTool) {
|
|
278
|
-
if (isContextstreamCall && normalizedContextstreamTool) {
|
|
279
|
-
return !isContextstreamReadOnlyOperation(normalizedContextstreamTool, toolInput);
|
|
280
|
-
}
|
|
281
|
-
if ([
|
|
282
|
-
"read",
|
|
283
|
-
"read_file",
|
|
284
|
-
"grep",
|
|
285
|
-
"glob",
|
|
286
|
-
"search",
|
|
287
|
-
"grep_search",
|
|
288
|
-
"code_search",
|
|
289
|
-
"semanticsearch",
|
|
290
|
-
"codebase_search",
|
|
291
|
-
"list_files",
|
|
292
|
-
"search_files",
|
|
293
|
-
"search_files_content",
|
|
294
|
-
"find_files",
|
|
295
|
-
"find_by_name",
|
|
296
|
-
"ls",
|
|
297
|
-
"cat",
|
|
298
|
-
"view"
|
|
299
|
-
].includes(toolLower)) {
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
const writeMarkers = [
|
|
303
|
-
"write",
|
|
304
|
-
"edit",
|
|
305
|
-
"create",
|
|
306
|
-
"delete",
|
|
307
|
-
"remove",
|
|
308
|
-
"rename",
|
|
309
|
-
"move",
|
|
310
|
-
"patch",
|
|
311
|
-
"apply",
|
|
312
|
-
"insert",
|
|
313
|
-
"append",
|
|
314
|
-
"replace",
|
|
315
|
-
"update",
|
|
316
|
-
"commit",
|
|
317
|
-
"push",
|
|
318
|
-
"install",
|
|
319
|
-
"exec",
|
|
320
|
-
"run",
|
|
321
|
-
"bash",
|
|
322
|
-
"shell"
|
|
323
|
-
];
|
|
324
|
-
return writeMarkers.some((marker) => toolLower.includes(marker));
|
|
325
|
-
}
|
|
326
|
-
function blockClaudeCode(message) {
|
|
327
|
-
const response = {
|
|
328
|
-
hookSpecificOutput: {
|
|
329
|
-
hookEventName: "PreToolUse",
|
|
330
|
-
// Use additionalContext instead of deny - tool runs but Claude sees the message
|
|
331
|
-
additionalContext: `[CONTEXTSTREAM] ${message}`
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] REDIRECT (additionalContext): ${JSON.stringify(response)}
|
|
335
|
-
`);
|
|
336
|
-
console.log(JSON.stringify(response));
|
|
337
|
-
process.exit(0);
|
|
338
|
-
}
|
|
339
|
-
function outputClineBlock(errorMessage, contextMod) {
|
|
340
|
-
const result = {
|
|
341
|
-
cancel: true,
|
|
342
|
-
errorMessage
|
|
343
|
-
};
|
|
344
|
-
if (contextMod) {
|
|
345
|
-
result.contextModification = contextMod;
|
|
346
|
-
}
|
|
347
|
-
console.log(JSON.stringify(result));
|
|
348
|
-
process.exit(0);
|
|
349
|
-
}
|
|
350
|
-
function outputClineAllow() {
|
|
351
|
-
console.log(JSON.stringify({ cancel: false }));
|
|
352
|
-
process.exit(0);
|
|
353
|
-
}
|
|
354
|
-
function outputCursorBlock(reason) {
|
|
355
|
-
console.log(
|
|
356
|
-
JSON.stringify({ permission: "deny", agent_message: reason, user_message: reason })
|
|
357
|
-
);
|
|
358
|
-
process.exit(0);
|
|
359
|
-
}
|
|
360
|
-
function outputCursorAllow() {
|
|
361
|
-
console.log(JSON.stringify({ permission: "allow" }));
|
|
362
|
-
process.exit(0);
|
|
363
|
-
}
|
|
364
|
-
function blockWithMessage(editorFormat, message) {
|
|
365
|
-
if (editorFormat === "cline") {
|
|
366
|
-
outputClineBlock(message, "[CONTEXTSTREAM] Follow ContextStream startup requirements.");
|
|
367
|
-
} else if (editorFormat === "cursor") {
|
|
368
|
-
outputCursorBlock(message);
|
|
369
|
-
}
|
|
370
|
-
blockClaudeCode(message);
|
|
371
|
-
}
|
|
372
|
-
function allowTool(editorFormat, cwd, recordStateChange) {
|
|
373
|
-
if (recordStateChange) {
|
|
374
|
-
markStateChanged(cwd);
|
|
375
|
-
}
|
|
376
|
-
if (editorFormat === "cline") {
|
|
377
|
-
outputClineAllow();
|
|
378
|
-
} else if (editorFormat === "cursor") {
|
|
379
|
-
outputCursorAllow();
|
|
380
|
-
}
|
|
381
|
-
process.exit(0);
|
|
382
|
-
}
|
|
383
|
-
function detectEditorFormat(input) {
|
|
384
|
-
if (process.argv.includes("--editor=cursor")) {
|
|
385
|
-
return "cursor";
|
|
386
|
-
}
|
|
387
|
-
if (input.hookName !== void 0 || input.toolName !== void 0) {
|
|
388
|
-
return "cline";
|
|
389
|
-
}
|
|
390
|
-
if (input.hook_event_name !== void 0 || input.tool_name !== void 0) {
|
|
391
|
-
return "claude";
|
|
392
|
-
}
|
|
393
|
-
return "claude";
|
|
394
|
-
}
|
|
395
|
-
async function runPreToolUseHook() {
|
|
396
|
-
fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] Hook invoked at ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
397
|
-
`);
|
|
398
|
-
console.error("[PreToolUse] Hook invoked at", (/* @__PURE__ */ new Date()).toISOString());
|
|
399
|
-
if (!ENABLED) {
|
|
400
|
-
fs2.appendFileSync(DEBUG_FILE, "[PreToolUse] Hook disabled, exiting\n");
|
|
401
|
-
console.error("[PreToolUse] Hook disabled, exiting");
|
|
402
|
-
process.exit(0);
|
|
403
|
-
}
|
|
404
|
-
let inputData = "";
|
|
405
|
-
for await (const chunk of process.stdin) {
|
|
406
|
-
inputData += chunk;
|
|
407
|
-
}
|
|
408
|
-
if (!inputData.trim()) {
|
|
409
|
-
process.exit(0);
|
|
410
|
-
}
|
|
411
|
-
let input;
|
|
412
|
-
try {
|
|
413
|
-
input = JSON.parse(inputData);
|
|
414
|
-
} catch {
|
|
415
|
-
process.exit(0);
|
|
416
|
-
}
|
|
417
|
-
const editorFormat = detectEditorFormat(input);
|
|
418
|
-
const cwd = extractCwd(input);
|
|
419
|
-
const tool = extractToolName(input);
|
|
420
|
-
const toolInput = extractToolInput(input);
|
|
421
|
-
const toolLower = tool.toLowerCase();
|
|
422
|
-
const normalizedContextstreamTool = normalizeContextstreamToolName(tool);
|
|
423
|
-
const isContextstreamCall = normalizedContextstreamTool !== null;
|
|
424
|
-
const recordStateChange = isLikelyStateChangingTool(
|
|
425
|
-
toolLower,
|
|
426
|
-
toolInput,
|
|
427
|
-
isContextstreamCall,
|
|
428
|
-
normalizedContextstreamTool
|
|
429
|
-
);
|
|
430
|
-
fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] tool=${tool}, cwd=${cwd}, editorFormat=${editorFormat}
|
|
431
|
-
`);
|
|
432
|
-
cleanupStale(180);
|
|
433
|
-
if (isInitRequired(cwd)) {
|
|
434
|
-
if (isContextstreamCall && normalizedContextstreamTool === "init") {
|
|
435
|
-
clearInitRequired(cwd);
|
|
436
|
-
} else {
|
|
437
|
-
const required = "mcp__contextstream__init(...)";
|
|
438
|
-
const msg = `First call required for this session: ${required}. Run it before any other MCP tool. Then call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>").`;
|
|
439
|
-
blockWithMessage(editorFormat, msg);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (isContextRequired(cwd)) {
|
|
443
|
-
if (isContextstreamCall && normalizedContextstreamTool === "context") {
|
|
444
|
-
clearContextRequired(cwd);
|
|
445
|
-
} else if (isContextstreamCall && normalizedContextstreamTool === "init") {
|
|
446
|
-
} else if (isContextstreamCall && normalizedContextstreamTool && isContextstreamReadOnlyOperation(normalizedContextstreamTool, toolInput) && isContextFreshAndClean(cwd, CONTEXT_FRESHNESS_SECONDS)) {
|
|
447
|
-
} else {
|
|
448
|
-
const msg = 'First call required for this prompt: mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>"). Run it before any other MCP tool.';
|
|
449
|
-
blockWithMessage(editorFormat, msg);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
const { isIndexed, isStale } = isProjectIndexed(cwd);
|
|
453
|
-
fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] isIndexed=${isIndexed}, isStale=${isStale}
|
|
454
|
-
`);
|
|
455
|
-
if (!isIndexed || isStale) {
|
|
456
|
-
allowTool(editorFormat, cwd, recordStateChange);
|
|
457
|
-
}
|
|
458
|
-
if (tool === "Glob") {
|
|
459
|
-
const pattern = toolInput?.pattern || "";
|
|
460
|
-
fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] Glob pattern=${pattern}, isDiscovery=${isDiscoveryGlob(pattern)}
|
|
461
|
-
`);
|
|
462
|
-
if (isDiscoveryGlob(pattern)) {
|
|
463
|
-
const msg = `This project index is current. Use mcp__contextstream__search(mode="auto", query="${pattern}") instead of Glob for faster, richer code results.`;
|
|
464
|
-
fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] Intercepting discovery glob: ${msg}
|
|
465
|
-
`);
|
|
466
|
-
if (editorFormat === "cline") {
|
|
467
|
-
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
468
|
-
} else if (editorFormat === "cursor") {
|
|
469
|
-
outputCursorBlock(msg);
|
|
470
|
-
}
|
|
471
|
-
blockClaudeCode(msg);
|
|
472
|
-
}
|
|
473
|
-
} else if (tool === "Grep" || tool === "Search") {
|
|
474
|
-
const pattern = toolInput?.pattern || "";
|
|
475
|
-
const filePath = toolInput?.path || "";
|
|
476
|
-
if (pattern) {
|
|
477
|
-
if (filePath && !isDiscoveryGrep(filePath)) {
|
|
478
|
-
const msg = `STOP: Use Read("${filePath}") to view file content, or mcp__contextstream__search(mode="keyword", query="${pattern}") for codebase search.`;
|
|
479
|
-
if (editorFormat === "cline") {
|
|
480
|
-
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
481
|
-
} else if (editorFormat === "cursor") {
|
|
482
|
-
outputCursorBlock(msg);
|
|
483
|
-
}
|
|
484
|
-
blockClaudeCode(msg);
|
|
485
|
-
} else {
|
|
486
|
-
const msg = `This project index is current. Use mcp__contextstream__search(mode="auto", query="${pattern}") instead of ${tool} for faster, richer code results.`;
|
|
487
|
-
if (editorFormat === "cline") {
|
|
488
|
-
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
489
|
-
} else if (editorFormat === "cursor") {
|
|
490
|
-
outputCursorBlock(msg);
|
|
491
|
-
}
|
|
492
|
-
blockClaudeCode(msg);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
} else if (tool === "Explore") {
|
|
496
|
-
const msg = 'Project index is current. Use mcp__contextstream__search(mode="auto", output_format="paths") instead of Explore for broad discovery.';
|
|
497
|
-
if (editorFormat === "cline") {
|
|
498
|
-
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
499
|
-
} else if (editorFormat === "cursor") {
|
|
500
|
-
outputCursorBlock(msg);
|
|
501
|
-
}
|
|
502
|
-
blockClaudeCode(msg);
|
|
503
|
-
} else if (tool === "Task") {
|
|
504
|
-
const subagentTypeRaw = toolInput?.subagent_type || toolInput?.subagentType || "";
|
|
505
|
-
const subagentType = subagentTypeRaw.toLowerCase();
|
|
506
|
-
if (subagentType.includes("explore")) {
|
|
507
|
-
const msg = 'Project index is current. Use mcp__contextstream__search(mode="auto") instead of Task(Explore) for broad discovery.';
|
|
508
|
-
if (editorFormat === "cline") {
|
|
509
|
-
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
510
|
-
} else if (editorFormat === "cursor") {
|
|
511
|
-
outputCursorBlock(msg);
|
|
512
|
-
}
|
|
513
|
-
blockClaudeCode(msg);
|
|
514
|
-
}
|
|
515
|
-
if (subagentType.includes("plan")) {
|
|
516
|
-
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="...").';
|
|
517
|
-
if (editorFormat === "cline") {
|
|
518
|
-
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream plans for persistence.");
|
|
519
|
-
} else if (editorFormat === "cursor") {
|
|
520
|
-
outputCursorBlock(msg);
|
|
521
|
-
}
|
|
522
|
-
blockClaudeCode(msg);
|
|
523
|
-
}
|
|
524
|
-
} else if (tool === "EnterPlanMode") {
|
|
525
|
-
const msg = 'After finalizing your plan, save it to ContextStream (not a local markdown file): mcp__contextstream__session(action="capture_plan", title="...", steps=[...]). Then create tasks with mcp__contextstream__memory(action="create_task", title="...", plan_id="...").';
|
|
526
|
-
if (editorFormat === "cline") {
|
|
527
|
-
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream plans for persistence.");
|
|
528
|
-
} else if (editorFormat === "cursor") {
|
|
529
|
-
outputCursorBlock(msg);
|
|
530
|
-
}
|
|
531
|
-
blockClaudeCode(msg);
|
|
532
|
-
}
|
|
533
|
-
if (tool === "list_files" || tool === "search_files") {
|
|
534
|
-
const pattern = toolInput?.path || toolInput?.regex || "";
|
|
535
|
-
if (isDiscoveryGlob(pattern) || isDiscoveryGrep(pattern)) {
|
|
536
|
-
const msg = `Project index is current. Use mcp__contextstream__search(mode="auto", query="${pattern}") instead of ${tool} for faster, richer code results.`;
|
|
537
|
-
if (editorFormat === "cline") {
|
|
538
|
-
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
539
|
-
} else if (editorFormat === "cursor") {
|
|
540
|
-
outputCursorBlock(msg);
|
|
541
|
-
}
|
|
542
|
-
blockClaudeCode(msg);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
allowTool(editorFormat, cwd, recordStateChange);
|
|
546
|
-
}
|
|
547
|
-
var isDirectRun = process.argv[1]?.includes("pre-tool-use") || process.argv[2] === "pre-tool-use";
|
|
548
|
-
if (isDirectRun) {
|
|
549
|
-
runPreToolUseHook().catch(() => process.exit(0));
|
|
550
|
-
}
|
|
551
|
-
export {
|
|
552
|
-
runPreToolUseHook
|
|
553
|
-
};
|