@contextstream/mcp-server 0.4.50 → 0.4.53

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