@contextstream/mcp-server 0.4.50 → 0.4.51
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/dist/hooks/auto-rules.js +136 -25
- package/dist/hooks/on-bash.js +190 -0
- package/dist/hooks/on-read.js +163 -0
- package/dist/hooks/on-save-intent.js +132 -0
- package/dist/hooks/on-task.js +139 -0
- package/dist/hooks/on-web.js +155 -0
- package/dist/hooks/post-compact.js +172 -0
- package/dist/hooks/runner.js +2889 -0
- package/dist/hooks/session-end.js +191 -0
- package/dist/hooks/session-init.js +174 -0
- package/dist/index.js +1994 -146
- package/dist/test-server.js +3 -0
- package/package.json +7 -4
- package/scripts/postinstall.js +56 -0
|
@@ -0,0 +1,2889 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/hooks/pre-tool-use.ts
|
|
19
|
+
var pre_tool_use_exports = {};
|
|
20
|
+
__export(pre_tool_use_exports, {
|
|
21
|
+
runPreToolUseHook: () => runPreToolUseHook
|
|
22
|
+
});
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
function isDiscoveryGlob(pattern) {
|
|
27
|
+
const patternLower = pattern.toLowerCase();
|
|
28
|
+
for (const p of DISCOVERY_PATTERNS) {
|
|
29
|
+
if (patternLower.includes(p)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (patternLower.startsWith("**/*.") || patternLower.startsWith("**/")) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (patternLower.includes("**") || patternLower.includes("*/")) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function isDiscoveryGrep(filePath) {
|
|
42
|
+
if (!filePath || filePath === "." || filePath === "./" || filePath === "*" || filePath === "**") {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (filePath.includes("*") || filePath.includes("**")) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function isProjectIndexed(cwd) {
|
|
51
|
+
if (!fs.existsSync(INDEX_STATUS_FILE)) {
|
|
52
|
+
return { isIndexed: false, isStale: false };
|
|
53
|
+
}
|
|
54
|
+
let data;
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(INDEX_STATUS_FILE, "utf-8");
|
|
57
|
+
data = JSON.parse(content);
|
|
58
|
+
} catch {
|
|
59
|
+
return { isIndexed: false, isStale: false };
|
|
60
|
+
}
|
|
61
|
+
const projects = data.projects || {};
|
|
62
|
+
const cwdPath = path.resolve(cwd);
|
|
63
|
+
for (const [projectPath, info] of Object.entries(projects)) {
|
|
64
|
+
try {
|
|
65
|
+
const indexedPath = path.resolve(projectPath);
|
|
66
|
+
if (cwdPath === indexedPath || cwdPath.startsWith(indexedPath + path.sep)) {
|
|
67
|
+
const indexedAt = info.indexed_at;
|
|
68
|
+
if (indexedAt) {
|
|
69
|
+
try {
|
|
70
|
+
const indexedTime = new Date(indexedAt);
|
|
71
|
+
const now = /* @__PURE__ */ new Date();
|
|
72
|
+
const diffDays = (now.getTime() - indexedTime.getTime()) / (1e3 * 60 * 60 * 24);
|
|
73
|
+
if (diffDays > STALE_THRESHOLD_DAYS) {
|
|
74
|
+
return { isIndexed: true, isStale: true };
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { isIndexed: true, isStale: false };
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { isIndexed: false, isStale: false };
|
|
86
|
+
}
|
|
87
|
+
function extractCwd(input) {
|
|
88
|
+
if (input.cwd) return input.cwd;
|
|
89
|
+
if (input.workspace_roots?.length) return input.workspace_roots[0];
|
|
90
|
+
if (input.workspaceRoots?.length) return input.workspaceRoots[0];
|
|
91
|
+
return process.cwd();
|
|
92
|
+
}
|
|
93
|
+
function extractToolName(input) {
|
|
94
|
+
return input.tool_name || input.toolName || "";
|
|
95
|
+
}
|
|
96
|
+
function extractToolInput(input) {
|
|
97
|
+
return input.tool_input || input.parameters || input.toolParameters || {};
|
|
98
|
+
}
|
|
99
|
+
function blockClaudeCode(message) {
|
|
100
|
+
console.error(message);
|
|
101
|
+
process.exit(2);
|
|
102
|
+
}
|
|
103
|
+
function outputClineBlock(errorMessage, contextMod) {
|
|
104
|
+
const result = {
|
|
105
|
+
cancel: true,
|
|
106
|
+
errorMessage
|
|
107
|
+
};
|
|
108
|
+
if (contextMod) {
|
|
109
|
+
result.contextModification = contextMod;
|
|
110
|
+
}
|
|
111
|
+
console.log(JSON.stringify(result));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
function outputClineAllow() {
|
|
115
|
+
console.log(JSON.stringify({ cancel: false }));
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
function outputCursorBlock(reason) {
|
|
119
|
+
console.log(JSON.stringify({ decision: "deny", reason }));
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
function outputCursorAllow() {
|
|
123
|
+
console.log(JSON.stringify({ decision: "allow" }));
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
function detectEditorFormat(input) {
|
|
127
|
+
if (input.hookName !== void 0 || input.toolName !== void 0) {
|
|
128
|
+
return "cline";
|
|
129
|
+
}
|
|
130
|
+
if (input.hook_event_name !== void 0) {
|
|
131
|
+
return "cursor";
|
|
132
|
+
}
|
|
133
|
+
return "claude";
|
|
134
|
+
}
|
|
135
|
+
async function runPreToolUseHook() {
|
|
136
|
+
if (!ENABLED) {
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
let inputData = "";
|
|
140
|
+
for await (const chunk of process.stdin) {
|
|
141
|
+
inputData += chunk;
|
|
142
|
+
}
|
|
143
|
+
if (!inputData.trim()) {
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
let input;
|
|
147
|
+
try {
|
|
148
|
+
input = JSON.parse(inputData);
|
|
149
|
+
} catch {
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
const editorFormat = detectEditorFormat(input);
|
|
153
|
+
const cwd = extractCwd(input);
|
|
154
|
+
const tool = extractToolName(input);
|
|
155
|
+
const toolInput = extractToolInput(input);
|
|
156
|
+
const { isIndexed } = isProjectIndexed(cwd);
|
|
157
|
+
if (!isIndexed) {
|
|
158
|
+
if (editorFormat === "cline") {
|
|
159
|
+
outputClineAllow();
|
|
160
|
+
} else if (editorFormat === "cursor") {
|
|
161
|
+
outputCursorAllow();
|
|
162
|
+
}
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
if (tool === "Glob") {
|
|
166
|
+
const pattern = toolInput?.pattern || "";
|
|
167
|
+
if (isDiscoveryGlob(pattern)) {
|
|
168
|
+
const msg = `STOP: Use mcp__contextstream__search(mode="hybrid", query="${pattern}") instead of Glob.`;
|
|
169
|
+
if (editorFormat === "cline") {
|
|
170
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
171
|
+
} else if (editorFormat === "cursor") {
|
|
172
|
+
outputCursorBlock(msg);
|
|
173
|
+
}
|
|
174
|
+
blockClaudeCode(msg);
|
|
175
|
+
}
|
|
176
|
+
} else if (tool === "Grep" || tool === "Search") {
|
|
177
|
+
const pattern = toolInput?.pattern || "";
|
|
178
|
+
const filePath = toolInput?.path || "";
|
|
179
|
+
if (pattern) {
|
|
180
|
+
if (filePath && !isDiscoveryGrep(filePath)) {
|
|
181
|
+
const msg = `STOP: Use Read("${filePath}") to view file content, or mcp__contextstream__search(mode="keyword", query="${pattern}") for codebase search.`;
|
|
182
|
+
if (editorFormat === "cline") {
|
|
183
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
184
|
+
} else if (editorFormat === "cursor") {
|
|
185
|
+
outputCursorBlock(msg);
|
|
186
|
+
}
|
|
187
|
+
blockClaudeCode(msg);
|
|
188
|
+
} else {
|
|
189
|
+
const msg = `STOP: Use mcp__contextstream__search(mode="hybrid", query="${pattern}") instead of ${tool}.`;
|
|
190
|
+
if (editorFormat === "cline") {
|
|
191
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
192
|
+
} else if (editorFormat === "cursor") {
|
|
193
|
+
outputCursorBlock(msg);
|
|
194
|
+
}
|
|
195
|
+
blockClaudeCode(msg);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else if (tool === "Task") {
|
|
199
|
+
const subagentType = toolInput?.subagent_type?.toLowerCase() || "";
|
|
200
|
+
if (subagentType === "explore") {
|
|
201
|
+
const msg = 'STOP: Use mcp__contextstream__search(mode="hybrid") instead of Task(Explore).';
|
|
202
|
+
if (editorFormat === "cline") {
|
|
203
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
204
|
+
} else if (editorFormat === "cursor") {
|
|
205
|
+
outputCursorBlock(msg);
|
|
206
|
+
}
|
|
207
|
+
blockClaudeCode(msg);
|
|
208
|
+
}
|
|
209
|
+
if (subagentType === "plan") {
|
|
210
|
+
const msg = 'STOP: Use mcp__contextstream__session(action="capture_plan") for planning. ContextStream plans persist across sessions.';
|
|
211
|
+
if (editorFormat === "cline") {
|
|
212
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream plans for persistence.");
|
|
213
|
+
} else if (editorFormat === "cursor") {
|
|
214
|
+
outputCursorBlock(msg);
|
|
215
|
+
}
|
|
216
|
+
blockClaudeCode(msg);
|
|
217
|
+
}
|
|
218
|
+
} else if (tool === "EnterPlanMode") {
|
|
219
|
+
const msg = 'STOP: Use mcp__contextstream__session(action="capture_plan", title="...", steps=[...]) instead of EnterPlanMode. ContextStream plans persist across sessions and are searchable.';
|
|
220
|
+
if (editorFormat === "cline") {
|
|
221
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream plans for persistence.");
|
|
222
|
+
} else if (editorFormat === "cursor") {
|
|
223
|
+
outputCursorBlock(msg);
|
|
224
|
+
}
|
|
225
|
+
blockClaudeCode(msg);
|
|
226
|
+
}
|
|
227
|
+
if (tool === "list_files" || tool === "search_files") {
|
|
228
|
+
const pattern = toolInput?.path || toolInput?.regex || "";
|
|
229
|
+
if (isDiscoveryGlob(pattern) || isDiscoveryGrep(pattern)) {
|
|
230
|
+
const msg = `Use mcp__contextstream__search(mode="hybrid", query="${pattern}") instead of ${tool}. ContextStream search is indexed and faster.`;
|
|
231
|
+
if (editorFormat === "cline") {
|
|
232
|
+
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
233
|
+
} else if (editorFormat === "cursor") {
|
|
234
|
+
outputCursorBlock(msg);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (editorFormat === "cline") {
|
|
239
|
+
outputClineAllow();
|
|
240
|
+
} else if (editorFormat === "cursor") {
|
|
241
|
+
outputCursorAllow();
|
|
242
|
+
}
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
245
|
+
var ENABLED, INDEX_STATUS_FILE, STALE_THRESHOLD_DAYS, DISCOVERY_PATTERNS, isDirectRun;
|
|
246
|
+
var init_pre_tool_use = __esm({
|
|
247
|
+
"src/hooks/pre-tool-use.ts"() {
|
|
248
|
+
"use strict";
|
|
249
|
+
ENABLED = process.env.CONTEXTSTREAM_HOOK_ENABLED !== "false";
|
|
250
|
+
INDEX_STATUS_FILE = path.join(homedir(), ".contextstream", "indexed-projects.json");
|
|
251
|
+
STALE_THRESHOLD_DAYS = 7;
|
|
252
|
+
DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"];
|
|
253
|
+
isDirectRun = process.argv[1]?.includes("pre-tool-use") || process.argv[2] === "pre-tool-use";
|
|
254
|
+
if (isDirectRun) {
|
|
255
|
+
runPreToolUseHook().catch(() => process.exit(0));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// src/hooks/user-prompt-submit.ts
|
|
261
|
+
var user_prompt_submit_exports = {};
|
|
262
|
+
__export(user_prompt_submit_exports, {
|
|
263
|
+
runUserPromptSubmitHook: () => runUserPromptSubmitHook
|
|
264
|
+
});
|
|
265
|
+
function detectEditorFormat2(input) {
|
|
266
|
+
if (input.hookName !== void 0) {
|
|
267
|
+
return "cline";
|
|
268
|
+
}
|
|
269
|
+
if (input.hook_event_name === "beforeSubmitPrompt") {
|
|
270
|
+
return "cursor";
|
|
271
|
+
}
|
|
272
|
+
return "claude";
|
|
273
|
+
}
|
|
274
|
+
async function runUserPromptSubmitHook() {
|
|
275
|
+
if (!ENABLED2) {
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
let inputData = "";
|
|
279
|
+
for await (const chunk of process.stdin) {
|
|
280
|
+
inputData += chunk;
|
|
281
|
+
}
|
|
282
|
+
if (!inputData.trim()) {
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
let input;
|
|
286
|
+
try {
|
|
287
|
+
input = JSON.parse(inputData);
|
|
288
|
+
} catch {
|
|
289
|
+
process.exit(0);
|
|
290
|
+
}
|
|
291
|
+
const editorFormat = detectEditorFormat2(input);
|
|
292
|
+
if (editorFormat === "claude") {
|
|
293
|
+
console.log(
|
|
294
|
+
JSON.stringify({
|
|
295
|
+
hookSpecificOutput: {
|
|
296
|
+
hookEventName: "UserPromptSubmit",
|
|
297
|
+
additionalContext: REMINDER
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
} else if (editorFormat === "cline") {
|
|
302
|
+
console.log(
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
cancel: false,
|
|
305
|
+
contextModification: REMINDER
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
} else if (editorFormat === "cursor") {
|
|
309
|
+
console.log(
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
continue: true,
|
|
312
|
+
user_message: "[CONTEXTSTREAM] Search with mcp__contextstream__search before using Glob/Grep/Read"
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
var ENABLED2, REMINDER, isDirectRun2;
|
|
319
|
+
var init_user_prompt_submit = __esm({
|
|
320
|
+
"src/hooks/user-prompt-submit.ts"() {
|
|
321
|
+
"use strict";
|
|
322
|
+
ENABLED2 = process.env.CONTEXTSTREAM_REMINDER_ENABLED !== "false";
|
|
323
|
+
REMINDER = `[CONTEXTSTREAM - DO THIS FIRST]
|
|
324
|
+
mcp__contextstream__context(user_message="<user's message>") - CALL THIS BEFORE ANY OTHER TOOL
|
|
325
|
+
|
|
326
|
+
Why: Delivers task-specific rules, lessons from past mistakes, and relevant context.
|
|
327
|
+
Skip it = repeat past mistakes, miss relevant decisions, fly blind.
|
|
328
|
+
|
|
329
|
+
BEFORE Glob/Grep/Read/Search/Explore:
|
|
330
|
+
mcp__contextstream__search(mode="hybrid", query="...") FIRST
|
|
331
|
+
Local tools ONLY if search returns 0 results.
|
|
332
|
+
[END]`;
|
|
333
|
+
isDirectRun2 = process.argv[1]?.includes("user-prompt-submit") || process.argv[2] === "user-prompt-submit";
|
|
334
|
+
if (isDirectRun2) {
|
|
335
|
+
runUserPromptSubmitHook().catch(() => process.exit(0));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// src/hooks/media-aware.ts
|
|
341
|
+
var media_aware_exports = {};
|
|
342
|
+
__export(media_aware_exports, {
|
|
343
|
+
runMediaAwareHook: () => runMediaAwareHook
|
|
344
|
+
});
|
|
345
|
+
function matchesMediaPattern(text) {
|
|
346
|
+
return PATTERNS.some((pattern) => pattern.test(text));
|
|
347
|
+
}
|
|
348
|
+
function extractPrompt(input) {
|
|
349
|
+
if (input.prompt) {
|
|
350
|
+
return input.prompt;
|
|
351
|
+
}
|
|
352
|
+
if (input.session?.messages) {
|
|
353
|
+
for (let i = input.session.messages.length - 1; i >= 0; i--) {
|
|
354
|
+
const msg = input.session.messages[i];
|
|
355
|
+
if (msg.role === "user") {
|
|
356
|
+
if (typeof msg.content === "string") {
|
|
357
|
+
return msg.content;
|
|
358
|
+
}
|
|
359
|
+
if (Array.isArray(msg.content)) {
|
|
360
|
+
for (const block of msg.content) {
|
|
361
|
+
if (block.type === "text" && block.text) {
|
|
362
|
+
return block.text;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return "";
|
|
371
|
+
}
|
|
372
|
+
function detectEditorFormat3(input) {
|
|
373
|
+
if (input.hookName !== void 0) {
|
|
374
|
+
return "cline";
|
|
375
|
+
}
|
|
376
|
+
return "claude";
|
|
377
|
+
}
|
|
378
|
+
async function runMediaAwareHook() {
|
|
379
|
+
if (!ENABLED3) {
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}
|
|
382
|
+
let inputData = "";
|
|
383
|
+
for await (const chunk of process.stdin) {
|
|
384
|
+
inputData += chunk;
|
|
385
|
+
}
|
|
386
|
+
if (!inputData.trim()) {
|
|
387
|
+
process.exit(0);
|
|
388
|
+
}
|
|
389
|
+
let input;
|
|
390
|
+
try {
|
|
391
|
+
input = JSON.parse(inputData);
|
|
392
|
+
} catch {
|
|
393
|
+
process.exit(0);
|
|
394
|
+
}
|
|
395
|
+
const prompt = extractPrompt(input);
|
|
396
|
+
if (!prompt || !matchesMediaPattern(prompt)) {
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
const editorFormat = detectEditorFormat3(input);
|
|
400
|
+
if (editorFormat === "claude") {
|
|
401
|
+
console.log(
|
|
402
|
+
JSON.stringify({
|
|
403
|
+
hookSpecificOutput: {
|
|
404
|
+
hookEventName: "UserPromptSubmit",
|
|
405
|
+
additionalContext: MEDIA_CONTEXT
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
} else {
|
|
410
|
+
console.log(
|
|
411
|
+
JSON.stringify({
|
|
412
|
+
cancel: false,
|
|
413
|
+
contextModification: MEDIA_CONTEXT
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
process.exit(0);
|
|
418
|
+
}
|
|
419
|
+
var ENABLED3, PATTERNS, MEDIA_CONTEXT, isDirectRun3;
|
|
420
|
+
var init_media_aware = __esm({
|
|
421
|
+
"src/hooks/media-aware.ts"() {
|
|
422
|
+
"use strict";
|
|
423
|
+
ENABLED3 = process.env.CONTEXTSTREAM_MEDIA_HOOK_ENABLED !== "false";
|
|
424
|
+
PATTERNS = [
|
|
425
|
+
/\b(video|videos|clip|clips|footage|keyframe)s?\b/i,
|
|
426
|
+
/\b(remotion|timeline|video\s*edit)\b/i,
|
|
427
|
+
/\b(image|images|photo|photos|picture|thumbnail)s?\b/i,
|
|
428
|
+
/\b(audio|podcast|transcript|transcription|voice)\b/i,
|
|
429
|
+
/\b(media|asset|assets|creative|b-roll)\b/i,
|
|
430
|
+
/\b(find|search|show).*(clip|video|image|audio|footage|media)\b/i
|
|
431
|
+
];
|
|
432
|
+
MEDIA_CONTEXT = `[MEDIA TOOLS AVAILABLE]
|
|
433
|
+
Your workspace may have indexed media. Use ContextStream media tools:
|
|
434
|
+
|
|
435
|
+
- **Search**: \`mcp__contextstream__media(action="search", query="description")\`
|
|
436
|
+
- **Get clip**: \`mcp__contextstream__media(action="get_clip", content_id="...", start="1:34", end="2:15", output_format="remotion|ffmpeg|raw")\`
|
|
437
|
+
- **List assets**: \`mcp__contextstream__media(action="list")\`
|
|
438
|
+
- **Index**: \`mcp__contextstream__media(action="index", file_path="...", content_type="video|audio|image|document")\`
|
|
439
|
+
|
|
440
|
+
For Remotion: use \`output_format="remotion"\` to get frame-based props.
|
|
441
|
+
[END MEDIA TOOLS]`;
|
|
442
|
+
isDirectRun3 = process.argv[1]?.includes("media-aware") || process.argv[2] === "media-aware";
|
|
443
|
+
if (isDirectRun3) {
|
|
444
|
+
runMediaAwareHook().catch(() => process.exit(0));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// src/hooks/pre-compact.ts
|
|
450
|
+
var pre_compact_exports = {};
|
|
451
|
+
__export(pre_compact_exports, {
|
|
452
|
+
runPreCompactHook: () => runPreCompactHook
|
|
453
|
+
});
|
|
454
|
+
import * as fs2 from "node:fs";
|
|
455
|
+
import * as path2 from "node:path";
|
|
456
|
+
import { homedir as homedir2 } from "node:os";
|
|
457
|
+
function loadConfigFromMcpJson(cwd) {
|
|
458
|
+
let searchDir = path2.resolve(cwd);
|
|
459
|
+
for (let i = 0; i < 5; i++) {
|
|
460
|
+
if (!API_KEY) {
|
|
461
|
+
const mcpPath = path2.join(searchDir, ".mcp.json");
|
|
462
|
+
if (fs2.existsSync(mcpPath)) {
|
|
463
|
+
try {
|
|
464
|
+
const content = fs2.readFileSync(mcpPath, "utf-8");
|
|
465
|
+
const config = JSON.parse(content);
|
|
466
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
467
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
468
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
469
|
+
}
|
|
470
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
471
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (!WORKSPACE_ID) {
|
|
478
|
+
const csConfigPath = path2.join(searchDir, ".contextstream", "config.json");
|
|
479
|
+
if (fs2.existsSync(csConfigPath)) {
|
|
480
|
+
try {
|
|
481
|
+
const content = fs2.readFileSync(csConfigPath, "utf-8");
|
|
482
|
+
const csConfig = JSON.parse(content);
|
|
483
|
+
if (csConfig.workspace_id) {
|
|
484
|
+
WORKSPACE_ID = csConfig.workspace_id;
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const parentDir = path2.dirname(searchDir);
|
|
491
|
+
if (parentDir === searchDir) break;
|
|
492
|
+
searchDir = parentDir;
|
|
493
|
+
}
|
|
494
|
+
if (!API_KEY) {
|
|
495
|
+
const homeMcpPath = path2.join(homedir2(), ".mcp.json");
|
|
496
|
+
if (fs2.existsSync(homeMcpPath)) {
|
|
497
|
+
try {
|
|
498
|
+
const content = fs2.readFileSync(homeMcpPath, "utf-8");
|
|
499
|
+
const config = JSON.parse(content);
|
|
500
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
501
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
502
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
503
|
+
}
|
|
504
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
505
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function parseTranscript(transcriptPath) {
|
|
513
|
+
const activeFiles = /* @__PURE__ */ new Set();
|
|
514
|
+
const recentMessages = [];
|
|
515
|
+
const toolCalls = [];
|
|
516
|
+
const messages = [];
|
|
517
|
+
let startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
518
|
+
let firstTimestamp = true;
|
|
519
|
+
try {
|
|
520
|
+
const content = fs2.readFileSync(transcriptPath, "utf-8");
|
|
521
|
+
const lines = content.split("\n");
|
|
522
|
+
for (const line of lines) {
|
|
523
|
+
if (!line.trim()) continue;
|
|
524
|
+
try {
|
|
525
|
+
const entry = JSON.parse(line);
|
|
526
|
+
const msgType = entry.type || "";
|
|
527
|
+
const timestamp = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
528
|
+
if (firstTimestamp && entry.timestamp) {
|
|
529
|
+
startedAt = entry.timestamp;
|
|
530
|
+
firstTimestamp = false;
|
|
531
|
+
}
|
|
532
|
+
if (msgType === "tool_use") {
|
|
533
|
+
const toolName = entry.name || "";
|
|
534
|
+
const toolInput = entry.input || {};
|
|
535
|
+
toolCalls.push({ name: toolName, input: toolInput });
|
|
536
|
+
if (["Read", "Write", "Edit", "NotebookEdit"].includes(toolName)) {
|
|
537
|
+
const filePath = toolInput.file_path || toolInput.notebook_path;
|
|
538
|
+
if (filePath) {
|
|
539
|
+
activeFiles.add(filePath);
|
|
540
|
+
}
|
|
541
|
+
} else if (toolName === "Glob") {
|
|
542
|
+
const pattern = toolInput.pattern;
|
|
543
|
+
if (pattern) {
|
|
544
|
+
activeFiles.add(`[glob:${pattern}]`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
messages.push({
|
|
548
|
+
role: "assistant",
|
|
549
|
+
content: `[Tool: ${toolName}]`,
|
|
550
|
+
timestamp,
|
|
551
|
+
tool_calls: { name: toolName, input: toolInput }
|
|
552
|
+
});
|
|
553
|
+
} else if (msgType === "tool_result") {
|
|
554
|
+
const resultContent = typeof entry.content === "string" ? entry.content.slice(0, 2e3) : JSON.stringify(entry.content || {}).slice(0, 2e3);
|
|
555
|
+
messages.push({
|
|
556
|
+
role: "tool",
|
|
557
|
+
content: resultContent,
|
|
558
|
+
timestamp,
|
|
559
|
+
tool_results: { name: entry.name }
|
|
560
|
+
});
|
|
561
|
+
} else if (msgType === "user" || entry.role === "user") {
|
|
562
|
+
const userContent = typeof entry.content === "string" ? entry.content : "";
|
|
563
|
+
if (userContent) {
|
|
564
|
+
messages.push({
|
|
565
|
+
role: "user",
|
|
566
|
+
content: userContent,
|
|
567
|
+
timestamp
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
} else if (msgType === "assistant" || entry.role === "assistant") {
|
|
571
|
+
const assistantContent = typeof entry.content === "string" ? entry.content : "";
|
|
572
|
+
if (assistantContent) {
|
|
573
|
+
messages.push({
|
|
574
|
+
role: "assistant",
|
|
575
|
+
content: assistantContent,
|
|
576
|
+
timestamp
|
|
577
|
+
});
|
|
578
|
+
if (assistantContent.length > 50) {
|
|
579
|
+
recentMessages.push(assistantContent.slice(0, 500));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
activeFiles: Array.from(activeFiles).slice(-20),
|
|
591
|
+
// Last 20 files
|
|
592
|
+
toolCallCount: toolCalls.length,
|
|
593
|
+
messageCount: messages.length,
|
|
594
|
+
lastTools: toolCalls.slice(-10).map((t) => t.name),
|
|
595
|
+
// Last 10 tool names
|
|
596
|
+
messages,
|
|
597
|
+
startedAt
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
async function saveFullTranscript(sessionId, transcriptData, trigger) {
|
|
601
|
+
if (!API_KEY) {
|
|
602
|
+
return { success: false, message: "No API key configured" };
|
|
603
|
+
}
|
|
604
|
+
if (transcriptData.messages.length === 0) {
|
|
605
|
+
return { success: false, message: "No messages to save" };
|
|
606
|
+
}
|
|
607
|
+
const payload = {
|
|
608
|
+
session_id: sessionId,
|
|
609
|
+
messages: transcriptData.messages,
|
|
610
|
+
started_at: transcriptData.startedAt,
|
|
611
|
+
source_type: "pre_compact",
|
|
612
|
+
title: `Pre-compaction save (${trigger})`,
|
|
613
|
+
metadata: {
|
|
614
|
+
trigger,
|
|
615
|
+
active_files: transcriptData.activeFiles,
|
|
616
|
+
tool_call_count: transcriptData.toolCallCount
|
|
617
|
+
},
|
|
618
|
+
tags: ["pre_compaction", trigger]
|
|
619
|
+
};
|
|
620
|
+
if (WORKSPACE_ID) {
|
|
621
|
+
payload.workspace_id = WORKSPACE_ID;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
const controller = new AbortController();
|
|
625
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
626
|
+
const response = await fetch(`${API_URL}/api/v1/transcripts`, {
|
|
627
|
+
method: "POST",
|
|
628
|
+
headers: {
|
|
629
|
+
"Content-Type": "application/json",
|
|
630
|
+
"X-API-Key": API_KEY
|
|
631
|
+
},
|
|
632
|
+
body: JSON.stringify(payload),
|
|
633
|
+
signal: controller.signal
|
|
634
|
+
});
|
|
635
|
+
clearTimeout(timeoutId);
|
|
636
|
+
if (response.ok) {
|
|
637
|
+
return { success: true, message: `Transcript saved (${transcriptData.messages.length} messages)` };
|
|
638
|
+
}
|
|
639
|
+
return { success: false, message: `API error: ${response.status}` };
|
|
640
|
+
} catch (error) {
|
|
641
|
+
return { success: false, message: String(error) };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async function saveSnapshot(sessionId, transcriptData, trigger) {
|
|
645
|
+
if (!API_KEY) {
|
|
646
|
+
return { success: false, message: "No API key configured" };
|
|
647
|
+
}
|
|
648
|
+
const snapshotContent = {
|
|
649
|
+
session_id: sessionId,
|
|
650
|
+
trigger,
|
|
651
|
+
captured_at: null,
|
|
652
|
+
// API will set timestamp
|
|
653
|
+
active_files: transcriptData.activeFiles,
|
|
654
|
+
tool_call_count: transcriptData.toolCallCount,
|
|
655
|
+
last_tools: transcriptData.lastTools,
|
|
656
|
+
auto_captured: true
|
|
657
|
+
};
|
|
658
|
+
const payload = {
|
|
659
|
+
event_type: "session_snapshot",
|
|
660
|
+
title: `Auto Pre-compaction Snapshot (${trigger})`,
|
|
661
|
+
content: JSON.stringify(snapshotContent),
|
|
662
|
+
importance: "high",
|
|
663
|
+
tags: ["session_snapshot", "pre_compaction", "auto_captured"],
|
|
664
|
+
source_type: "hook"
|
|
665
|
+
};
|
|
666
|
+
if (WORKSPACE_ID) {
|
|
667
|
+
payload.workspace_id = WORKSPACE_ID;
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
const controller = new AbortController();
|
|
671
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
672
|
+
const response = await fetch(`${API_URL}/api/v1/memory/events`, {
|
|
673
|
+
method: "POST",
|
|
674
|
+
headers: {
|
|
675
|
+
"Content-Type": "application/json",
|
|
676
|
+
"X-API-Key": API_KEY
|
|
677
|
+
},
|
|
678
|
+
body: JSON.stringify(payload),
|
|
679
|
+
signal: controller.signal
|
|
680
|
+
});
|
|
681
|
+
clearTimeout(timeoutId);
|
|
682
|
+
if (response.ok) {
|
|
683
|
+
return { success: true, message: "Snapshot saved" };
|
|
684
|
+
}
|
|
685
|
+
return { success: false, message: `API error: ${response.status}` };
|
|
686
|
+
} catch (error) {
|
|
687
|
+
return { success: false, message: String(error) };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async function runPreCompactHook() {
|
|
691
|
+
if (!ENABLED4) {
|
|
692
|
+
process.exit(0);
|
|
693
|
+
}
|
|
694
|
+
let inputData = "";
|
|
695
|
+
for await (const chunk of process.stdin) {
|
|
696
|
+
inputData += chunk;
|
|
697
|
+
}
|
|
698
|
+
if (!inputData.trim()) {
|
|
699
|
+
process.exit(0);
|
|
700
|
+
}
|
|
701
|
+
let input;
|
|
702
|
+
try {
|
|
703
|
+
input = JSON.parse(inputData);
|
|
704
|
+
} catch {
|
|
705
|
+
process.exit(0);
|
|
706
|
+
}
|
|
707
|
+
const cwd = input.cwd || process.cwd();
|
|
708
|
+
loadConfigFromMcpJson(cwd);
|
|
709
|
+
const sessionId = input.session_id || "unknown";
|
|
710
|
+
const transcriptPath = input.transcript_path || "";
|
|
711
|
+
const trigger = input.trigger || "unknown";
|
|
712
|
+
const customInstructions = input.custom_instructions || "";
|
|
713
|
+
let transcriptData = {
|
|
714
|
+
activeFiles: [],
|
|
715
|
+
toolCallCount: 0,
|
|
716
|
+
messageCount: 0,
|
|
717
|
+
lastTools: []
|
|
718
|
+
};
|
|
719
|
+
if (transcriptPath && fs2.existsSync(transcriptPath)) {
|
|
720
|
+
transcriptData = parseTranscript(transcriptPath);
|
|
721
|
+
}
|
|
722
|
+
let autoSaveStatus = "";
|
|
723
|
+
if (AUTO_SAVE && API_KEY) {
|
|
724
|
+
const transcriptResult = await saveFullTranscript(sessionId, transcriptData, trigger);
|
|
725
|
+
if (transcriptResult.success) {
|
|
726
|
+
autoSaveStatus = `
|
|
727
|
+
[ContextStream: ${transcriptResult.message}]`;
|
|
728
|
+
} else {
|
|
729
|
+
const { success, message } = await saveSnapshot(sessionId, transcriptData, trigger);
|
|
730
|
+
if (success) {
|
|
731
|
+
autoSaveStatus = `
|
|
732
|
+
[ContextStream: Auto-saved snapshot with ${transcriptData.activeFiles.length} active files (transcript save failed: ${transcriptResult.message})]`;
|
|
733
|
+
} else {
|
|
734
|
+
autoSaveStatus = `
|
|
735
|
+
[ContextStream: Auto-save failed - ${message}]`;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const filesList = transcriptData.activeFiles.slice(0, 5).join(", ") || "none detected";
|
|
740
|
+
const context = `[CONTEXT COMPACTION - ${trigger.toUpperCase()}]${autoSaveStatus}
|
|
741
|
+
|
|
742
|
+
Active files detected: ${filesList}
|
|
743
|
+
Tool calls in session: ${transcriptData.toolCallCount}
|
|
744
|
+
|
|
745
|
+
After compaction, call session_init(is_post_compact=true) to restore context.${customInstructions ? `
|
|
746
|
+
User instructions: ${customInstructions}` : ""}`;
|
|
747
|
+
console.log(
|
|
748
|
+
JSON.stringify({
|
|
749
|
+
hookSpecificOutput: {
|
|
750
|
+
hookEventName: "PreCompact",
|
|
751
|
+
additionalContext: context
|
|
752
|
+
}
|
|
753
|
+
})
|
|
754
|
+
);
|
|
755
|
+
process.exit(0);
|
|
756
|
+
}
|
|
757
|
+
var ENABLED4, AUTO_SAVE, API_URL, API_KEY, WORKSPACE_ID, isDirectRun4;
|
|
758
|
+
var init_pre_compact = __esm({
|
|
759
|
+
"src/hooks/pre-compact.ts"() {
|
|
760
|
+
"use strict";
|
|
761
|
+
ENABLED4 = process.env.CONTEXTSTREAM_PRECOMPACT_ENABLED !== "false";
|
|
762
|
+
AUTO_SAVE = process.env.CONTEXTSTREAM_PRECOMPACT_AUTO_SAVE !== "false";
|
|
763
|
+
API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
764
|
+
API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
765
|
+
WORKSPACE_ID = null;
|
|
766
|
+
isDirectRun4 = process.argv[1]?.includes("pre-compact") || process.argv[2] === "pre-compact";
|
|
767
|
+
if (isDirectRun4) {
|
|
768
|
+
runPreCompactHook().catch(() => process.exit(0));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// src/hooks/post-write.ts
|
|
774
|
+
var post_write_exports = {};
|
|
775
|
+
__export(post_write_exports, {
|
|
776
|
+
runPostWriteHook: () => runPostWriteHook
|
|
777
|
+
});
|
|
778
|
+
import * as fs3 from "node:fs";
|
|
779
|
+
import * as path3 from "node:path";
|
|
780
|
+
import { homedir as homedir3 } from "node:os";
|
|
781
|
+
function extractFilePath(input) {
|
|
782
|
+
if (input.tool_input) {
|
|
783
|
+
const filePath = input.tool_input.file_path || input.tool_input.notebook_path || input.tool_input.path;
|
|
784
|
+
if (filePath) return filePath;
|
|
785
|
+
}
|
|
786
|
+
if (input.parameters) {
|
|
787
|
+
const filePath = input.parameters.path || input.parameters.file_path;
|
|
788
|
+
if (filePath) return filePath;
|
|
789
|
+
}
|
|
790
|
+
if (input.toolParameters?.path) {
|
|
791
|
+
return input.toolParameters.path;
|
|
792
|
+
}
|
|
793
|
+
if (input.file_path) {
|
|
794
|
+
return input.file_path;
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
function extractCwd2(input) {
|
|
799
|
+
if (input.cwd) return input.cwd;
|
|
800
|
+
if (input.workspace_roots?.length) return input.workspace_roots[0];
|
|
801
|
+
if (input.workspaceRoots?.length) return input.workspaceRoots[0];
|
|
802
|
+
return process.cwd();
|
|
803
|
+
}
|
|
804
|
+
function findLocalConfig(startDir) {
|
|
805
|
+
let currentDir = path3.resolve(startDir);
|
|
806
|
+
for (let i = 0; i < 10; i++) {
|
|
807
|
+
const configPath = path3.join(currentDir, ".contextstream", "config.json");
|
|
808
|
+
if (fs3.existsSync(configPath)) {
|
|
809
|
+
try {
|
|
810
|
+
const content = fs3.readFileSync(configPath, "utf-8");
|
|
811
|
+
return JSON.parse(content);
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const parentDir = path3.dirname(currentDir);
|
|
816
|
+
if (parentDir === currentDir) break;
|
|
817
|
+
currentDir = parentDir;
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
function loadApiConfig(startDir) {
|
|
822
|
+
let apiUrl = API_URL2;
|
|
823
|
+
let apiKey = API_KEY2;
|
|
824
|
+
if (apiKey) {
|
|
825
|
+
return { apiUrl, apiKey };
|
|
826
|
+
}
|
|
827
|
+
let currentDir = path3.resolve(startDir);
|
|
828
|
+
for (let i = 0; i < 10; i++) {
|
|
829
|
+
const mcpPath = path3.join(currentDir, ".mcp.json");
|
|
830
|
+
if (fs3.existsSync(mcpPath)) {
|
|
831
|
+
try {
|
|
832
|
+
const content = fs3.readFileSync(mcpPath, "utf-8");
|
|
833
|
+
const config = JSON.parse(content);
|
|
834
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
835
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
836
|
+
apiKey = csEnv.CONTEXTSTREAM_API_KEY;
|
|
837
|
+
}
|
|
838
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
839
|
+
apiUrl = csEnv.CONTEXTSTREAM_API_URL;
|
|
840
|
+
}
|
|
841
|
+
if (apiKey) break;
|
|
842
|
+
} catch {
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const parentDir = path3.dirname(currentDir);
|
|
846
|
+
if (parentDir === currentDir) break;
|
|
847
|
+
currentDir = parentDir;
|
|
848
|
+
}
|
|
849
|
+
if (!apiKey) {
|
|
850
|
+
const homeMcpPath = path3.join(homedir3(), ".mcp.json");
|
|
851
|
+
if (fs3.existsSync(homeMcpPath)) {
|
|
852
|
+
try {
|
|
853
|
+
const content = fs3.readFileSync(homeMcpPath, "utf-8");
|
|
854
|
+
const config = JSON.parse(content);
|
|
855
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
856
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
857
|
+
apiKey = csEnv.CONTEXTSTREAM_API_KEY;
|
|
858
|
+
}
|
|
859
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
860
|
+
apiUrl = csEnv.CONTEXTSTREAM_API_URL;
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return { apiUrl, apiKey };
|
|
867
|
+
}
|
|
868
|
+
function shouldIndexFile(filePath) {
|
|
869
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
870
|
+
if (!INDEXABLE_EXTENSIONS.has(ext)) {
|
|
871
|
+
const basename2 = path3.basename(filePath).toLowerCase();
|
|
872
|
+
if (!["dockerfile", "makefile", "rakefile", "gemfile", "procfile"].includes(basename2)) {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
const stats = fs3.statSync(filePath);
|
|
878
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
} catch {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
function detectLanguage(filePath) {
|
|
887
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
888
|
+
const langMap = {
|
|
889
|
+
".ts": "typescript",
|
|
890
|
+
".tsx": "typescript",
|
|
891
|
+
".js": "javascript",
|
|
892
|
+
".jsx": "javascript",
|
|
893
|
+
".mjs": "javascript",
|
|
894
|
+
".cjs": "javascript",
|
|
895
|
+
".py": "python",
|
|
896
|
+
".pyw": "python",
|
|
897
|
+
".rs": "rust",
|
|
898
|
+
".go": "go",
|
|
899
|
+
".java": "java",
|
|
900
|
+
".kt": "kotlin",
|
|
901
|
+
".scala": "scala",
|
|
902
|
+
".c": "c",
|
|
903
|
+
".cpp": "cpp",
|
|
904
|
+
".cc": "cpp",
|
|
905
|
+
".cxx": "cpp",
|
|
906
|
+
".h": "c",
|
|
907
|
+
".hpp": "cpp",
|
|
908
|
+
".cs": "csharp",
|
|
909
|
+
".fs": "fsharp",
|
|
910
|
+
".vb": "vb",
|
|
911
|
+
".rb": "ruby",
|
|
912
|
+
".php": "php",
|
|
913
|
+
".pl": "perl",
|
|
914
|
+
".pm": "perl",
|
|
915
|
+
".swift": "swift",
|
|
916
|
+
".m": "objective-c",
|
|
917
|
+
".mm": "objective-cpp",
|
|
918
|
+
".lua": "lua",
|
|
919
|
+
".r": "r",
|
|
920
|
+
".jl": "julia",
|
|
921
|
+
".sh": "shell",
|
|
922
|
+
".bash": "shell",
|
|
923
|
+
".zsh": "shell",
|
|
924
|
+
".fish": "shell",
|
|
925
|
+
".sql": "sql",
|
|
926
|
+
".graphql": "graphql",
|
|
927
|
+
".gql": "graphql",
|
|
928
|
+
".html": "html",
|
|
929
|
+
".htm": "html",
|
|
930
|
+
".css": "css",
|
|
931
|
+
".scss": "scss",
|
|
932
|
+
".sass": "sass",
|
|
933
|
+
".less": "less",
|
|
934
|
+
".json": "json",
|
|
935
|
+
".yaml": "yaml",
|
|
936
|
+
".yml": "yaml",
|
|
937
|
+
".toml": "toml",
|
|
938
|
+
".xml": "xml",
|
|
939
|
+
".ini": "ini",
|
|
940
|
+
".cfg": "ini",
|
|
941
|
+
".md": "markdown",
|
|
942
|
+
".mdx": "mdx",
|
|
943
|
+
".txt": "text",
|
|
944
|
+
".rst": "rst",
|
|
945
|
+
".vue": "vue",
|
|
946
|
+
".svelte": "svelte",
|
|
947
|
+
".astro": "astro",
|
|
948
|
+
".tf": "terraform",
|
|
949
|
+
".hcl": "hcl",
|
|
950
|
+
".prisma": "prisma",
|
|
951
|
+
".proto": "protobuf"
|
|
952
|
+
};
|
|
953
|
+
return langMap[ext] || "text";
|
|
954
|
+
}
|
|
955
|
+
async function indexFile(filePath, projectId, apiUrl, apiKey, projectRoot) {
|
|
956
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
957
|
+
const relativePath = path3.relative(projectRoot, filePath);
|
|
958
|
+
const payload = {
|
|
959
|
+
files: [
|
|
960
|
+
{
|
|
961
|
+
path: relativePath,
|
|
962
|
+
content,
|
|
963
|
+
language: detectLanguage(filePath)
|
|
964
|
+
}
|
|
965
|
+
]
|
|
966
|
+
};
|
|
967
|
+
const response = await fetch(`${apiUrl}/api/v1/projects/${projectId}/files/ingest`, {
|
|
968
|
+
method: "POST",
|
|
969
|
+
headers: {
|
|
970
|
+
"Content-Type": "application/json",
|
|
971
|
+
"X-API-Key": apiKey
|
|
972
|
+
},
|
|
973
|
+
body: JSON.stringify(payload),
|
|
974
|
+
signal: AbortSignal.timeout(1e4)
|
|
975
|
+
// 10 second timeout
|
|
976
|
+
});
|
|
977
|
+
if (!response.ok) {
|
|
978
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function findProjectRoot(filePath) {
|
|
982
|
+
let currentDir = path3.dirname(path3.resolve(filePath));
|
|
983
|
+
for (let i = 0; i < 10; i++) {
|
|
984
|
+
const configPath = path3.join(currentDir, ".contextstream", "config.json");
|
|
985
|
+
if (fs3.existsSync(configPath)) {
|
|
986
|
+
return currentDir;
|
|
987
|
+
}
|
|
988
|
+
const parentDir = path3.dirname(currentDir);
|
|
989
|
+
if (parentDir === currentDir) break;
|
|
990
|
+
currentDir = parentDir;
|
|
991
|
+
}
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
async function runPostWriteHook() {
|
|
995
|
+
if (!ENABLED5) {
|
|
996
|
+
process.exit(0);
|
|
997
|
+
}
|
|
998
|
+
let inputData = "";
|
|
999
|
+
for await (const chunk of process.stdin) {
|
|
1000
|
+
inputData += chunk;
|
|
1001
|
+
}
|
|
1002
|
+
if (!inputData.trim()) {
|
|
1003
|
+
process.exit(0);
|
|
1004
|
+
}
|
|
1005
|
+
let input;
|
|
1006
|
+
try {
|
|
1007
|
+
input = JSON.parse(inputData);
|
|
1008
|
+
} catch {
|
|
1009
|
+
process.exit(0);
|
|
1010
|
+
}
|
|
1011
|
+
const filePath = extractFilePath(input);
|
|
1012
|
+
if (!filePath) {
|
|
1013
|
+
process.exit(0);
|
|
1014
|
+
}
|
|
1015
|
+
const cwd = extractCwd2(input);
|
|
1016
|
+
const absolutePath = path3.isAbsolute(filePath) ? filePath : path3.resolve(cwd, filePath);
|
|
1017
|
+
if (!fs3.existsSync(absolutePath) || !shouldIndexFile(absolutePath)) {
|
|
1018
|
+
process.exit(0);
|
|
1019
|
+
}
|
|
1020
|
+
const projectRoot = findProjectRoot(absolutePath);
|
|
1021
|
+
if (!projectRoot) {
|
|
1022
|
+
process.exit(0);
|
|
1023
|
+
}
|
|
1024
|
+
const localConfig = findLocalConfig(projectRoot);
|
|
1025
|
+
if (!localConfig?.project_id) {
|
|
1026
|
+
process.exit(0);
|
|
1027
|
+
}
|
|
1028
|
+
const { apiUrl, apiKey } = loadApiConfig(projectRoot);
|
|
1029
|
+
if (!apiKey) {
|
|
1030
|
+
process.exit(0);
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
await indexFile(absolutePath, localConfig.project_id, apiUrl, apiKey, projectRoot);
|
|
1034
|
+
} catch {
|
|
1035
|
+
}
|
|
1036
|
+
process.exit(0);
|
|
1037
|
+
}
|
|
1038
|
+
var API_URL2, API_KEY2, ENABLED5, INDEXABLE_EXTENSIONS, MAX_FILE_SIZE, isDirectRun5;
|
|
1039
|
+
var init_post_write = __esm({
|
|
1040
|
+
"src/hooks/post-write.ts"() {
|
|
1041
|
+
"use strict";
|
|
1042
|
+
API_URL2 = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
1043
|
+
API_KEY2 = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
1044
|
+
ENABLED5 = process.env.CONTEXTSTREAM_POSTWRITE_ENABLED !== "false";
|
|
1045
|
+
INDEXABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1046
|
+
".ts",
|
|
1047
|
+
".tsx",
|
|
1048
|
+
".js",
|
|
1049
|
+
".jsx",
|
|
1050
|
+
".mjs",
|
|
1051
|
+
".cjs",
|
|
1052
|
+
".py",
|
|
1053
|
+
".pyw",
|
|
1054
|
+
".rs",
|
|
1055
|
+
".go",
|
|
1056
|
+
".java",
|
|
1057
|
+
".kt",
|
|
1058
|
+
".scala",
|
|
1059
|
+
".c",
|
|
1060
|
+
".cpp",
|
|
1061
|
+
".cc",
|
|
1062
|
+
".cxx",
|
|
1063
|
+
".h",
|
|
1064
|
+
".hpp",
|
|
1065
|
+
".cs",
|
|
1066
|
+
".fs",
|
|
1067
|
+
".vb",
|
|
1068
|
+
".rb",
|
|
1069
|
+
".php",
|
|
1070
|
+
".pl",
|
|
1071
|
+
".pm",
|
|
1072
|
+
".swift",
|
|
1073
|
+
".m",
|
|
1074
|
+
".mm",
|
|
1075
|
+
".lua",
|
|
1076
|
+
".r",
|
|
1077
|
+
".jl",
|
|
1078
|
+
".sh",
|
|
1079
|
+
".bash",
|
|
1080
|
+
".zsh",
|
|
1081
|
+
".fish",
|
|
1082
|
+
".sql",
|
|
1083
|
+
".graphql",
|
|
1084
|
+
".gql",
|
|
1085
|
+
".html",
|
|
1086
|
+
".htm",
|
|
1087
|
+
".css",
|
|
1088
|
+
".scss",
|
|
1089
|
+
".sass",
|
|
1090
|
+
".less",
|
|
1091
|
+
".json",
|
|
1092
|
+
".yaml",
|
|
1093
|
+
".yml",
|
|
1094
|
+
".toml",
|
|
1095
|
+
".xml",
|
|
1096
|
+
".ini",
|
|
1097
|
+
".cfg",
|
|
1098
|
+
".md",
|
|
1099
|
+
".mdx",
|
|
1100
|
+
".txt",
|
|
1101
|
+
".rst",
|
|
1102
|
+
".vue",
|
|
1103
|
+
".svelte",
|
|
1104
|
+
".astro",
|
|
1105
|
+
".tf",
|
|
1106
|
+
".hcl",
|
|
1107
|
+
".dockerfile",
|
|
1108
|
+
".containerfile",
|
|
1109
|
+
".prisma",
|
|
1110
|
+
".proto"
|
|
1111
|
+
]);
|
|
1112
|
+
MAX_FILE_SIZE = 1024 * 1024;
|
|
1113
|
+
isDirectRun5 = process.argv[1]?.includes("post-write") || process.argv[2] === "post-write";
|
|
1114
|
+
if (isDirectRun5) {
|
|
1115
|
+
runPostWriteHook().catch(() => process.exit(0));
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// src/hooks-config.ts
|
|
1121
|
+
var hooks_config_exports = {};
|
|
1122
|
+
__export(hooks_config_exports, {
|
|
1123
|
+
CLINE_POSTTOOLUSE_HOOK_SCRIPT: () => CLINE_POSTTOOLUSE_HOOK_SCRIPT,
|
|
1124
|
+
CLINE_PRETOOLUSE_HOOK_SCRIPT: () => CLINE_PRETOOLUSE_HOOK_SCRIPT,
|
|
1125
|
+
CLINE_USER_PROMPT_HOOK_SCRIPT: () => CLINE_USER_PROMPT_HOOK_SCRIPT,
|
|
1126
|
+
CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT: () => CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT,
|
|
1127
|
+
CURSOR_PRETOOLUSE_HOOK_SCRIPT: () => CURSOR_PRETOOLUSE_HOOK_SCRIPT,
|
|
1128
|
+
MEDIA_AWARE_HOOK_SCRIPT: () => MEDIA_AWARE_HOOK_SCRIPT,
|
|
1129
|
+
PRECOMPACT_HOOK_SCRIPT: () => PRECOMPACT_HOOK_SCRIPT,
|
|
1130
|
+
PRETOOLUSE_HOOK_SCRIPT: () => PRETOOLUSE_HOOK_SCRIPT,
|
|
1131
|
+
USER_PROMPT_HOOK_SCRIPT: () => USER_PROMPT_HOOK_SCRIPT,
|
|
1132
|
+
buildHooksConfig: () => buildHooksConfig,
|
|
1133
|
+
generateAllHooksDocumentation: () => generateAllHooksDocumentation,
|
|
1134
|
+
generateHooksDocumentation: () => generateHooksDocumentation,
|
|
1135
|
+
getClaudeSettingsPath: () => getClaudeSettingsPath,
|
|
1136
|
+
getClineHooksDir: () => getClineHooksDir,
|
|
1137
|
+
getCursorHooksConfigPath: () => getCursorHooksConfigPath,
|
|
1138
|
+
getCursorHooksDir: () => getCursorHooksDir,
|
|
1139
|
+
getHookCommand: () => getHookCommand,
|
|
1140
|
+
getHooksDir: () => getHooksDir,
|
|
1141
|
+
getIndexStatusPath: () => getIndexStatusPath,
|
|
1142
|
+
getKiloCodeHooksDir: () => getKiloCodeHooksDir,
|
|
1143
|
+
getRooCodeHooksDir: () => getRooCodeHooksDir,
|
|
1144
|
+
installAllEditorHooks: () => installAllEditorHooks,
|
|
1145
|
+
installClaudeCodeHooks: () => installClaudeCodeHooks,
|
|
1146
|
+
installClineHookScripts: () => installClineHookScripts,
|
|
1147
|
+
installCursorHookScripts: () => installCursorHookScripts,
|
|
1148
|
+
installEditorHooks: () => installEditorHooks,
|
|
1149
|
+
installHookScripts: () => installHookScripts,
|
|
1150
|
+
installKiloCodeHookScripts: () => installKiloCodeHookScripts,
|
|
1151
|
+
installRooCodeHookScripts: () => installRooCodeHookScripts,
|
|
1152
|
+
markProjectIndexed: () => markProjectIndexed,
|
|
1153
|
+
mergeHooksIntoSettings: () => mergeHooksIntoSettings,
|
|
1154
|
+
readClaudeSettings: () => readClaudeSettings,
|
|
1155
|
+
readCursorHooksConfig: () => readCursorHooksConfig,
|
|
1156
|
+
readIndexStatus: () => readIndexStatus,
|
|
1157
|
+
unmarkProjectIndexed: () => unmarkProjectIndexed,
|
|
1158
|
+
writeClaudeSettings: () => writeClaudeSettings,
|
|
1159
|
+
writeCursorHooksConfig: () => writeCursorHooksConfig,
|
|
1160
|
+
writeIndexStatus: () => writeIndexStatus
|
|
1161
|
+
});
|
|
1162
|
+
import * as fs4 from "node:fs/promises";
|
|
1163
|
+
import * as path4 from "node:path";
|
|
1164
|
+
import { homedir as homedir4 } from "node:os";
|
|
1165
|
+
import { fileURLToPath } from "node:url";
|
|
1166
|
+
function getHookCommand(hookName2) {
|
|
1167
|
+
try {
|
|
1168
|
+
const __dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
1169
|
+
const indexPath = path4.join(__dirname, "index.js");
|
|
1170
|
+
const fs6 = __require("node:fs");
|
|
1171
|
+
if (fs6.existsSync(indexPath)) {
|
|
1172
|
+
return `node ${indexPath} hook ${hookName2}`;
|
|
1173
|
+
}
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
return `npx @contextstream/mcp-server hook ${hookName2}`;
|
|
1177
|
+
}
|
|
1178
|
+
function getClaudeSettingsPath(scope, projectPath) {
|
|
1179
|
+
if (scope === "user") {
|
|
1180
|
+
return path4.join(homedir4(), ".claude", "settings.json");
|
|
1181
|
+
}
|
|
1182
|
+
if (!projectPath) {
|
|
1183
|
+
throw new Error("projectPath required for project scope");
|
|
1184
|
+
}
|
|
1185
|
+
return path4.join(projectPath, ".claude", "settings.json");
|
|
1186
|
+
}
|
|
1187
|
+
function getHooksDir() {
|
|
1188
|
+
return path4.join(homedir4(), ".claude", "hooks");
|
|
1189
|
+
}
|
|
1190
|
+
function buildHooksConfig(options) {
|
|
1191
|
+
const userPromptHooks = [
|
|
1192
|
+
{
|
|
1193
|
+
matcher: "*",
|
|
1194
|
+
hooks: [
|
|
1195
|
+
{
|
|
1196
|
+
type: "command",
|
|
1197
|
+
command: getHookCommand("user-prompt-submit"),
|
|
1198
|
+
timeout: 5
|
|
1199
|
+
}
|
|
1200
|
+
]
|
|
1201
|
+
}
|
|
1202
|
+
];
|
|
1203
|
+
if (options?.includeOnSaveIntent !== false) {
|
|
1204
|
+
userPromptHooks.push({
|
|
1205
|
+
matcher: "*",
|
|
1206
|
+
hooks: [
|
|
1207
|
+
{
|
|
1208
|
+
type: "command",
|
|
1209
|
+
command: getHookCommand("on-save-intent"),
|
|
1210
|
+
timeout: 5
|
|
1211
|
+
}
|
|
1212
|
+
]
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
if (options?.includeMediaAware !== false) {
|
|
1216
|
+
userPromptHooks.push({
|
|
1217
|
+
matcher: "*",
|
|
1218
|
+
hooks: [
|
|
1219
|
+
{
|
|
1220
|
+
type: "command",
|
|
1221
|
+
command: getHookCommand("media-aware"),
|
|
1222
|
+
timeout: 5
|
|
1223
|
+
}
|
|
1224
|
+
]
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
const config = {
|
|
1228
|
+
PreToolUse: [
|
|
1229
|
+
{
|
|
1230
|
+
matcher: "Glob|Grep|Search|Task|EnterPlanMode",
|
|
1231
|
+
hooks: [
|
|
1232
|
+
{
|
|
1233
|
+
type: "command",
|
|
1234
|
+
command: getHookCommand("pre-tool-use"),
|
|
1235
|
+
timeout: 5
|
|
1236
|
+
}
|
|
1237
|
+
]
|
|
1238
|
+
}
|
|
1239
|
+
],
|
|
1240
|
+
UserPromptSubmit: userPromptHooks
|
|
1241
|
+
};
|
|
1242
|
+
if (options?.includePreCompact !== false) {
|
|
1243
|
+
config.PreCompact = [
|
|
1244
|
+
{
|
|
1245
|
+
matcher: "*",
|
|
1246
|
+
hooks: [
|
|
1247
|
+
{
|
|
1248
|
+
type: "command",
|
|
1249
|
+
command: getHookCommand("pre-compact"),
|
|
1250
|
+
timeout: 10
|
|
1251
|
+
}
|
|
1252
|
+
]
|
|
1253
|
+
}
|
|
1254
|
+
];
|
|
1255
|
+
}
|
|
1256
|
+
if (options?.includeSessionInit !== false) {
|
|
1257
|
+
config.SessionStart = [
|
|
1258
|
+
{
|
|
1259
|
+
matcher: "*",
|
|
1260
|
+
hooks: [
|
|
1261
|
+
{
|
|
1262
|
+
type: "command",
|
|
1263
|
+
command: getHookCommand("session-init"),
|
|
1264
|
+
timeout: 10
|
|
1265
|
+
}
|
|
1266
|
+
]
|
|
1267
|
+
}
|
|
1268
|
+
];
|
|
1269
|
+
}
|
|
1270
|
+
if (options?.includeSessionEnd !== false) {
|
|
1271
|
+
config.Stop = [
|
|
1272
|
+
{
|
|
1273
|
+
matcher: "*",
|
|
1274
|
+
hooks: [
|
|
1275
|
+
{
|
|
1276
|
+
type: "command",
|
|
1277
|
+
command: getHookCommand("session-end"),
|
|
1278
|
+
timeout: 10
|
|
1279
|
+
}
|
|
1280
|
+
]
|
|
1281
|
+
}
|
|
1282
|
+
];
|
|
1283
|
+
}
|
|
1284
|
+
const postToolUseHooks = [];
|
|
1285
|
+
if (options?.includePostWrite !== false) {
|
|
1286
|
+
postToolUseHooks.push({
|
|
1287
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
1288
|
+
hooks: [
|
|
1289
|
+
{
|
|
1290
|
+
type: "command",
|
|
1291
|
+
command: getHookCommand("post-write"),
|
|
1292
|
+
timeout: 10
|
|
1293
|
+
}
|
|
1294
|
+
]
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
if (options?.includeAutoRules !== false) {
|
|
1298
|
+
postToolUseHooks.push({
|
|
1299
|
+
matcher: "mcp__contextstream__init|mcp__contextstream__context",
|
|
1300
|
+
hooks: [
|
|
1301
|
+
{
|
|
1302
|
+
type: "command",
|
|
1303
|
+
command: getHookCommand("auto-rules"),
|
|
1304
|
+
timeout: 15
|
|
1305
|
+
}
|
|
1306
|
+
]
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
if (options?.includeOnBash !== false) {
|
|
1310
|
+
postToolUseHooks.push({
|
|
1311
|
+
matcher: "Bash",
|
|
1312
|
+
hooks: [
|
|
1313
|
+
{
|
|
1314
|
+
type: "command",
|
|
1315
|
+
command: getHookCommand("on-bash"),
|
|
1316
|
+
timeout: 5
|
|
1317
|
+
}
|
|
1318
|
+
]
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (options?.includeOnTask !== false) {
|
|
1322
|
+
postToolUseHooks.push({
|
|
1323
|
+
matcher: "Task",
|
|
1324
|
+
hooks: [
|
|
1325
|
+
{
|
|
1326
|
+
type: "command",
|
|
1327
|
+
command: getHookCommand("on-task"),
|
|
1328
|
+
timeout: 5
|
|
1329
|
+
}
|
|
1330
|
+
]
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
if (options?.includeOnRead !== false) {
|
|
1334
|
+
postToolUseHooks.push({
|
|
1335
|
+
matcher: "Read|Glob|Grep",
|
|
1336
|
+
hooks: [
|
|
1337
|
+
{
|
|
1338
|
+
type: "command",
|
|
1339
|
+
command: getHookCommand("on-read"),
|
|
1340
|
+
timeout: 5
|
|
1341
|
+
}
|
|
1342
|
+
]
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
if (options?.includeOnWeb !== false) {
|
|
1346
|
+
postToolUseHooks.push({
|
|
1347
|
+
matcher: "WebFetch|WebSearch",
|
|
1348
|
+
hooks: [
|
|
1349
|
+
{
|
|
1350
|
+
type: "command",
|
|
1351
|
+
command: getHookCommand("on-web"),
|
|
1352
|
+
timeout: 5
|
|
1353
|
+
}
|
|
1354
|
+
]
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
if (postToolUseHooks.length > 0) {
|
|
1358
|
+
config.PostToolUse = postToolUseHooks;
|
|
1359
|
+
}
|
|
1360
|
+
return config;
|
|
1361
|
+
}
|
|
1362
|
+
async function installHookScripts(options) {
|
|
1363
|
+
const hooksDir = getHooksDir();
|
|
1364
|
+
await fs4.mkdir(hooksDir, { recursive: true });
|
|
1365
|
+
const result = {
|
|
1366
|
+
preToolUse: getHookCommand("pre-tool-use"),
|
|
1367
|
+
userPrompt: getHookCommand("user-prompt-submit")
|
|
1368
|
+
};
|
|
1369
|
+
if (options?.includePreCompact !== false) {
|
|
1370
|
+
result.preCompact = getHookCommand("pre-compact");
|
|
1371
|
+
}
|
|
1372
|
+
if (options?.includeMediaAware !== false) {
|
|
1373
|
+
result.mediaAware = getHookCommand("media-aware");
|
|
1374
|
+
}
|
|
1375
|
+
if (options?.includeAutoRules !== false) {
|
|
1376
|
+
result.autoRules = getHookCommand("auto-rules");
|
|
1377
|
+
}
|
|
1378
|
+
return result;
|
|
1379
|
+
}
|
|
1380
|
+
async function readClaudeSettings(scope, projectPath) {
|
|
1381
|
+
const settingsPath = getClaudeSettingsPath(scope, projectPath);
|
|
1382
|
+
try {
|
|
1383
|
+
const content = await fs4.readFile(settingsPath, "utf-8");
|
|
1384
|
+
return JSON.parse(content);
|
|
1385
|
+
} catch {
|
|
1386
|
+
return {};
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
async function writeClaudeSettings(settings, scope, projectPath) {
|
|
1390
|
+
const settingsPath = getClaudeSettingsPath(scope, projectPath);
|
|
1391
|
+
const dir = path4.dirname(settingsPath);
|
|
1392
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
1393
|
+
await fs4.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
1394
|
+
}
|
|
1395
|
+
function mergeHooksIntoSettings(existingSettings, newHooks) {
|
|
1396
|
+
const settings = { ...existingSettings };
|
|
1397
|
+
const existingHooks = settings.hooks || {};
|
|
1398
|
+
for (const [hookType, matchers] of Object.entries(newHooks || {})) {
|
|
1399
|
+
if (!matchers) continue;
|
|
1400
|
+
const existing = existingHooks?.[hookType] || [];
|
|
1401
|
+
const filtered = existing.filter((m) => {
|
|
1402
|
+
return !m.hooks?.some((h) => h.command?.includes("contextstream"));
|
|
1403
|
+
});
|
|
1404
|
+
existingHooks[hookType] = [...filtered, ...matchers];
|
|
1405
|
+
}
|
|
1406
|
+
settings.hooks = existingHooks;
|
|
1407
|
+
return settings;
|
|
1408
|
+
}
|
|
1409
|
+
async function installClaudeCodeHooks(options) {
|
|
1410
|
+
const result = { scripts: [], settings: [] };
|
|
1411
|
+
result.scripts.push(
|
|
1412
|
+
getHookCommand("pre-tool-use"),
|
|
1413
|
+
getHookCommand("user-prompt-submit")
|
|
1414
|
+
);
|
|
1415
|
+
if (options.includePreCompact !== false) {
|
|
1416
|
+
result.scripts.push(getHookCommand("pre-compact"));
|
|
1417
|
+
}
|
|
1418
|
+
if (options.includeMediaAware !== false) {
|
|
1419
|
+
result.scripts.push(getHookCommand("media-aware"));
|
|
1420
|
+
}
|
|
1421
|
+
if (options.includePostWrite !== false) {
|
|
1422
|
+
result.scripts.push(getHookCommand("post-write"));
|
|
1423
|
+
}
|
|
1424
|
+
if (options.includeAutoRules !== false) {
|
|
1425
|
+
result.scripts.push(getHookCommand("auto-rules"));
|
|
1426
|
+
}
|
|
1427
|
+
const hooksConfig = buildHooksConfig({
|
|
1428
|
+
includePreCompact: options.includePreCompact,
|
|
1429
|
+
includeMediaAware: options.includeMediaAware,
|
|
1430
|
+
includePostWrite: options.includePostWrite,
|
|
1431
|
+
includeAutoRules: options.includeAutoRules
|
|
1432
|
+
});
|
|
1433
|
+
if (options.scope === "user" || options.scope === "both") {
|
|
1434
|
+
const settingsPath = getClaudeSettingsPath("user");
|
|
1435
|
+
if (!options.dryRun) {
|
|
1436
|
+
const existing = await readClaudeSettings("user");
|
|
1437
|
+
const merged = mergeHooksIntoSettings(existing, hooksConfig);
|
|
1438
|
+
await writeClaudeSettings(merged, "user");
|
|
1439
|
+
}
|
|
1440
|
+
result.settings.push(settingsPath);
|
|
1441
|
+
}
|
|
1442
|
+
if ((options.scope === "project" || options.scope === "both") && options.projectPath) {
|
|
1443
|
+
const settingsPath = getClaudeSettingsPath("project", options.projectPath);
|
|
1444
|
+
if (!options.dryRun) {
|
|
1445
|
+
const existing = await readClaudeSettings("project", options.projectPath);
|
|
1446
|
+
const merged = mergeHooksIntoSettings(existing, hooksConfig);
|
|
1447
|
+
await writeClaudeSettings(merged, "project", options.projectPath);
|
|
1448
|
+
}
|
|
1449
|
+
result.settings.push(settingsPath);
|
|
1450
|
+
}
|
|
1451
|
+
return result;
|
|
1452
|
+
}
|
|
1453
|
+
function generateHooksDocumentation() {
|
|
1454
|
+
return `
|
|
1455
|
+
## Claude Code Hooks (ContextStream)
|
|
1456
|
+
|
|
1457
|
+
ContextStream installs hooks to enforce ContextStream-first behavior.
|
|
1458
|
+
All hooks run via Node.js - no Python dependency required.
|
|
1459
|
+
|
|
1460
|
+
### PreToolUse Hook
|
|
1461
|
+
- **Command:** \`npx @contextstream/mcp-server hook pre-tool-use\`
|
|
1462
|
+
- **Purpose:** Blocks Glob/Grep/Search/EnterPlanMode and redirects to ContextStream
|
|
1463
|
+
- **Blocked tools:** Glob, Grep, Search, Task(Explore), Task(Plan), EnterPlanMode
|
|
1464
|
+
- **Disable:** Set \`CONTEXTSTREAM_HOOK_ENABLED=false\` environment variable
|
|
1465
|
+
|
|
1466
|
+
### UserPromptSubmit Hook
|
|
1467
|
+
- **Command:** \`npx @contextstream/mcp-server hook user-prompt-submit\`
|
|
1468
|
+
- **Purpose:** Injects a reminder about ContextStream rules on every message
|
|
1469
|
+
- **Disable:** Set \`CONTEXTSTREAM_REMINDER_ENABLED=false\` environment variable
|
|
1470
|
+
|
|
1471
|
+
### Media-Aware Hook
|
|
1472
|
+
- **Command:** \`npx @contextstream/mcp-server hook media-aware\`
|
|
1473
|
+
- **Purpose:** Detects media-related prompts and injects media tool guidance
|
|
1474
|
+
- **Triggers:** Patterns like video, clips, Remotion, image, audio, creative assets
|
|
1475
|
+
- **Disable:** Set \`CONTEXTSTREAM_MEDIA_HOOK_ENABLED=false\` environment variable
|
|
1476
|
+
|
|
1477
|
+
When Media-Aware hook detects media patterns, it injects context about:
|
|
1478
|
+
- How to search indexed media assets
|
|
1479
|
+
- How to get clips for Remotion (with frame-based props)
|
|
1480
|
+
- How to index new media files
|
|
1481
|
+
|
|
1482
|
+
### PreCompact Hook
|
|
1483
|
+
- **Command:** \`npx @contextstream/mcp-server hook pre-compact\`
|
|
1484
|
+
- **Purpose:** Saves conversation state before context compaction
|
|
1485
|
+
- **Triggers:** Both manual (/compact) and automatic compaction
|
|
1486
|
+
- **Installed:** By default (disable with \`CONTEXTSTREAM_HOOK_ENABLED=false\`)
|
|
1487
|
+
|
|
1488
|
+
When PreCompact runs, it:
|
|
1489
|
+
1. Parses the transcript for active files and tool calls
|
|
1490
|
+
2. Saves a session_snapshot to ContextStream API
|
|
1491
|
+
3. Injects context about using \`session_init(is_post_compact=true)\` after compaction
|
|
1492
|
+
|
|
1493
|
+
### PostToolUse Hook (Real-time Indexing)
|
|
1494
|
+
- **Command:** \`npx @contextstream/mcp-server hook post-write\`
|
|
1495
|
+
- **Purpose:** Indexes files immediately after Edit/Write/NotebookEdit operations
|
|
1496
|
+
- **Matcher:** Edit|Write|NotebookEdit
|
|
1497
|
+
- **Disable:** Set \`CONTEXTSTREAM_POSTWRITE_ENABLED=false\` environment variable
|
|
1498
|
+
|
|
1499
|
+
### Why Hooks?
|
|
1500
|
+
Claude Code has strong built-in behaviors to use its default tools (Grep, Glob, Read)
|
|
1501
|
+
and its built-in plan mode. CLAUDE.md instructions decay over long conversations.
|
|
1502
|
+
Hooks provide:
|
|
1503
|
+
1. **Physical enforcement** - Blocked tools can't be used
|
|
1504
|
+
2. **Continuous reminders** - Rules stay in recent context
|
|
1505
|
+
3. **Better UX** - Faster searches via indexed ContextStream
|
|
1506
|
+
4. **Persistent plans** - ContextStream plans survive across sessions
|
|
1507
|
+
5. **Compaction awareness** - Save state before context is compacted
|
|
1508
|
+
6. **Real-time indexing** - Files indexed immediately after writes
|
|
1509
|
+
|
|
1510
|
+
### Manual Configuration
|
|
1511
|
+
If you prefer to configure manually, add to \`~/.claude/settings.json\`:
|
|
1512
|
+
\`\`\`json
|
|
1513
|
+
{
|
|
1514
|
+
"hooks": {
|
|
1515
|
+
"PreToolUse": [{
|
|
1516
|
+
"matcher": "Glob|Grep|Search|Task|EnterPlanMode",
|
|
1517
|
+
"hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook pre-tool-use"}]
|
|
1518
|
+
}],
|
|
1519
|
+
"UserPromptSubmit": [
|
|
1520
|
+
{
|
|
1521
|
+
"matcher": "*",
|
|
1522
|
+
"hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook user-prompt-submit"}]
|
|
1523
|
+
},
|
|
1524
|
+
{
|
|
1525
|
+
"matcher": "*",
|
|
1526
|
+
"hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook media-aware"}]
|
|
1527
|
+
}
|
|
1528
|
+
],
|
|
1529
|
+
"PreCompact": [{
|
|
1530
|
+
"matcher": "*",
|
|
1531
|
+
"hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook pre-compact", "timeout": 10}]
|
|
1532
|
+
}],
|
|
1533
|
+
"PostToolUse": [{
|
|
1534
|
+
"matcher": "Edit|Write|NotebookEdit",
|
|
1535
|
+
"hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook post-write", "timeout": 10}]
|
|
1536
|
+
}]
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
\`\`\`
|
|
1540
|
+
`.trim();
|
|
1541
|
+
}
|
|
1542
|
+
function getIndexStatusPath() {
|
|
1543
|
+
return path4.join(homedir4(), ".contextstream", "indexed-projects.json");
|
|
1544
|
+
}
|
|
1545
|
+
async function readIndexStatus() {
|
|
1546
|
+
const statusPath = getIndexStatusPath();
|
|
1547
|
+
try {
|
|
1548
|
+
const content = await fs4.readFile(statusPath, "utf-8");
|
|
1549
|
+
return JSON.parse(content);
|
|
1550
|
+
} catch {
|
|
1551
|
+
return { version: 1, projects: {} };
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
async function writeIndexStatus(status) {
|
|
1555
|
+
const statusPath = getIndexStatusPath();
|
|
1556
|
+
const dir = path4.dirname(statusPath);
|
|
1557
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
1558
|
+
await fs4.writeFile(statusPath, JSON.stringify(status, null, 2));
|
|
1559
|
+
}
|
|
1560
|
+
async function markProjectIndexed(projectPath, options) {
|
|
1561
|
+
const status = await readIndexStatus();
|
|
1562
|
+
const resolvedPath = path4.resolve(projectPath);
|
|
1563
|
+
status.projects[resolvedPath] = {
|
|
1564
|
+
indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1565
|
+
project_id: options?.project_id,
|
|
1566
|
+
project_name: options?.project_name
|
|
1567
|
+
};
|
|
1568
|
+
await writeIndexStatus(status);
|
|
1569
|
+
}
|
|
1570
|
+
async function unmarkProjectIndexed(projectPath) {
|
|
1571
|
+
const status = await readIndexStatus();
|
|
1572
|
+
const resolvedPath = path4.resolve(projectPath);
|
|
1573
|
+
delete status.projects[resolvedPath];
|
|
1574
|
+
await writeIndexStatus(status);
|
|
1575
|
+
}
|
|
1576
|
+
function getClineHooksDir(scope, projectPath) {
|
|
1577
|
+
if (scope === "global") {
|
|
1578
|
+
return path4.join(homedir4(), "Documents", "Cline", "Rules", "Hooks");
|
|
1579
|
+
}
|
|
1580
|
+
if (!projectPath) {
|
|
1581
|
+
throw new Error("projectPath required for project scope");
|
|
1582
|
+
}
|
|
1583
|
+
return path4.join(projectPath, ".clinerules", "hooks");
|
|
1584
|
+
}
|
|
1585
|
+
async function installClineHookScripts(options) {
|
|
1586
|
+
const hooksDir = getClineHooksDir(options.scope, options.projectPath);
|
|
1587
|
+
await fs4.mkdir(hooksDir, { recursive: true });
|
|
1588
|
+
const preToolUsePath = path4.join(hooksDir, "PreToolUse");
|
|
1589
|
+
const userPromptPath = path4.join(hooksDir, "UserPromptSubmit");
|
|
1590
|
+
const postToolUsePath = path4.join(hooksDir, "PostToolUse");
|
|
1591
|
+
await fs4.writeFile(preToolUsePath, CLINE_HOOK_WRAPPER("pre-tool-use"), { mode: 493 });
|
|
1592
|
+
await fs4.writeFile(userPromptPath, CLINE_HOOK_WRAPPER("user-prompt-submit"), { mode: 493 });
|
|
1593
|
+
const result = {
|
|
1594
|
+
preToolUse: preToolUsePath,
|
|
1595
|
+
userPromptSubmit: userPromptPath
|
|
1596
|
+
};
|
|
1597
|
+
if (options.includePostWrite !== false) {
|
|
1598
|
+
await fs4.writeFile(postToolUsePath, CLINE_HOOK_WRAPPER("post-write"), { mode: 493 });
|
|
1599
|
+
result.postToolUse = postToolUsePath;
|
|
1600
|
+
}
|
|
1601
|
+
return result;
|
|
1602
|
+
}
|
|
1603
|
+
function getRooCodeHooksDir(scope, projectPath) {
|
|
1604
|
+
if (scope === "global") {
|
|
1605
|
+
return path4.join(homedir4(), ".roo", "hooks");
|
|
1606
|
+
}
|
|
1607
|
+
if (!projectPath) {
|
|
1608
|
+
throw new Error("projectPath required for project scope");
|
|
1609
|
+
}
|
|
1610
|
+
return path4.join(projectPath, ".roo", "hooks");
|
|
1611
|
+
}
|
|
1612
|
+
async function installRooCodeHookScripts(options) {
|
|
1613
|
+
const hooksDir = getRooCodeHooksDir(options.scope, options.projectPath);
|
|
1614
|
+
await fs4.mkdir(hooksDir, { recursive: true });
|
|
1615
|
+
const preToolUsePath = path4.join(hooksDir, "PreToolUse");
|
|
1616
|
+
const userPromptPath = path4.join(hooksDir, "UserPromptSubmit");
|
|
1617
|
+
const postToolUsePath = path4.join(hooksDir, "PostToolUse");
|
|
1618
|
+
await fs4.writeFile(preToolUsePath, CLINE_HOOK_WRAPPER("pre-tool-use"), { mode: 493 });
|
|
1619
|
+
await fs4.writeFile(userPromptPath, CLINE_HOOK_WRAPPER("user-prompt-submit"), { mode: 493 });
|
|
1620
|
+
const result = {
|
|
1621
|
+
preToolUse: preToolUsePath,
|
|
1622
|
+
userPromptSubmit: userPromptPath
|
|
1623
|
+
};
|
|
1624
|
+
if (options.includePostWrite !== false) {
|
|
1625
|
+
await fs4.writeFile(postToolUsePath, CLINE_HOOK_WRAPPER("post-write"), { mode: 493 });
|
|
1626
|
+
result.postToolUse = postToolUsePath;
|
|
1627
|
+
}
|
|
1628
|
+
return result;
|
|
1629
|
+
}
|
|
1630
|
+
function getKiloCodeHooksDir(scope, projectPath) {
|
|
1631
|
+
if (scope === "global") {
|
|
1632
|
+
return path4.join(homedir4(), ".kilocode", "hooks");
|
|
1633
|
+
}
|
|
1634
|
+
if (!projectPath) {
|
|
1635
|
+
throw new Error("projectPath required for project scope");
|
|
1636
|
+
}
|
|
1637
|
+
return path4.join(projectPath, ".kilocode", "hooks");
|
|
1638
|
+
}
|
|
1639
|
+
async function installKiloCodeHookScripts(options) {
|
|
1640
|
+
const hooksDir = getKiloCodeHooksDir(options.scope, options.projectPath);
|
|
1641
|
+
await fs4.mkdir(hooksDir, { recursive: true });
|
|
1642
|
+
const preToolUsePath = path4.join(hooksDir, "PreToolUse");
|
|
1643
|
+
const userPromptPath = path4.join(hooksDir, "UserPromptSubmit");
|
|
1644
|
+
const postToolUsePath = path4.join(hooksDir, "PostToolUse");
|
|
1645
|
+
await fs4.writeFile(preToolUsePath, CLINE_HOOK_WRAPPER("pre-tool-use"), { mode: 493 });
|
|
1646
|
+
await fs4.writeFile(userPromptPath, CLINE_HOOK_WRAPPER("user-prompt-submit"), { mode: 493 });
|
|
1647
|
+
const result = {
|
|
1648
|
+
preToolUse: preToolUsePath,
|
|
1649
|
+
userPromptSubmit: userPromptPath
|
|
1650
|
+
};
|
|
1651
|
+
if (options.includePostWrite !== false) {
|
|
1652
|
+
await fs4.writeFile(postToolUsePath, CLINE_HOOK_WRAPPER("post-write"), { mode: 493 });
|
|
1653
|
+
result.postToolUse = postToolUsePath;
|
|
1654
|
+
}
|
|
1655
|
+
return result;
|
|
1656
|
+
}
|
|
1657
|
+
function getCursorHooksConfigPath(scope, projectPath) {
|
|
1658
|
+
if (scope === "global") {
|
|
1659
|
+
return path4.join(homedir4(), ".cursor", "hooks.json");
|
|
1660
|
+
}
|
|
1661
|
+
if (!projectPath) {
|
|
1662
|
+
throw new Error("projectPath required for project scope");
|
|
1663
|
+
}
|
|
1664
|
+
return path4.join(projectPath, ".cursor", "hooks.json");
|
|
1665
|
+
}
|
|
1666
|
+
function getCursorHooksDir(scope, projectPath) {
|
|
1667
|
+
if (scope === "global") {
|
|
1668
|
+
return path4.join(homedir4(), ".cursor", "hooks");
|
|
1669
|
+
}
|
|
1670
|
+
if (!projectPath) {
|
|
1671
|
+
throw new Error("projectPath required for project scope");
|
|
1672
|
+
}
|
|
1673
|
+
return path4.join(projectPath, ".cursor", "hooks");
|
|
1674
|
+
}
|
|
1675
|
+
async function readCursorHooksConfig(scope, projectPath) {
|
|
1676
|
+
const configPath = getCursorHooksConfigPath(scope, projectPath);
|
|
1677
|
+
try {
|
|
1678
|
+
const content = await fs4.readFile(configPath, "utf-8");
|
|
1679
|
+
return JSON.parse(content);
|
|
1680
|
+
} catch {
|
|
1681
|
+
return { version: 1, hooks: {} };
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
async function writeCursorHooksConfig(config, scope, projectPath) {
|
|
1685
|
+
const configPath = getCursorHooksConfigPath(scope, projectPath);
|
|
1686
|
+
const dir = path4.dirname(configPath);
|
|
1687
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
1688
|
+
await fs4.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
1689
|
+
}
|
|
1690
|
+
async function installCursorHookScripts(options) {
|
|
1691
|
+
const hooksDir = getCursorHooksDir(options.scope, options.projectPath);
|
|
1692
|
+
await fs4.mkdir(hooksDir, { recursive: true });
|
|
1693
|
+
const existingConfig = await readCursorHooksConfig(options.scope, options.projectPath);
|
|
1694
|
+
const filterContextStreamHooks = (hooks2) => {
|
|
1695
|
+
if (!hooks2) return [];
|
|
1696
|
+
return hooks2.filter((h) => {
|
|
1697
|
+
const hook = h;
|
|
1698
|
+
return !hook.command?.includes("contextstream");
|
|
1699
|
+
});
|
|
1700
|
+
};
|
|
1701
|
+
const filteredPreToolUse = filterContextStreamHooks(existingConfig.hooks.preToolUse);
|
|
1702
|
+
const filteredBeforeSubmit = filterContextStreamHooks(existingConfig.hooks.beforeSubmitPrompt);
|
|
1703
|
+
const preToolUseCommand = getHookCommand("pre-tool-use");
|
|
1704
|
+
const userPromptCommand = getHookCommand("user-prompt-submit");
|
|
1705
|
+
const config = {
|
|
1706
|
+
version: 1,
|
|
1707
|
+
hooks: {
|
|
1708
|
+
...existingConfig.hooks,
|
|
1709
|
+
preToolUse: [
|
|
1710
|
+
...filteredPreToolUse,
|
|
1711
|
+
{
|
|
1712
|
+
command: preToolUseCommand,
|
|
1713
|
+
type: "command",
|
|
1714
|
+
timeout: 5,
|
|
1715
|
+
matcher: { tool_name: "Glob|Grep|search_files|list_files|ripgrep" }
|
|
1716
|
+
}
|
|
1717
|
+
],
|
|
1718
|
+
beforeSubmitPrompt: [
|
|
1719
|
+
...filteredBeforeSubmit,
|
|
1720
|
+
{
|
|
1721
|
+
command: userPromptCommand,
|
|
1722
|
+
type: "command",
|
|
1723
|
+
timeout: 5
|
|
1724
|
+
}
|
|
1725
|
+
]
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
await writeCursorHooksConfig(config, options.scope, options.projectPath);
|
|
1729
|
+
const configPath = getCursorHooksConfigPath(options.scope, options.projectPath);
|
|
1730
|
+
return {
|
|
1731
|
+
preToolUse: preToolUseCommand,
|
|
1732
|
+
beforeSubmitPrompt: userPromptCommand,
|
|
1733
|
+
config: configPath
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
async function installEditorHooks(options) {
|
|
1737
|
+
const { editor, scope, projectPath, includePreCompact, includePostWrite } = options;
|
|
1738
|
+
switch (editor) {
|
|
1739
|
+
case "claude": {
|
|
1740
|
+
if (scope === "project" && !projectPath) {
|
|
1741
|
+
throw new Error("projectPath required for project scope");
|
|
1742
|
+
}
|
|
1743
|
+
const scripts = await installHookScripts({ includePreCompact });
|
|
1744
|
+
const hooksConfig = buildHooksConfig({ includePreCompact, includePostWrite });
|
|
1745
|
+
const settingsScope = scope === "global" ? "user" : "project";
|
|
1746
|
+
const existing = await readClaudeSettings(settingsScope, projectPath);
|
|
1747
|
+
const merged = mergeHooksIntoSettings(existing, hooksConfig);
|
|
1748
|
+
await writeClaudeSettings(merged, settingsScope, projectPath);
|
|
1749
|
+
const installed = [scripts.preToolUse, scripts.userPrompt];
|
|
1750
|
+
if (scripts.preCompact) installed.push(scripts.preCompact);
|
|
1751
|
+
return {
|
|
1752
|
+
editor: "claude",
|
|
1753
|
+
installed,
|
|
1754
|
+
hooksDir: getHooksDir()
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
case "cline": {
|
|
1758
|
+
const scripts = await installClineHookScripts({ scope, projectPath, includePostWrite });
|
|
1759
|
+
const installed = [scripts.preToolUse, scripts.userPromptSubmit];
|
|
1760
|
+
if (scripts.postToolUse) installed.push(scripts.postToolUse);
|
|
1761
|
+
return {
|
|
1762
|
+
editor: "cline",
|
|
1763
|
+
installed,
|
|
1764
|
+
hooksDir: getClineHooksDir(scope, projectPath)
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
case "roo": {
|
|
1768
|
+
const scripts = await installRooCodeHookScripts({ scope, projectPath, includePostWrite });
|
|
1769
|
+
const installed = [scripts.preToolUse, scripts.userPromptSubmit];
|
|
1770
|
+
if (scripts.postToolUse) installed.push(scripts.postToolUse);
|
|
1771
|
+
return {
|
|
1772
|
+
editor: "roo",
|
|
1773
|
+
installed,
|
|
1774
|
+
hooksDir: getRooCodeHooksDir(scope, projectPath)
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
case "kilo": {
|
|
1778
|
+
const scripts = await installKiloCodeHookScripts({ scope, projectPath, includePostWrite });
|
|
1779
|
+
const installed = [scripts.preToolUse, scripts.userPromptSubmit];
|
|
1780
|
+
if (scripts.postToolUse) installed.push(scripts.postToolUse);
|
|
1781
|
+
return {
|
|
1782
|
+
editor: "kilo",
|
|
1783
|
+
installed,
|
|
1784
|
+
hooksDir: getKiloCodeHooksDir(scope, projectPath)
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
case "cursor": {
|
|
1788
|
+
const scripts = await installCursorHookScripts({ scope, projectPath });
|
|
1789
|
+
return {
|
|
1790
|
+
editor: "cursor",
|
|
1791
|
+
installed: [scripts.preToolUse, scripts.beforeSubmitPrompt],
|
|
1792
|
+
hooksDir: getCursorHooksDir(scope, projectPath)
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
default:
|
|
1796
|
+
throw new Error(`Unsupported editor: ${editor}`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
async function installAllEditorHooks(options) {
|
|
1800
|
+
const editors = options.editors || ["claude", "cline", "roo", "kilo", "cursor"];
|
|
1801
|
+
const results = [];
|
|
1802
|
+
for (const editor of editors) {
|
|
1803
|
+
try {
|
|
1804
|
+
const result = await installEditorHooks({
|
|
1805
|
+
editor,
|
|
1806
|
+
scope: options.scope,
|
|
1807
|
+
projectPath: options.projectPath,
|
|
1808
|
+
includePreCompact: options.includePreCompact,
|
|
1809
|
+
includePostWrite: options.includePostWrite
|
|
1810
|
+
});
|
|
1811
|
+
results.push(result);
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
console.error(`Failed to install hooks for ${editor}:`, error);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return results;
|
|
1817
|
+
}
|
|
1818
|
+
function generateAllHooksDocumentation() {
|
|
1819
|
+
return `
|
|
1820
|
+
## Editor Hooks Support (ContextStream)
|
|
1821
|
+
|
|
1822
|
+
ContextStream can install hooks for multiple AI code editors to enforce ContextStream-first behavior.
|
|
1823
|
+
|
|
1824
|
+
### Supported Editors
|
|
1825
|
+
|
|
1826
|
+
| Editor | Hooks Location | Hook Types |
|
|
1827
|
+
|--------|---------------|------------|
|
|
1828
|
+
| **Claude Code** | \`~/.claude/hooks/\` | PreToolUse, UserPromptSubmit, PreCompact |
|
|
1829
|
+
| **Cursor** | \`~/.cursor/hooks/\` | preToolUse, beforeSubmit |
|
|
1830
|
+
| **Cline** | \`~/Documents/Cline/Rules/Hooks/\` | PreToolUse, UserPromptSubmit |
|
|
1831
|
+
| **Roo Code** | \`~/.roo/hooks/\` | PreToolUse, UserPromptSubmit |
|
|
1832
|
+
| **Kilo Code** | \`~/.kilocode/hooks/\` | PreToolUse, UserPromptSubmit |
|
|
1833
|
+
|
|
1834
|
+
### Claude Code Hooks
|
|
1835
|
+
|
|
1836
|
+
${generateHooksDocumentation()}
|
|
1837
|
+
|
|
1838
|
+
### Cursor Hooks
|
|
1839
|
+
|
|
1840
|
+
Cursor uses a \`hooks.json\` configuration file:
|
|
1841
|
+
- **preToolUse**: Blocks discovery tools before execution
|
|
1842
|
+
- **beforeSubmitPrompt**: Injects ContextStream rules reminder
|
|
1843
|
+
|
|
1844
|
+
#### Output Format
|
|
1845
|
+
\`\`\`json
|
|
1846
|
+
{ "decision": "allow" }
|
|
1847
|
+
\`\`\`
|
|
1848
|
+
or
|
|
1849
|
+
\`\`\`json
|
|
1850
|
+
{ "decision": "deny", "reason": "Use ContextStream search instead" }
|
|
1851
|
+
\`\`\`
|
|
1852
|
+
|
|
1853
|
+
### Cline/Roo/Kilo Code Hooks
|
|
1854
|
+
|
|
1855
|
+
These editors use the same hook format (JSON output):
|
|
1856
|
+
- **PreToolUse**: Blocks discovery tools, redirects to ContextStream search
|
|
1857
|
+
- **UserPromptSubmit**: Injects ContextStream rules reminder
|
|
1858
|
+
|
|
1859
|
+
Hooks are executable scripts named after the hook type (no extension).
|
|
1860
|
+
|
|
1861
|
+
#### Output Format
|
|
1862
|
+
\`\`\`json
|
|
1863
|
+
{
|
|
1864
|
+
"cancel": true,
|
|
1865
|
+
"errorMessage": "Use ContextStream search instead",
|
|
1866
|
+
"contextModification": "[CONTEXTSTREAM] Use search tool first"
|
|
1867
|
+
}
|
|
1868
|
+
\`\`\`
|
|
1869
|
+
|
|
1870
|
+
### Installation
|
|
1871
|
+
|
|
1872
|
+
Use \`generate_rules(install_hooks=true, editors=["claude", "cursor", "cline", "roo", "kilo"])\` to install hooks for specific editors, or omit \`editors\` to install for all.
|
|
1873
|
+
|
|
1874
|
+
### Disabling Hooks
|
|
1875
|
+
|
|
1876
|
+
Set environment variables:
|
|
1877
|
+
- \`CONTEXTSTREAM_HOOK_ENABLED=false\` - Disable PreToolUse blocking
|
|
1878
|
+
- \`CONTEXTSTREAM_REMINDER_ENABLED=false\` - Disable UserPromptSubmit reminders
|
|
1879
|
+
`.trim();
|
|
1880
|
+
}
|
|
1881
|
+
var PRETOOLUSE_HOOK_SCRIPT, USER_PROMPT_HOOK_SCRIPT, MEDIA_AWARE_HOOK_SCRIPT, PRECOMPACT_HOOK_SCRIPT, CLINE_PRETOOLUSE_HOOK_SCRIPT, CLINE_USER_PROMPT_HOOK_SCRIPT, CLINE_POSTTOOLUSE_HOOK_SCRIPT, CLINE_HOOK_WRAPPER, CURSOR_PRETOOLUSE_HOOK_SCRIPT, CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT;
|
|
1882
|
+
var init_hooks_config = __esm({
|
|
1883
|
+
"src/hooks-config.ts"() {
|
|
1884
|
+
"use strict";
|
|
1885
|
+
PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
1886
|
+
"""
|
|
1887
|
+
ContextStream PreToolUse Hook for Claude Code
|
|
1888
|
+
Blocks Grep/Glob/Search/Task(Explore)/EnterPlanMode and redirects to ContextStream.
|
|
1889
|
+
|
|
1890
|
+
Only blocks if the current project is indexed in ContextStream.
|
|
1891
|
+
If not indexed, allows local tools through with a suggestion to index.
|
|
1892
|
+
"""
|
|
1893
|
+
|
|
1894
|
+
import json
|
|
1895
|
+
import sys
|
|
1896
|
+
import os
|
|
1897
|
+
from pathlib import Path
|
|
1898
|
+
from datetime import datetime, timedelta
|
|
1899
|
+
|
|
1900
|
+
ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
|
|
1901
|
+
INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
|
|
1902
|
+
# Consider index stale after 7 days
|
|
1903
|
+
STALE_THRESHOLD_DAYS = 7
|
|
1904
|
+
|
|
1905
|
+
DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
|
|
1906
|
+
|
|
1907
|
+
def is_discovery_glob(pattern):
|
|
1908
|
+
pattern_lower = pattern.lower()
|
|
1909
|
+
for p in DISCOVERY_PATTERNS:
|
|
1910
|
+
if p in pattern_lower:
|
|
1911
|
+
return True
|
|
1912
|
+
if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
|
|
1913
|
+
return True
|
|
1914
|
+
if "**" in pattern or "*/" in pattern:
|
|
1915
|
+
return True
|
|
1916
|
+
return False
|
|
1917
|
+
|
|
1918
|
+
def is_discovery_grep(file_path):
|
|
1919
|
+
if not file_path or file_path in [".", "./", "*", "**"]:
|
|
1920
|
+
return True
|
|
1921
|
+
if "*" in file_path or "**" in file_path:
|
|
1922
|
+
return True
|
|
1923
|
+
return False
|
|
1924
|
+
|
|
1925
|
+
def is_project_indexed(cwd: str) -> tuple[bool, bool]:
|
|
1926
|
+
"""
|
|
1927
|
+
Check if the current directory is in an indexed project.
|
|
1928
|
+
Returns (is_indexed, is_stale).
|
|
1929
|
+
"""
|
|
1930
|
+
if not INDEX_STATUS_FILE.exists():
|
|
1931
|
+
return False, False
|
|
1932
|
+
|
|
1933
|
+
try:
|
|
1934
|
+
with open(INDEX_STATUS_FILE, "r") as f:
|
|
1935
|
+
data = json.load(f)
|
|
1936
|
+
except:
|
|
1937
|
+
return False, False
|
|
1938
|
+
|
|
1939
|
+
projects = data.get("projects", {})
|
|
1940
|
+
cwd_path = Path(cwd).resolve()
|
|
1941
|
+
|
|
1942
|
+
# Check if cwd is within any indexed project
|
|
1943
|
+
for project_path, info in projects.items():
|
|
1944
|
+
try:
|
|
1945
|
+
indexed_path = Path(project_path).resolve()
|
|
1946
|
+
# Check if cwd is the project or a subdirectory
|
|
1947
|
+
if cwd_path == indexed_path or indexed_path in cwd_path.parents:
|
|
1948
|
+
# Check if stale
|
|
1949
|
+
indexed_at = info.get("indexed_at")
|
|
1950
|
+
if indexed_at:
|
|
1951
|
+
try:
|
|
1952
|
+
indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
|
|
1953
|
+
if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
|
|
1954
|
+
return True, True # Indexed but stale
|
|
1955
|
+
except:
|
|
1956
|
+
pass
|
|
1957
|
+
return True, False # Indexed and fresh
|
|
1958
|
+
except:
|
|
1959
|
+
continue
|
|
1960
|
+
|
|
1961
|
+
return False, False
|
|
1962
|
+
|
|
1963
|
+
def main():
|
|
1964
|
+
if not ENABLED:
|
|
1965
|
+
sys.exit(0)
|
|
1966
|
+
|
|
1967
|
+
try:
|
|
1968
|
+
data = json.load(sys.stdin)
|
|
1969
|
+
except:
|
|
1970
|
+
sys.exit(0)
|
|
1971
|
+
|
|
1972
|
+
tool = data.get("tool_name", "")
|
|
1973
|
+
inp = data.get("tool_input", {})
|
|
1974
|
+
cwd = data.get("cwd", os.getcwd())
|
|
1975
|
+
|
|
1976
|
+
# Check if project is indexed
|
|
1977
|
+
is_indexed, is_stale = is_project_indexed(cwd)
|
|
1978
|
+
|
|
1979
|
+
if not is_indexed:
|
|
1980
|
+
# Project not indexed - allow local tools but suggest indexing
|
|
1981
|
+
# Don't block, just exit successfully
|
|
1982
|
+
sys.exit(0)
|
|
1983
|
+
|
|
1984
|
+
if is_stale:
|
|
1985
|
+
# Index is stale - allow with warning (printed but not blocking)
|
|
1986
|
+
# Still allow the tool but remind about re-indexing
|
|
1987
|
+
pass # Continue to blocking logic but could add warning
|
|
1988
|
+
|
|
1989
|
+
if tool == "Glob":
|
|
1990
|
+
pattern = inp.get("pattern", "")
|
|
1991
|
+
if is_discovery_glob(pattern):
|
|
1992
|
+
print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of Glob.", file=sys.stderr)
|
|
1993
|
+
sys.exit(2)
|
|
1994
|
+
|
|
1995
|
+
elif tool == "Grep" or tool == "Search":
|
|
1996
|
+
# Block ALL Grep/Search operations - use ContextStream search or Read for specific files
|
|
1997
|
+
pattern = inp.get("pattern", "")
|
|
1998
|
+
path = inp.get("path", "")
|
|
1999
|
+
if pattern:
|
|
2000
|
+
if path and not is_discovery_grep(path):
|
|
2001
|
+
# Specific file - suggest Read instead
|
|
2002
|
+
print(f"STOP: Use Read(\\"{path}\\") to view file content, or mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") for codebase search.", file=sys.stderr)
|
|
2003
|
+
else:
|
|
2004
|
+
print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}.", file=sys.stderr)
|
|
2005
|
+
sys.exit(2)
|
|
2006
|
+
|
|
2007
|
+
elif tool == "Task":
|
|
2008
|
+
if inp.get("subagent_type", "").lower() == "explore":
|
|
2009
|
+
print("STOP: Use mcp__contextstream__search(mode=\\"hybrid\\") instead of Task(Explore).", file=sys.stderr)
|
|
2010
|
+
sys.exit(2)
|
|
2011
|
+
if inp.get("subagent_type", "").lower() == "plan":
|
|
2012
|
+
print("STOP: Use mcp__contextstream__session(action=\\"capture_plan\\") for planning. ContextStream plans persist across sessions.", file=sys.stderr)
|
|
2013
|
+
sys.exit(2)
|
|
2014
|
+
|
|
2015
|
+
elif tool == "EnterPlanMode":
|
|
2016
|
+
print("STOP: Use mcp__contextstream__session(action=\\"capture_plan\\", title=\\"...\\", steps=[...]) instead of EnterPlanMode. ContextStream plans persist across sessions and are searchable.", file=sys.stderr)
|
|
2017
|
+
sys.exit(2)
|
|
2018
|
+
|
|
2019
|
+
sys.exit(0)
|
|
2020
|
+
|
|
2021
|
+
if __name__ == "__main__":
|
|
2022
|
+
main()
|
|
2023
|
+
`;
|
|
2024
|
+
USER_PROMPT_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
2025
|
+
"""
|
|
2026
|
+
ContextStream UserPromptSubmit Hook - Injects reminder on every message.
|
|
2027
|
+
"""
|
|
2028
|
+
|
|
2029
|
+
import json
|
|
2030
|
+
import sys
|
|
2031
|
+
import os
|
|
2032
|
+
|
|
2033
|
+
ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
|
|
2034
|
+
|
|
2035
|
+
REMINDER = """[CONTEXTSTREAM RULES]
|
|
2036
|
+
1. BEFORE Glob/Grep/Read/Search: mcp__contextstream__search(mode="hybrid") FIRST
|
|
2037
|
+
2. Call context_smart at start of EVERY response
|
|
2038
|
+
3. Local tools ONLY if ContextStream returns 0 results
|
|
2039
|
+
[END RULES]"""
|
|
2040
|
+
|
|
2041
|
+
def main():
|
|
2042
|
+
if not ENABLED:
|
|
2043
|
+
sys.exit(0)
|
|
2044
|
+
|
|
2045
|
+
try:
|
|
2046
|
+
json.load(sys.stdin)
|
|
2047
|
+
except:
|
|
2048
|
+
sys.exit(0)
|
|
2049
|
+
|
|
2050
|
+
print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": REMINDER}}))
|
|
2051
|
+
sys.exit(0)
|
|
2052
|
+
|
|
2053
|
+
if __name__ == "__main__":
|
|
2054
|
+
main()
|
|
2055
|
+
`;
|
|
2056
|
+
MEDIA_AWARE_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
2057
|
+
"""
|
|
2058
|
+
ContextStream Media-Aware Hook for Claude Code
|
|
2059
|
+
|
|
2060
|
+
Detects media-related prompts and injects context about the media tool.
|
|
2061
|
+
"""
|
|
2062
|
+
|
|
2063
|
+
import json
|
|
2064
|
+
import sys
|
|
2065
|
+
import os
|
|
2066
|
+
import re
|
|
2067
|
+
|
|
2068
|
+
ENABLED = os.environ.get("CONTEXTSTREAM_MEDIA_HOOK_ENABLED", "true").lower() == "true"
|
|
2069
|
+
|
|
2070
|
+
# Media patterns (case-insensitive)
|
|
2071
|
+
PATTERNS = [
|
|
2072
|
+
r"\\b(video|videos|clip|clips|footage|keyframe)s?\\b",
|
|
2073
|
+
r"\\b(remotion|timeline|video\\s*edit)\\b",
|
|
2074
|
+
r"\\b(image|images|photo|photos|picture|thumbnail)s?\\b",
|
|
2075
|
+
r"\\b(audio|podcast|transcript|transcription|voice)\\b",
|
|
2076
|
+
r"\\b(media|asset|assets|creative|b-roll)\\b",
|
|
2077
|
+
r"\\b(find|search|show).*(clip|video|image|audio|footage|media)\\b",
|
|
2078
|
+
]
|
|
2079
|
+
|
|
2080
|
+
COMPILED = [re.compile(p, re.IGNORECASE) for p in PATTERNS]
|
|
2081
|
+
|
|
2082
|
+
MEDIA_CONTEXT = """[MEDIA TOOLS AVAILABLE]
|
|
2083
|
+
Your workspace may have indexed media. Use ContextStream media tools:
|
|
2084
|
+
|
|
2085
|
+
- **Search**: \`mcp__contextstream__media(action="search", query="description")\`
|
|
2086
|
+
- **Get clip**: \`mcp__contextstream__media(action="get_clip", content_id="...", start="1:34", end="2:15", output_format="remotion|ffmpeg|raw")\`
|
|
2087
|
+
- **List assets**: \`mcp__contextstream__media(action="list")\`
|
|
2088
|
+
- **Index**: \`mcp__contextstream__media(action="index", file_path="...", content_type="video|audio|image|document")\`
|
|
2089
|
+
|
|
2090
|
+
For Remotion: use \`output_format="remotion"\` to get frame-based props.
|
|
2091
|
+
[END MEDIA TOOLS]"""
|
|
2092
|
+
|
|
2093
|
+
def matches(text):
|
|
2094
|
+
return any(p.search(text) for p in COMPILED)
|
|
2095
|
+
|
|
2096
|
+
def main():
|
|
2097
|
+
if not ENABLED:
|
|
2098
|
+
sys.exit(0)
|
|
2099
|
+
|
|
2100
|
+
try:
|
|
2101
|
+
data = json.load(sys.stdin)
|
|
2102
|
+
except:
|
|
2103
|
+
sys.exit(0)
|
|
2104
|
+
|
|
2105
|
+
prompt = data.get("prompt", "")
|
|
2106
|
+
if not prompt:
|
|
2107
|
+
session = data.get("session", {})
|
|
2108
|
+
for msg in reversed(session.get("messages", [])):
|
|
2109
|
+
if msg.get("role") == "user":
|
|
2110
|
+
content = msg.get("content", "")
|
|
2111
|
+
prompt = content if isinstance(content, str) else ""
|
|
2112
|
+
if isinstance(content, list):
|
|
2113
|
+
for b in content:
|
|
2114
|
+
if isinstance(b, dict) and b.get("type") == "text":
|
|
2115
|
+
prompt = b.get("text", "")
|
|
2116
|
+
break
|
|
2117
|
+
break
|
|
2118
|
+
|
|
2119
|
+
if not prompt or not matches(prompt):
|
|
2120
|
+
sys.exit(0)
|
|
2121
|
+
|
|
2122
|
+
print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": MEDIA_CONTEXT}}))
|
|
2123
|
+
sys.exit(0)
|
|
2124
|
+
|
|
2125
|
+
if __name__ == "__main__":
|
|
2126
|
+
main()
|
|
2127
|
+
`;
|
|
2128
|
+
PRECOMPACT_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
2129
|
+
"""
|
|
2130
|
+
ContextStream PreCompact Hook for Claude Code
|
|
2131
|
+
|
|
2132
|
+
Runs BEFORE conversation context is compacted (manual via /compact or automatic).
|
|
2133
|
+
Automatically saves conversation state to ContextStream by parsing the transcript.
|
|
2134
|
+
|
|
2135
|
+
Input (via stdin):
|
|
2136
|
+
{
|
|
2137
|
+
"session_id": "...",
|
|
2138
|
+
"transcript_path": "/path/to/transcript.jsonl",
|
|
2139
|
+
"permission_mode": "default",
|
|
2140
|
+
"hook_event_name": "PreCompact",
|
|
2141
|
+
"trigger": "manual" | "auto",
|
|
2142
|
+
"custom_instructions": "..."
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
Output (to stdout):
|
|
2146
|
+
{
|
|
2147
|
+
"hookSpecificOutput": {
|
|
2148
|
+
"hookEventName": "PreCompact",
|
|
2149
|
+
"additionalContext": "... status message ..."
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
"""
|
|
2153
|
+
|
|
2154
|
+
import json
|
|
2155
|
+
import sys
|
|
2156
|
+
import os
|
|
2157
|
+
import re
|
|
2158
|
+
import urllib.request
|
|
2159
|
+
import urllib.error
|
|
2160
|
+
|
|
2161
|
+
ENABLED = os.environ.get("CONTEXTSTREAM_PRECOMPACT_ENABLED", "true").lower() == "true"
|
|
2162
|
+
AUTO_SAVE = os.environ.get("CONTEXTSTREAM_PRECOMPACT_AUTO_SAVE", "true").lower() == "true"
|
|
2163
|
+
API_URL = os.environ.get("CONTEXTSTREAM_API_URL", "https://api.contextstream.io")
|
|
2164
|
+
API_KEY = os.environ.get("CONTEXTSTREAM_API_KEY", "")
|
|
2165
|
+
|
|
2166
|
+
WORKSPACE_ID = None
|
|
2167
|
+
|
|
2168
|
+
def load_config_from_mcp_json(cwd):
|
|
2169
|
+
"""Load API config from .mcp.json if env vars not set."""
|
|
2170
|
+
global API_URL, API_KEY, WORKSPACE_ID
|
|
2171
|
+
|
|
2172
|
+
# Try to find .mcp.json and .contextstream/config.json in cwd or parent directories
|
|
2173
|
+
search_dir = cwd
|
|
2174
|
+
for _ in range(5): # Search up to 5 levels
|
|
2175
|
+
# Load API config from .mcp.json
|
|
2176
|
+
if not API_KEY:
|
|
2177
|
+
mcp_path = os.path.join(search_dir, ".mcp.json")
|
|
2178
|
+
if os.path.exists(mcp_path):
|
|
2179
|
+
try:
|
|
2180
|
+
with open(mcp_path, 'r') as f:
|
|
2181
|
+
config = json.load(f)
|
|
2182
|
+
servers = config.get("mcpServers", {})
|
|
2183
|
+
cs_config = servers.get("contextstream", {})
|
|
2184
|
+
env = cs_config.get("env", {})
|
|
2185
|
+
if env.get("CONTEXTSTREAM_API_KEY"):
|
|
2186
|
+
API_KEY = env["CONTEXTSTREAM_API_KEY"]
|
|
2187
|
+
if env.get("CONTEXTSTREAM_API_URL"):
|
|
2188
|
+
API_URL = env["CONTEXTSTREAM_API_URL"]
|
|
2189
|
+
except:
|
|
2190
|
+
pass
|
|
2191
|
+
|
|
2192
|
+
# Load workspace_id from .contextstream/config.json
|
|
2193
|
+
if not WORKSPACE_ID:
|
|
2194
|
+
cs_config_path = os.path.join(search_dir, ".contextstream", "config.json")
|
|
2195
|
+
if os.path.exists(cs_config_path):
|
|
2196
|
+
try:
|
|
2197
|
+
with open(cs_config_path, 'r') as f:
|
|
2198
|
+
cs_config = json.load(f)
|
|
2199
|
+
if cs_config.get("workspace_id"):
|
|
2200
|
+
WORKSPACE_ID = cs_config["workspace_id"]
|
|
2201
|
+
except:
|
|
2202
|
+
pass
|
|
2203
|
+
|
|
2204
|
+
parent = os.path.dirname(search_dir)
|
|
2205
|
+
if parent == search_dir:
|
|
2206
|
+
break
|
|
2207
|
+
search_dir = parent
|
|
2208
|
+
|
|
2209
|
+
def parse_transcript(transcript_path):
|
|
2210
|
+
"""Parse transcript to extract active files, decisions, and context."""
|
|
2211
|
+
active_files = set()
|
|
2212
|
+
recent_messages = []
|
|
2213
|
+
tool_calls = []
|
|
2214
|
+
|
|
2215
|
+
try:
|
|
2216
|
+
with open(transcript_path, 'r') as f:
|
|
2217
|
+
for line in f:
|
|
2218
|
+
try:
|
|
2219
|
+
entry = json.loads(line.strip())
|
|
2220
|
+
msg_type = entry.get("type", "")
|
|
2221
|
+
|
|
2222
|
+
# Extract files from tool calls
|
|
2223
|
+
if msg_type == "tool_use":
|
|
2224
|
+
tool_name = entry.get("name", "")
|
|
2225
|
+
tool_input = entry.get("input", {})
|
|
2226
|
+
tool_calls.append({"name": tool_name, "input": tool_input})
|
|
2227
|
+
|
|
2228
|
+
# Extract file paths from common tools
|
|
2229
|
+
if tool_name in ["Read", "Write", "Edit", "NotebookEdit"]:
|
|
2230
|
+
file_path = tool_input.get("file_path") or tool_input.get("notebook_path")
|
|
2231
|
+
if file_path:
|
|
2232
|
+
active_files.add(file_path)
|
|
2233
|
+
elif tool_name == "Glob":
|
|
2234
|
+
pattern = tool_input.get("pattern", "")
|
|
2235
|
+
if pattern:
|
|
2236
|
+
active_files.add(f"[glob:{pattern}]")
|
|
2237
|
+
|
|
2238
|
+
# Collect recent assistant messages for summary
|
|
2239
|
+
if msg_type == "assistant" and entry.get("content"):
|
|
2240
|
+
content = entry.get("content", "")
|
|
2241
|
+
if isinstance(content, str) and len(content) > 50:
|
|
2242
|
+
recent_messages.append(content[:500])
|
|
2243
|
+
|
|
2244
|
+
except json.JSONDecodeError:
|
|
2245
|
+
continue
|
|
2246
|
+
except Exception as e:
|
|
2247
|
+
pass
|
|
2248
|
+
|
|
2249
|
+
return {
|
|
2250
|
+
"active_files": list(active_files)[-20:], # Last 20 files
|
|
2251
|
+
"tool_call_count": len(tool_calls),
|
|
2252
|
+
"message_count": len(recent_messages),
|
|
2253
|
+
"last_tools": [t["name"] for t in tool_calls[-10:]], # Last 10 tool names
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
def save_snapshot(session_id, transcript_data, trigger):
|
|
2257
|
+
"""Save snapshot to ContextStream API."""
|
|
2258
|
+
if not API_KEY:
|
|
2259
|
+
return False, "No API key configured"
|
|
2260
|
+
|
|
2261
|
+
snapshot_content = {
|
|
2262
|
+
"session_id": session_id,
|
|
2263
|
+
"trigger": trigger,
|
|
2264
|
+
"captured_at": None, # API will set timestamp
|
|
2265
|
+
"active_files": transcript_data.get("active_files", []),
|
|
2266
|
+
"tool_call_count": transcript_data.get("tool_call_count", 0),
|
|
2267
|
+
"last_tools": transcript_data.get("last_tools", []),
|
|
2268
|
+
"auto_captured": True,
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
payload = {
|
|
2272
|
+
"event_type": "session_snapshot",
|
|
2273
|
+
"title": f"Auto Pre-compaction Snapshot ({trigger})",
|
|
2274
|
+
"content": json.dumps(snapshot_content),
|
|
2275
|
+
"importance": "high",
|
|
2276
|
+
"tags": ["session_snapshot", "pre_compaction", "auto_captured"],
|
|
2277
|
+
"source_type": "hook",
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
# Add workspace_id if available
|
|
2281
|
+
if WORKSPACE_ID:
|
|
2282
|
+
payload["workspace_id"] = WORKSPACE_ID
|
|
2283
|
+
|
|
2284
|
+
try:
|
|
2285
|
+
req = urllib.request.Request(
|
|
2286
|
+
f"{API_URL}/api/v1/memory/events",
|
|
2287
|
+
data=json.dumps(payload).encode('utf-8'),
|
|
2288
|
+
headers={
|
|
2289
|
+
"Content-Type": "application/json",
|
|
2290
|
+
"X-API-Key": API_KEY,
|
|
2291
|
+
},
|
|
2292
|
+
method="POST"
|
|
2293
|
+
)
|
|
2294
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
2295
|
+
return True, "Snapshot saved"
|
|
2296
|
+
except urllib.error.URLError as e:
|
|
2297
|
+
return False, str(e)
|
|
2298
|
+
except Exception as e:
|
|
2299
|
+
return False, str(e)
|
|
2300
|
+
|
|
2301
|
+
def main():
|
|
2302
|
+
if not ENABLED:
|
|
2303
|
+
sys.exit(0)
|
|
2304
|
+
|
|
2305
|
+
try:
|
|
2306
|
+
data = json.load(sys.stdin)
|
|
2307
|
+
except:
|
|
2308
|
+
sys.exit(0)
|
|
2309
|
+
|
|
2310
|
+
# Load config from .mcp.json if env vars not set
|
|
2311
|
+
cwd = data.get("cwd", os.getcwd())
|
|
2312
|
+
load_config_from_mcp_json(cwd)
|
|
2313
|
+
|
|
2314
|
+
session_id = data.get("session_id", "unknown")
|
|
2315
|
+
transcript_path = data.get("transcript_path", "")
|
|
2316
|
+
trigger = data.get("trigger", "unknown")
|
|
2317
|
+
custom_instructions = data.get("custom_instructions", "")
|
|
2318
|
+
|
|
2319
|
+
# Parse transcript for context
|
|
2320
|
+
transcript_data = {}
|
|
2321
|
+
if transcript_path and os.path.exists(transcript_path):
|
|
2322
|
+
transcript_data = parse_transcript(transcript_path)
|
|
2323
|
+
|
|
2324
|
+
# Auto-save snapshot if enabled
|
|
2325
|
+
auto_save_status = ""
|
|
2326
|
+
if AUTO_SAVE and API_KEY:
|
|
2327
|
+
success, msg = save_snapshot(session_id, transcript_data, trigger)
|
|
2328
|
+
if success:
|
|
2329
|
+
auto_save_status = f"\\n[ContextStream: Auto-saved snapshot with {len(transcript_data.get('active_files', []))} active files]"
|
|
2330
|
+
else:
|
|
2331
|
+
auto_save_status = f"\\n[ContextStream: Auto-save failed - {msg}]"
|
|
2332
|
+
|
|
2333
|
+
# Build context injection for the AI (backup in case auto-save fails)
|
|
2334
|
+
files_list = ", ".join(transcript_data.get("active_files", [])[:5]) or "none detected"
|
|
2335
|
+
context = f"""[CONTEXT COMPACTION - {trigger.upper()}]{auto_save_status}
|
|
2336
|
+
|
|
2337
|
+
Active files detected: {files_list}
|
|
2338
|
+
Tool calls in session: {transcript_data.get('tool_call_count', 0)}
|
|
2339
|
+
|
|
2340
|
+
After compaction, call session_init(is_post_compact=true) to restore context.
|
|
2341
|
+
{f"User instructions: {custom_instructions}" if custom_instructions else ""}"""
|
|
2342
|
+
|
|
2343
|
+
output = {
|
|
2344
|
+
"hookSpecificOutput": {
|
|
2345
|
+
"hookEventName": "PreCompact",
|
|
2346
|
+
"additionalContext": context
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
print(json.dumps(output))
|
|
2351
|
+
sys.exit(0)
|
|
2352
|
+
|
|
2353
|
+
if __name__ == "__main__":
|
|
2354
|
+
main()
|
|
2355
|
+
`;
|
|
2356
|
+
CLINE_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
2357
|
+
"""
|
|
2358
|
+
ContextStream PreToolUse Hook for Cline
|
|
2359
|
+
Blocks discovery tools and redirects to ContextStream search.
|
|
2360
|
+
|
|
2361
|
+
Cline hooks use JSON output format:
|
|
2362
|
+
{
|
|
2363
|
+
"cancel": true/false,
|
|
2364
|
+
"errorMessage": "optional error description",
|
|
2365
|
+
"contextModification": "optional text to inject"
|
|
2366
|
+
}
|
|
2367
|
+
"""
|
|
2368
|
+
|
|
2369
|
+
import json
|
|
2370
|
+
import sys
|
|
2371
|
+
import os
|
|
2372
|
+
from pathlib import Path
|
|
2373
|
+
from datetime import datetime, timedelta
|
|
2374
|
+
|
|
2375
|
+
ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
|
|
2376
|
+
INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
|
|
2377
|
+
STALE_THRESHOLD_DAYS = 7
|
|
2378
|
+
|
|
2379
|
+
DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
|
|
2380
|
+
|
|
2381
|
+
def is_discovery_glob(pattern):
|
|
2382
|
+
pattern_lower = pattern.lower()
|
|
2383
|
+
for p in DISCOVERY_PATTERNS:
|
|
2384
|
+
if p in pattern_lower:
|
|
2385
|
+
return True
|
|
2386
|
+
if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
|
|
2387
|
+
return True
|
|
2388
|
+
if "**" in pattern or "*/" in pattern:
|
|
2389
|
+
return True
|
|
2390
|
+
return False
|
|
2391
|
+
|
|
2392
|
+
def is_discovery_grep(file_path):
|
|
2393
|
+
if not file_path or file_path in [".", "./", "*", "**"]:
|
|
2394
|
+
return True
|
|
2395
|
+
if "*" in file_path or "**" in file_path:
|
|
2396
|
+
return True
|
|
2397
|
+
return False
|
|
2398
|
+
|
|
2399
|
+
def is_project_indexed(workspace_roots):
|
|
2400
|
+
"""Check if any workspace root is in an indexed project."""
|
|
2401
|
+
if not INDEX_STATUS_FILE.exists():
|
|
2402
|
+
return False, False
|
|
2403
|
+
|
|
2404
|
+
try:
|
|
2405
|
+
with open(INDEX_STATUS_FILE, "r") as f:
|
|
2406
|
+
data = json.load(f)
|
|
2407
|
+
except:
|
|
2408
|
+
return False, False
|
|
2409
|
+
|
|
2410
|
+
projects = data.get("projects", {})
|
|
2411
|
+
|
|
2412
|
+
for workspace in workspace_roots:
|
|
2413
|
+
cwd_path = Path(workspace).resolve()
|
|
2414
|
+
for project_path, info in projects.items():
|
|
2415
|
+
try:
|
|
2416
|
+
indexed_path = Path(project_path).resolve()
|
|
2417
|
+
if cwd_path == indexed_path or indexed_path in cwd_path.parents:
|
|
2418
|
+
indexed_at = info.get("indexed_at")
|
|
2419
|
+
if indexed_at:
|
|
2420
|
+
try:
|
|
2421
|
+
indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
|
|
2422
|
+
if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
|
|
2423
|
+
return True, True
|
|
2424
|
+
except:
|
|
2425
|
+
pass
|
|
2426
|
+
return True, False
|
|
2427
|
+
except:
|
|
2428
|
+
continue
|
|
2429
|
+
return False, False
|
|
2430
|
+
|
|
2431
|
+
def output_allow(context_mod=None):
|
|
2432
|
+
result = {"cancel": False}
|
|
2433
|
+
if context_mod:
|
|
2434
|
+
result["contextModification"] = context_mod
|
|
2435
|
+
print(json.dumps(result))
|
|
2436
|
+
sys.exit(0)
|
|
2437
|
+
|
|
2438
|
+
def output_block(error_msg, context_mod=None):
|
|
2439
|
+
result = {"cancel": True, "errorMessage": error_msg}
|
|
2440
|
+
if context_mod:
|
|
2441
|
+
result["contextModification"] = context_mod
|
|
2442
|
+
print(json.dumps(result))
|
|
2443
|
+
sys.exit(0)
|
|
2444
|
+
|
|
2445
|
+
def main():
|
|
2446
|
+
if not ENABLED:
|
|
2447
|
+
output_allow()
|
|
2448
|
+
|
|
2449
|
+
try:
|
|
2450
|
+
data = json.load(sys.stdin)
|
|
2451
|
+
except:
|
|
2452
|
+
output_allow()
|
|
2453
|
+
|
|
2454
|
+
hook_name = data.get("hookName", "")
|
|
2455
|
+
if hook_name != "PreToolUse":
|
|
2456
|
+
output_allow()
|
|
2457
|
+
|
|
2458
|
+
tool = data.get("toolName", "")
|
|
2459
|
+
params = data.get("toolParameters", {})
|
|
2460
|
+
workspace_roots = data.get("workspaceRoots", [])
|
|
2461
|
+
|
|
2462
|
+
# Check if project is indexed
|
|
2463
|
+
is_indexed, is_stale = is_project_indexed(workspace_roots)
|
|
2464
|
+
if not is_indexed:
|
|
2465
|
+
output_allow()
|
|
2466
|
+
|
|
2467
|
+
# Check for discovery patterns
|
|
2468
|
+
if tool == "list_files" or tool == "search_files":
|
|
2469
|
+
pattern = params.get("path", "") or params.get("regex", "")
|
|
2470
|
+
if is_discovery_glob(pattern) or is_discovery_grep(pattern):
|
|
2471
|
+
output_block(
|
|
2472
|
+
f"Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}. "
|
|
2473
|
+
"ContextStream search is indexed and faster. Only use local tools if ContextStream returns 0 results.",
|
|
2474
|
+
"[CONTEXTSTREAM] Use ContextStream search for code discovery."
|
|
2475
|
+
)
|
|
2476
|
+
|
|
2477
|
+
elif tool == "read_file":
|
|
2478
|
+
# Allow read_file by default - blocking discovery at search level is enough
|
|
2479
|
+
pass
|
|
2480
|
+
|
|
2481
|
+
output_allow()
|
|
2482
|
+
|
|
2483
|
+
if __name__ == "__main__":
|
|
2484
|
+
main()
|
|
2485
|
+
`;
|
|
2486
|
+
CLINE_USER_PROMPT_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
2487
|
+
"""
|
|
2488
|
+
ContextStream UserPromptSubmit Hook for Cline
|
|
2489
|
+
Injects reminder about ContextStream rules on every message.
|
|
2490
|
+
"""
|
|
2491
|
+
|
|
2492
|
+
import json
|
|
2493
|
+
import sys
|
|
2494
|
+
import os
|
|
2495
|
+
|
|
2496
|
+
ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
|
|
2497
|
+
|
|
2498
|
+
REMINDER = """[CONTEXTSTREAM RULES]
|
|
2499
|
+
1. BEFORE list_files/search_files/read_file: mcp__contextstream__search(mode="hybrid") FIRST
|
|
2500
|
+
2. Call context_smart at start of EVERY response
|
|
2501
|
+
3. Local tools ONLY if ContextStream returns 0 results
|
|
2502
|
+
[END RULES]"""
|
|
2503
|
+
|
|
2504
|
+
def main():
|
|
2505
|
+
if not ENABLED:
|
|
2506
|
+
print(json.dumps({"cancel": False}))
|
|
2507
|
+
sys.exit(0)
|
|
2508
|
+
|
|
2509
|
+
try:
|
|
2510
|
+
json.load(sys.stdin)
|
|
2511
|
+
except:
|
|
2512
|
+
print(json.dumps({"cancel": False}))
|
|
2513
|
+
sys.exit(0)
|
|
2514
|
+
|
|
2515
|
+
print(json.dumps({
|
|
2516
|
+
"cancel": False,
|
|
2517
|
+
"contextModification": REMINDER
|
|
2518
|
+
}))
|
|
2519
|
+
sys.exit(0)
|
|
2520
|
+
|
|
2521
|
+
if __name__ == "__main__":
|
|
2522
|
+
main()
|
|
2523
|
+
`;
|
|
2524
|
+
CLINE_POSTTOOLUSE_HOOK_SCRIPT = `#!/bin/bash
|
|
2525
|
+
# ContextStream PostToolUse Hook for Cline/Roo/Kilo Code
|
|
2526
|
+
# Indexes files after Edit/Write/NotebookEdit operations for real-time search updates.
|
|
2527
|
+
#
|
|
2528
|
+
# The hook receives JSON on stdin with tool_name and toolParameters.
|
|
2529
|
+
# Only runs for write operations (write_to_file, edit_file).
|
|
2530
|
+
|
|
2531
|
+
TOOL_NAME=$(cat | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('toolName', d.get('tool_name', '')))" 2>/dev/null)
|
|
2532
|
+
|
|
2533
|
+
case "$TOOL_NAME" in
|
|
2534
|
+
write_to_file|edit_file|Write|Edit|NotebookEdit)
|
|
2535
|
+
npx @contextstream/mcp-server hook post-write
|
|
2536
|
+
;;
|
|
2537
|
+
esac
|
|
2538
|
+
|
|
2539
|
+
exit 0
|
|
2540
|
+
`;
|
|
2541
|
+
CLINE_HOOK_WRAPPER = (hookName2) => {
|
|
2542
|
+
const command = getHookCommand(hookName2);
|
|
2543
|
+
return `#!/bin/bash
|
|
2544
|
+
# ContextStream ${hookName2} Hook Wrapper for Cline/Roo/Kilo Code
|
|
2545
|
+
exec ${command}
|
|
2546
|
+
`;
|
|
2547
|
+
};
|
|
2548
|
+
CURSOR_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
2549
|
+
"""
|
|
2550
|
+
ContextStream PreToolUse Hook for Cursor
|
|
2551
|
+
Blocks discovery tools and redirects to ContextStream search.
|
|
2552
|
+
|
|
2553
|
+
Cursor hooks use JSON output format:
|
|
2554
|
+
{
|
|
2555
|
+
"decision": "allow" | "deny",
|
|
2556
|
+
"reason": "optional error description"
|
|
2557
|
+
}
|
|
2558
|
+
"""
|
|
2559
|
+
|
|
2560
|
+
import json
|
|
2561
|
+
import sys
|
|
2562
|
+
import os
|
|
2563
|
+
from pathlib import Path
|
|
2564
|
+
from datetime import datetime, timedelta
|
|
2565
|
+
|
|
2566
|
+
ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
|
|
2567
|
+
INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
|
|
2568
|
+
STALE_THRESHOLD_DAYS = 7
|
|
2569
|
+
|
|
2570
|
+
DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
|
|
2571
|
+
|
|
2572
|
+
def is_discovery_glob(pattern):
|
|
2573
|
+
pattern_lower = pattern.lower()
|
|
2574
|
+
for p in DISCOVERY_PATTERNS:
|
|
2575
|
+
if p in pattern_lower:
|
|
2576
|
+
return True
|
|
2577
|
+
if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
|
|
2578
|
+
return True
|
|
2579
|
+
if "**" in pattern or "*/" in pattern:
|
|
2580
|
+
return True
|
|
2581
|
+
return False
|
|
2582
|
+
|
|
2583
|
+
def is_discovery_grep(file_path):
|
|
2584
|
+
if not file_path or file_path in [".", "./", "*", "**"]:
|
|
2585
|
+
return True
|
|
2586
|
+
if "*" in file_path or "**" in file_path:
|
|
2587
|
+
return True
|
|
2588
|
+
return False
|
|
2589
|
+
|
|
2590
|
+
def is_project_indexed(workspace_roots):
|
|
2591
|
+
"""Check if any workspace root is in an indexed project."""
|
|
2592
|
+
if not INDEX_STATUS_FILE.exists():
|
|
2593
|
+
return False, False
|
|
2594
|
+
|
|
2595
|
+
try:
|
|
2596
|
+
with open(INDEX_STATUS_FILE, "r") as f:
|
|
2597
|
+
data = json.load(f)
|
|
2598
|
+
except:
|
|
2599
|
+
return False, False
|
|
2600
|
+
|
|
2601
|
+
projects = data.get("projects", {})
|
|
2602
|
+
|
|
2603
|
+
for workspace in workspace_roots:
|
|
2604
|
+
cwd_path = Path(workspace).resolve()
|
|
2605
|
+
for project_path, info in projects.items():
|
|
2606
|
+
try:
|
|
2607
|
+
indexed_path = Path(project_path).resolve()
|
|
2608
|
+
if cwd_path == indexed_path or indexed_path in cwd_path.parents:
|
|
2609
|
+
indexed_at = info.get("indexed_at")
|
|
2610
|
+
if indexed_at:
|
|
2611
|
+
try:
|
|
2612
|
+
indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
|
|
2613
|
+
if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
|
|
2614
|
+
return True, True
|
|
2615
|
+
except:
|
|
2616
|
+
pass
|
|
2617
|
+
return True, False
|
|
2618
|
+
except:
|
|
2619
|
+
continue
|
|
2620
|
+
return False, False
|
|
2621
|
+
|
|
2622
|
+
def output_allow():
|
|
2623
|
+
print(json.dumps({"decision": "allow"}))
|
|
2624
|
+
sys.exit(0)
|
|
2625
|
+
|
|
2626
|
+
def output_deny(reason):
|
|
2627
|
+
print(json.dumps({"decision": "deny", "reason": reason}))
|
|
2628
|
+
sys.exit(0)
|
|
2629
|
+
|
|
2630
|
+
def main():
|
|
2631
|
+
if not ENABLED:
|
|
2632
|
+
output_allow()
|
|
2633
|
+
|
|
2634
|
+
try:
|
|
2635
|
+
data = json.load(sys.stdin)
|
|
2636
|
+
except:
|
|
2637
|
+
output_allow()
|
|
2638
|
+
|
|
2639
|
+
hook_name = data.get("hook_event_name", "")
|
|
2640
|
+
if hook_name != "preToolUse":
|
|
2641
|
+
output_allow()
|
|
2642
|
+
|
|
2643
|
+
tool = data.get("tool_name", "")
|
|
2644
|
+
params = data.get("tool_input", {}) or data.get("parameters", {})
|
|
2645
|
+
workspace_roots = data.get("workspace_roots", [])
|
|
2646
|
+
|
|
2647
|
+
# Check if project is indexed
|
|
2648
|
+
is_indexed, _ = is_project_indexed(workspace_roots)
|
|
2649
|
+
if not is_indexed:
|
|
2650
|
+
output_allow()
|
|
2651
|
+
|
|
2652
|
+
# Check for Cursor tools
|
|
2653
|
+
if tool in ["Glob", "glob", "list_files"]:
|
|
2654
|
+
pattern = params.get("pattern", "") or params.get("path", "")
|
|
2655
|
+
if is_discovery_glob(pattern):
|
|
2656
|
+
output_deny(
|
|
2657
|
+
f"Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}. "
|
|
2658
|
+
"ContextStream search is indexed and faster."
|
|
2659
|
+
)
|
|
2660
|
+
|
|
2661
|
+
elif tool in ["Grep", "grep", "search_files", "ripgrep"]:
|
|
2662
|
+
pattern = params.get("pattern", "") or params.get("regex", "")
|
|
2663
|
+
file_path = params.get("path", "")
|
|
2664
|
+
if is_discovery_grep(file_path):
|
|
2665
|
+
output_deny(
|
|
2666
|
+
f"Use mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") instead of {tool}. "
|
|
2667
|
+
"ContextStream search is indexed and faster."
|
|
2668
|
+
)
|
|
2669
|
+
|
|
2670
|
+
output_allow()
|
|
2671
|
+
|
|
2672
|
+
if __name__ == "__main__":
|
|
2673
|
+
main()
|
|
2674
|
+
`;
|
|
2675
|
+
CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
2676
|
+
"""
|
|
2677
|
+
ContextStream BeforeSubmitPrompt Hook for Cursor
|
|
2678
|
+
Injects reminder about ContextStream rules.
|
|
2679
|
+
"""
|
|
2680
|
+
|
|
2681
|
+
import json
|
|
2682
|
+
import sys
|
|
2683
|
+
import os
|
|
2684
|
+
|
|
2685
|
+
ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
|
|
2686
|
+
|
|
2687
|
+
def main():
|
|
2688
|
+
if not ENABLED:
|
|
2689
|
+
print(json.dumps({"continue": True}))
|
|
2690
|
+
sys.exit(0)
|
|
2691
|
+
|
|
2692
|
+
try:
|
|
2693
|
+
json.load(sys.stdin)
|
|
2694
|
+
except:
|
|
2695
|
+
print(json.dumps({"continue": True}))
|
|
2696
|
+
sys.exit(0)
|
|
2697
|
+
|
|
2698
|
+
print(json.dumps({
|
|
2699
|
+
"continue": True,
|
|
2700
|
+
"user_message": "[CONTEXTSTREAM] Search with mcp__contextstream__search before using Glob/Grep/Read"
|
|
2701
|
+
}))
|
|
2702
|
+
sys.exit(0)
|
|
2703
|
+
|
|
2704
|
+
if __name__ == "__main__":
|
|
2705
|
+
main()
|
|
2706
|
+
`;
|
|
2707
|
+
}
|
|
2708
|
+
});
|
|
2709
|
+
|
|
2710
|
+
// src/hooks/auto-rules.ts
|
|
2711
|
+
var auto_rules_exports = {};
|
|
2712
|
+
__export(auto_rules_exports, {
|
|
2713
|
+
runAutoRulesHook: () => runAutoRulesHook
|
|
2714
|
+
});
|
|
2715
|
+
import * as fs5 from "node:fs";
|
|
2716
|
+
import * as path5 from "node:path";
|
|
2717
|
+
import { homedir as homedir5 } from "node:os";
|
|
2718
|
+
function hasRunRecently() {
|
|
2719
|
+
try {
|
|
2720
|
+
if (!fs5.existsSync(MARKER_FILE)) return false;
|
|
2721
|
+
const stat = fs5.statSync(MARKER_FILE);
|
|
2722
|
+
const age = Date.now() - stat.mtimeMs;
|
|
2723
|
+
return age < COOLDOWN_MS;
|
|
2724
|
+
} catch {
|
|
2725
|
+
return false;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
function markAsRan() {
|
|
2729
|
+
try {
|
|
2730
|
+
const dir = path5.dirname(MARKER_FILE);
|
|
2731
|
+
if (!fs5.existsSync(dir)) {
|
|
2732
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
2733
|
+
}
|
|
2734
|
+
fs5.writeFileSync(MARKER_FILE, (/* @__PURE__ */ new Date()).toISOString());
|
|
2735
|
+
} catch {
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
function extractRulesNotice(input) {
|
|
2739
|
+
if (input.tool_result) {
|
|
2740
|
+
try {
|
|
2741
|
+
const parsed = JSON.parse(input.tool_result);
|
|
2742
|
+
if (parsed.rules_notice) return parsed.rules_notice;
|
|
2743
|
+
} catch {
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (input.tool_response?.structuredContent) {
|
|
2747
|
+
const sc = input.tool_response.structuredContent;
|
|
2748
|
+
if (sc.rules_notice) return sc.rules_notice;
|
|
2749
|
+
}
|
|
2750
|
+
if (input.response) {
|
|
2751
|
+
if (input.response.rules_notice) {
|
|
2752
|
+
return input.response.rules_notice;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
return null;
|
|
2756
|
+
}
|
|
2757
|
+
function extractCwd3(input) {
|
|
2758
|
+
if (input.cwd) return input.cwd;
|
|
2759
|
+
return process.cwd();
|
|
2760
|
+
}
|
|
2761
|
+
function hasPythonHooks(settingsPath) {
|
|
2762
|
+
try {
|
|
2763
|
+
if (!fs5.existsSync(settingsPath)) return false;
|
|
2764
|
+
const content = fs5.readFileSync(settingsPath, "utf-8");
|
|
2765
|
+
const settings = JSON.parse(content);
|
|
2766
|
+
const hooks2 = settings.hooks;
|
|
2767
|
+
if (!hooks2) return false;
|
|
2768
|
+
for (const hookType of Object.keys(hooks2)) {
|
|
2769
|
+
const matchers = hooks2[hookType];
|
|
2770
|
+
if (!Array.isArray(matchers)) continue;
|
|
2771
|
+
for (const matcher of matchers) {
|
|
2772
|
+
const hookList = matcher.hooks;
|
|
2773
|
+
if (!Array.isArray(hookList)) continue;
|
|
2774
|
+
for (const hook of hookList) {
|
|
2775
|
+
const cmd = hook.command || "";
|
|
2776
|
+
if (cmd.includes("python3") && cmd.includes("contextstream")) {
|
|
2777
|
+
return true;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
return false;
|
|
2783
|
+
} catch {
|
|
2784
|
+
return false;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
function detectPythonHooks(cwd) {
|
|
2788
|
+
const globalSettingsPath = path5.join(homedir5(), ".claude", "settings.json");
|
|
2789
|
+
const projectSettingsPath = path5.join(cwd, ".claude", "settings.json");
|
|
2790
|
+
return {
|
|
2791
|
+
global: hasPythonHooks(globalSettingsPath),
|
|
2792
|
+
project: hasPythonHooks(projectSettingsPath)
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
async function upgradeHooksForFolder(folderPath) {
|
|
2796
|
+
const { installClaudeCodeHooks: installClaudeCodeHooks2 } = await Promise.resolve().then(() => (init_hooks_config(), hooks_config_exports));
|
|
2797
|
+
await installClaudeCodeHooks2({
|
|
2798
|
+
scope: "both",
|
|
2799
|
+
projectPath: folderPath,
|
|
2800
|
+
includePreCompact: true,
|
|
2801
|
+
includeMediaAware: true,
|
|
2802
|
+
includePostWrite: true,
|
|
2803
|
+
includeAutoRules: true
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
async function runAutoRulesHook() {
|
|
2807
|
+
if (!ENABLED6) {
|
|
2808
|
+
process.exit(0);
|
|
2809
|
+
}
|
|
2810
|
+
if (hasRunRecently()) {
|
|
2811
|
+
process.exit(0);
|
|
2812
|
+
}
|
|
2813
|
+
let inputData = "";
|
|
2814
|
+
for await (const chunk of process.stdin) {
|
|
2815
|
+
inputData += chunk;
|
|
2816
|
+
}
|
|
2817
|
+
if (!inputData.trim()) {
|
|
2818
|
+
process.exit(0);
|
|
2819
|
+
}
|
|
2820
|
+
let input;
|
|
2821
|
+
try {
|
|
2822
|
+
input = JSON.parse(inputData);
|
|
2823
|
+
} catch {
|
|
2824
|
+
process.exit(0);
|
|
2825
|
+
}
|
|
2826
|
+
const toolName = input.tool_name || input.toolName || "";
|
|
2827
|
+
const isContextTool = toolName.includes("init") || toolName.includes("context") || toolName.includes("session_init") || toolName.includes("context_smart");
|
|
2828
|
+
if (!isContextTool) {
|
|
2829
|
+
process.exit(0);
|
|
2830
|
+
}
|
|
2831
|
+
const cwd = extractCwd3(input);
|
|
2832
|
+
const pythonHooks = detectPythonHooks(cwd);
|
|
2833
|
+
const hasPythonHooksToUpgrade = pythonHooks.global || pythonHooks.project;
|
|
2834
|
+
const rulesNotice = extractRulesNotice(input);
|
|
2835
|
+
const rulesNeedUpdate = rulesNotice && rulesNotice.status !== "current";
|
|
2836
|
+
if (!hasPythonHooksToUpgrade && !rulesNeedUpdate) {
|
|
2837
|
+
process.exit(0);
|
|
2838
|
+
}
|
|
2839
|
+
const folderPath = rulesNotice?.update_args?.folder_path || cwd;
|
|
2840
|
+
try {
|
|
2841
|
+
await upgradeHooksForFolder(folderPath);
|
|
2842
|
+
markAsRan();
|
|
2843
|
+
} catch {
|
|
2844
|
+
}
|
|
2845
|
+
process.exit(0);
|
|
2846
|
+
}
|
|
2847
|
+
var API_URL3, API_KEY3, ENABLED6, MARKER_FILE, COOLDOWN_MS, isDirectRun6;
|
|
2848
|
+
var init_auto_rules = __esm({
|
|
2849
|
+
"src/hooks/auto-rules.ts"() {
|
|
2850
|
+
"use strict";
|
|
2851
|
+
API_URL3 = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
2852
|
+
API_KEY3 = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
2853
|
+
ENABLED6 = process.env.CONTEXTSTREAM_AUTO_RULES !== "false";
|
|
2854
|
+
MARKER_FILE = path5.join(homedir5(), ".contextstream", ".auto-rules-ran");
|
|
2855
|
+
COOLDOWN_MS = 4 * 60 * 60 * 1e3;
|
|
2856
|
+
isDirectRun6 = process.argv[1]?.includes("auto-rules") || process.argv[2] === "auto-rules";
|
|
2857
|
+
if (isDirectRun6) {
|
|
2858
|
+
runAutoRulesHook().catch(() => process.exit(0));
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
});
|
|
2862
|
+
|
|
2863
|
+
// src/hooks/runner.ts
|
|
2864
|
+
var hookName = process.argv[2];
|
|
2865
|
+
if (!hookName) {
|
|
2866
|
+
console.error("Usage: contextstream-hook <hook-name>");
|
|
2867
|
+
console.error(
|
|
2868
|
+
"Available hooks: pre-tool-use, user-prompt-submit, media-aware, pre-compact, post-write, auto-rules"
|
|
2869
|
+
);
|
|
2870
|
+
process.exit(1);
|
|
2871
|
+
}
|
|
2872
|
+
var hooks = {
|
|
2873
|
+
"pre-tool-use": () => Promise.resolve().then(() => (init_pre_tool_use(), pre_tool_use_exports)),
|
|
2874
|
+
"user-prompt-submit": () => Promise.resolve().then(() => (init_user_prompt_submit(), user_prompt_submit_exports)),
|
|
2875
|
+
"media-aware": () => Promise.resolve().then(() => (init_media_aware(), media_aware_exports)),
|
|
2876
|
+
"pre-compact": () => Promise.resolve().then(() => (init_pre_compact(), pre_compact_exports)),
|
|
2877
|
+
"post-write": () => Promise.resolve().then(() => (init_post_write(), post_write_exports)),
|
|
2878
|
+
"auto-rules": () => Promise.resolve().then(() => (init_auto_rules(), auto_rules_exports))
|
|
2879
|
+
};
|
|
2880
|
+
var handler = hooks[hookName];
|
|
2881
|
+
if (!handler) {
|
|
2882
|
+
console.error(`Unknown hook: ${hookName}`);
|
|
2883
|
+
console.error(`Available: ${Object.keys(hooks).join(", ")}`);
|
|
2884
|
+
process.exit(1);
|
|
2885
|
+
}
|
|
2886
|
+
handler().catch((err) => {
|
|
2887
|
+
console.error(`Hook ${hookName} failed:`, err.message);
|
|
2888
|
+
process.exit(1);
|
|
2889
|
+
});
|