@contextstream/mcp-server 0.4.49 → 0.4.51

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