@contextstream/mcp-server 0.4.46 → 0.4.47

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.
@@ -9,813 +9,1505 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/version.ts
13
- import { createRequire } from "module";
14
- function getVersion() {
12
+ // src/hooks-config.ts
13
+ var hooks_config_exports = {};
14
+ __export(hooks_config_exports, {
15
+ CLINE_POSTTOOLUSE_HOOK_SCRIPT: () => CLINE_POSTTOOLUSE_HOOK_SCRIPT,
16
+ CLINE_PRETOOLUSE_HOOK_SCRIPT: () => CLINE_PRETOOLUSE_HOOK_SCRIPT,
17
+ CLINE_USER_PROMPT_HOOK_SCRIPT: () => CLINE_USER_PROMPT_HOOK_SCRIPT,
18
+ CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT: () => CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT,
19
+ CURSOR_PRETOOLUSE_HOOK_SCRIPT: () => CURSOR_PRETOOLUSE_HOOK_SCRIPT,
20
+ MEDIA_AWARE_HOOK_SCRIPT: () => MEDIA_AWARE_HOOK_SCRIPT,
21
+ PRECOMPACT_HOOK_SCRIPT: () => PRECOMPACT_HOOK_SCRIPT,
22
+ PRETOOLUSE_HOOK_SCRIPT: () => PRETOOLUSE_HOOK_SCRIPT,
23
+ USER_PROMPT_HOOK_SCRIPT: () => USER_PROMPT_HOOK_SCRIPT,
24
+ buildHooksConfig: () => buildHooksConfig,
25
+ generateAllHooksDocumentation: () => generateAllHooksDocumentation,
26
+ generateHooksDocumentation: () => generateHooksDocumentation,
27
+ getClaudeSettingsPath: () => getClaudeSettingsPath,
28
+ getClineHooksDir: () => getClineHooksDir,
29
+ getCursorHooksConfigPath: () => getCursorHooksConfigPath,
30
+ getCursorHooksDir: () => getCursorHooksDir,
31
+ getHooksDir: () => getHooksDir,
32
+ getIndexStatusPath: () => getIndexStatusPath,
33
+ getKiloCodeHooksDir: () => getKiloCodeHooksDir,
34
+ getRooCodeHooksDir: () => getRooCodeHooksDir,
35
+ installAllEditorHooks: () => installAllEditorHooks,
36
+ installClaudeCodeHooks: () => installClaudeCodeHooks,
37
+ installClineHookScripts: () => installClineHookScripts,
38
+ installCursorHookScripts: () => installCursorHookScripts,
39
+ installEditorHooks: () => installEditorHooks,
40
+ installHookScripts: () => installHookScripts,
41
+ installKiloCodeHookScripts: () => installKiloCodeHookScripts,
42
+ installRooCodeHookScripts: () => installRooCodeHookScripts,
43
+ markProjectIndexed: () => markProjectIndexed,
44
+ mergeHooksIntoSettings: () => mergeHooksIntoSettings,
45
+ readClaudeSettings: () => readClaudeSettings,
46
+ readCursorHooksConfig: () => readCursorHooksConfig,
47
+ readIndexStatus: () => readIndexStatus,
48
+ unmarkProjectIndexed: () => unmarkProjectIndexed,
49
+ writeClaudeSettings: () => writeClaudeSettings,
50
+ writeCursorHooksConfig: () => writeCursorHooksConfig,
51
+ writeIndexStatus: () => writeIndexStatus
52
+ });
53
+ import * as fs from "node:fs/promises";
54
+ import * as path from "node:path";
55
+ import { homedir } from "node:os";
56
+ function getClaudeSettingsPath(scope, projectPath) {
57
+ if (scope === "user") {
58
+ return path.join(homedir(), ".claude", "settings.json");
59
+ }
60
+ if (!projectPath) {
61
+ throw new Error("projectPath required for project scope");
62
+ }
63
+ return path.join(projectPath, ".claude", "settings.json");
64
+ }
65
+ function getHooksDir() {
66
+ return path.join(homedir(), ".claude", "hooks");
67
+ }
68
+ function buildHooksConfig(options) {
69
+ const userPromptHooks = [
70
+ {
71
+ matcher: "*",
72
+ hooks: [
73
+ {
74
+ type: "command",
75
+ command: "npx @contextstream/mcp-server hook user-prompt-submit",
76
+ timeout: 5
77
+ }
78
+ ]
79
+ }
80
+ ];
81
+ if (options?.includeMediaAware !== false) {
82
+ userPromptHooks.push({
83
+ matcher: "*",
84
+ hooks: [
85
+ {
86
+ type: "command",
87
+ command: "npx @contextstream/mcp-server hook media-aware",
88
+ timeout: 5
89
+ }
90
+ ]
91
+ });
92
+ }
93
+ const config = {
94
+ PreToolUse: [
95
+ {
96
+ matcher: "Glob|Grep|Search|Task|EnterPlanMode",
97
+ hooks: [
98
+ {
99
+ type: "command",
100
+ command: "npx @contextstream/mcp-server hook pre-tool-use",
101
+ timeout: 5
102
+ }
103
+ ]
104
+ }
105
+ ],
106
+ UserPromptSubmit: userPromptHooks
107
+ };
108
+ if (options?.includePreCompact) {
109
+ config.PreCompact = [
110
+ {
111
+ // Match both manual (/compact) and automatic compaction
112
+ matcher: "*",
113
+ hooks: [
114
+ {
115
+ type: "command",
116
+ command: "npx @contextstream/mcp-server hook pre-compact",
117
+ timeout: 10
118
+ }
119
+ ]
120
+ }
121
+ ];
122
+ }
123
+ const postToolUseHooks = [];
124
+ if (options?.includePostWrite !== false) {
125
+ postToolUseHooks.push({
126
+ matcher: "Edit|Write|NotebookEdit",
127
+ hooks: [
128
+ {
129
+ type: "command",
130
+ command: "npx @contextstream/mcp-server hook post-write",
131
+ timeout: 10
132
+ }
133
+ ]
134
+ });
135
+ }
136
+ if (options?.includeAutoRules !== false) {
137
+ postToolUseHooks.push({
138
+ matcher: "mcp__contextstream__init|mcp__contextstream__context",
139
+ hooks: [
140
+ {
141
+ type: "command",
142
+ command: "npx @contextstream/mcp-server hook auto-rules",
143
+ timeout: 15
144
+ }
145
+ ]
146
+ });
147
+ }
148
+ if (postToolUseHooks.length > 0) {
149
+ config.PostToolUse = postToolUseHooks;
150
+ }
151
+ return config;
152
+ }
153
+ async function installHookScripts(options) {
154
+ const hooksDir = getHooksDir();
155
+ await fs.mkdir(hooksDir, { recursive: true });
156
+ const result = {
157
+ preToolUse: "npx @contextstream/mcp-server hook pre-tool-use",
158
+ userPrompt: "npx @contextstream/mcp-server hook user-prompt-submit"
159
+ };
160
+ if (options?.includePreCompact) {
161
+ result.preCompact = "npx @contextstream/mcp-server hook pre-compact";
162
+ }
163
+ if (options?.includeMediaAware !== false) {
164
+ result.mediaAware = "npx @contextstream/mcp-server hook media-aware";
165
+ }
166
+ if (options?.includeAutoRules !== false) {
167
+ result.autoRules = "npx @contextstream/mcp-server hook auto-rules";
168
+ }
169
+ return result;
170
+ }
171
+ async function readClaudeSettings(scope, projectPath) {
172
+ const settingsPath = getClaudeSettingsPath(scope, projectPath);
15
173
  try {
16
- const require2 = createRequire(import.meta.url);
17
- const pkg = require2("../package.json");
18
- const version = pkg?.version;
19
- if (typeof version === "string" && version.trim()) return version.trim();
174
+ const content = await fs.readFile(settingsPath, "utf-8");
175
+ return JSON.parse(content);
20
176
  } catch {
177
+ return {};
21
178
  }
22
- return "unknown";
23
179
  }
24
- var VERSION, CACHE_TTL_MS;
25
- var init_version = __esm({
26
- "src/version.ts"() {
27
- "use strict";
28
- VERSION = getVersion();
29
- CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
180
+ async function writeClaudeSettings(settings, scope, projectPath) {
181
+ const settingsPath = getClaudeSettingsPath(scope, projectPath);
182
+ const dir = path.dirname(settingsPath);
183
+ await fs.mkdir(dir, { recursive: true });
184
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
185
+ }
186
+ function mergeHooksIntoSettings(existingSettings, newHooks) {
187
+ const settings = { ...existingSettings };
188
+ const existingHooks = settings.hooks || {};
189
+ for (const [hookType, matchers] of Object.entries(newHooks || {})) {
190
+ if (!matchers) continue;
191
+ const existing = existingHooks?.[hookType] || [];
192
+ const filtered = existing.filter((m) => {
193
+ return !m.hooks?.some((h) => h.command?.includes("contextstream"));
194
+ });
195
+ existingHooks[hookType] = [...filtered, ...matchers];
30
196
  }
31
- });
32
-
33
- // src/rules-templates.ts
34
- var rules_templates_exports = {};
35
- __export(rules_templates_exports, {
36
- RULES_VERSION: () => RULES_VERSION,
37
- TEMPLATES: () => TEMPLATES,
38
- generateAllRuleFiles: () => generateAllRuleFiles,
39
- generateRuleContent: () => generateRuleContent,
40
- getAvailableEditors: () => getAvailableEditors,
41
- getTemplate: () => getTemplate
42
- });
43
- function applyMcpToolPrefix(markdown, toolPrefix) {
44
- const toolPattern = CONTEXTSTREAM_TOOL_NAMES.join("|");
45
- const toolRegex = new RegExp(`(?<!__)\\b(${toolPattern})\\b(?=\\s*\\()`, "g");
46
- return markdown.replace(toolRegex, `${toolPrefix}$1`);
47
- }
48
- function getAvailableEditors() {
49
- return Object.keys(TEMPLATES);
50
- }
51
- function getTemplate(editor) {
52
- return TEMPLATES[editor.toLowerCase()] || null;
53
- }
54
- function generateRuleContent(editor, options) {
55
- const template = getTemplate(editor);
56
- if (!template) return null;
57
- const mode = options?.mode || "dynamic";
58
- const rules = mode === "full" ? CONTEXTSTREAM_RULES_FULL : mode === "minimal" ? CONTEXTSTREAM_RULES_MINIMAL : CONTEXTSTREAM_RULES_DYNAMIC;
59
- let content = template.build(rules);
60
- if (options?.workspaceName || options?.projectName) {
61
- const header = `
62
- # Workspace: ${options.workspaceName || "Unknown"}
63
- ${options.projectName ? `# Project: ${options.projectName}` : ""}
64
- ${options.workspaceId ? `# Workspace ID: ${options.workspaceId}` : ""}
65
-
66
- `;
67
- content = header + content;
197
+ settings.hooks = existingHooks;
198
+ return settings;
199
+ }
200
+ async function installClaudeCodeHooks(options) {
201
+ const result = { scripts: [], settings: [] };
202
+ result.scripts.push(
203
+ "npx @contextstream/mcp-server hook pre-tool-use",
204
+ "npx @contextstream/mcp-server hook user-prompt-submit"
205
+ );
206
+ if (options.includePreCompact) {
207
+ result.scripts.push("npx @contextstream/mcp-server hook pre-compact");
208
+ }
209
+ if (options.includeMediaAware !== false) {
210
+ result.scripts.push("npx @contextstream/mcp-server hook media-aware");
211
+ }
212
+ if (options.includePostWrite !== false) {
213
+ result.scripts.push("npx @contextstream/mcp-server hook post-write");
214
+ }
215
+ if (options.includeAutoRules !== false) {
216
+ result.scripts.push("npx @contextstream/mcp-server hook auto-rules");
217
+ }
218
+ const hooksConfig = buildHooksConfig({
219
+ includePreCompact: options.includePreCompact,
220
+ includeMediaAware: options.includeMediaAware,
221
+ includePostWrite: options.includePostWrite,
222
+ includeAutoRules: options.includeAutoRules
223
+ });
224
+ if (options.scope === "user" || options.scope === "both") {
225
+ const settingsPath = getClaudeSettingsPath("user");
226
+ if (!options.dryRun) {
227
+ const existing = await readClaudeSettings("user");
228
+ const merged = mergeHooksIntoSettings(existing, hooksConfig);
229
+ await writeClaudeSettings(merged, "user");
230
+ }
231
+ result.settings.push(settingsPath);
232
+ }
233
+ if ((options.scope === "project" || options.scope === "both") && options.projectPath) {
234
+ const settingsPath = getClaudeSettingsPath("project", options.projectPath);
235
+ if (!options.dryRun) {
236
+ const existing = await readClaudeSettings("project", options.projectPath);
237
+ const merged = mergeHooksIntoSettings(existing, hooksConfig);
238
+ await writeClaudeSettings(merged, "project", options.projectPath);
239
+ }
240
+ result.settings.push(settingsPath);
241
+ }
242
+ return result;
243
+ }
244
+ function generateHooksDocumentation() {
245
+ return `
246
+ ## Claude Code Hooks (ContextStream)
247
+
248
+ ContextStream installs hooks to enforce ContextStream-first behavior.
249
+ All hooks run via Node.js - no Python dependency required.
250
+
251
+ ### PreToolUse Hook
252
+ - **Command:** \`npx @contextstream/mcp-server hook pre-tool-use\`
253
+ - **Purpose:** Blocks Glob/Grep/Search/EnterPlanMode and redirects to ContextStream
254
+ - **Blocked tools:** Glob, Grep, Search, Task(Explore), Task(Plan), EnterPlanMode
255
+ - **Disable:** Set \`CONTEXTSTREAM_HOOK_ENABLED=false\` environment variable
256
+
257
+ ### UserPromptSubmit Hook
258
+ - **Command:** \`npx @contextstream/mcp-server hook user-prompt-submit\`
259
+ - **Purpose:** Injects a reminder about ContextStream rules on every message
260
+ - **Disable:** Set \`CONTEXTSTREAM_REMINDER_ENABLED=false\` environment variable
261
+
262
+ ### Media-Aware Hook
263
+ - **Command:** \`npx @contextstream/mcp-server hook media-aware\`
264
+ - **Purpose:** Detects media-related prompts and injects media tool guidance
265
+ - **Triggers:** Patterns like video, clips, Remotion, image, audio, creative assets
266
+ - **Disable:** Set \`CONTEXTSTREAM_MEDIA_HOOK_ENABLED=false\` environment variable
267
+
268
+ When Media-Aware hook detects media patterns, it injects context about:
269
+ - How to search indexed media assets
270
+ - How to get clips for Remotion (with frame-based props)
271
+ - How to index new media files
272
+
273
+ ### PreCompact Hook (Optional)
274
+ - **Command:** \`npx @contextstream/mcp-server hook pre-compact\`
275
+ - **Purpose:** Saves conversation state before context compaction
276
+ - **Triggers:** Both manual (/compact) and automatic compaction
277
+ - **Disable:** Set \`CONTEXTSTREAM_PRECOMPACT_ENABLED=false\` environment variable
278
+ - **Note:** Enable with \`generate_rules(include_pre_compact=true)\` to activate
279
+
280
+ When PreCompact runs, it:
281
+ 1. Parses the transcript for active files and tool calls
282
+ 2. Saves a session_snapshot to ContextStream API
283
+ 3. Injects context about using \`session_init(is_post_compact=true)\` after compaction
284
+
285
+ ### PostToolUse Hook (Real-time Indexing)
286
+ - **Command:** \`npx @contextstream/mcp-server hook post-write\`
287
+ - **Purpose:** Indexes files immediately after Edit/Write/NotebookEdit operations
288
+ - **Matcher:** Edit|Write|NotebookEdit
289
+ - **Disable:** Set \`CONTEXTSTREAM_POSTWRITE_ENABLED=false\` environment variable
290
+
291
+ ### Why Hooks?
292
+ Claude Code has strong built-in behaviors to use its default tools (Grep, Glob, Read)
293
+ and its built-in plan mode. CLAUDE.md instructions decay over long conversations.
294
+ Hooks provide:
295
+ 1. **Physical enforcement** - Blocked tools can't be used
296
+ 2. **Continuous reminders** - Rules stay in recent context
297
+ 3. **Better UX** - Faster searches via indexed ContextStream
298
+ 4. **Persistent plans** - ContextStream plans survive across sessions
299
+ 5. **Compaction awareness** - Save state before context is compacted
300
+ 6. **Real-time indexing** - Files indexed immediately after writes
301
+
302
+ ### Manual Configuration
303
+ If you prefer to configure manually, add to \`~/.claude/settings.json\`:
304
+ \`\`\`json
305
+ {
306
+ "hooks": {
307
+ "PreToolUse": [{
308
+ "matcher": "Glob|Grep|Search|Task|EnterPlanMode",
309
+ "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook pre-tool-use"}]
310
+ }],
311
+ "UserPromptSubmit": [
312
+ {
313
+ "matcher": "*",
314
+ "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook user-prompt-submit"}]
315
+ },
316
+ {
317
+ "matcher": "*",
318
+ "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook media-aware"}]
319
+ }
320
+ ],
321
+ "PreCompact": [{
322
+ "matcher": "*",
323
+ "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook pre-compact", "timeout": 10}]
324
+ }],
325
+ "PostToolUse": [{
326
+ "matcher": "Edit|Write|NotebookEdit",
327
+ "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook post-write", "timeout": 10}]
328
+ }]
329
+ }
330
+ }
331
+ \`\`\`
332
+ `.trim();
333
+ }
334
+ function getIndexStatusPath() {
335
+ return path.join(homedir(), ".contextstream", "indexed-projects.json");
336
+ }
337
+ async function readIndexStatus() {
338
+ const statusPath = getIndexStatusPath();
339
+ try {
340
+ const content = await fs.readFile(statusPath, "utf-8");
341
+ return JSON.parse(content);
342
+ } catch {
343
+ return { version: 1, projects: {} };
344
+ }
345
+ }
346
+ async function writeIndexStatus(status) {
347
+ const statusPath = getIndexStatusPath();
348
+ const dir = path.dirname(statusPath);
349
+ await fs.mkdir(dir, { recursive: true });
350
+ await fs.writeFile(statusPath, JSON.stringify(status, null, 2));
351
+ }
352
+ async function markProjectIndexed(projectPath, options) {
353
+ const status = await readIndexStatus();
354
+ const resolvedPath = path.resolve(projectPath);
355
+ status.projects[resolvedPath] = {
356
+ indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
357
+ project_id: options?.project_id,
358
+ project_name: options?.project_name
359
+ };
360
+ await writeIndexStatus(status);
361
+ }
362
+ async function unmarkProjectIndexed(projectPath) {
363
+ const status = await readIndexStatus();
364
+ const resolvedPath = path.resolve(projectPath);
365
+ delete status.projects[resolvedPath];
366
+ await writeIndexStatus(status);
367
+ }
368
+ function getClineHooksDir(scope, projectPath) {
369
+ if (scope === "global") {
370
+ return path.join(homedir(), "Documents", "Cline", "Rules", "Hooks");
371
+ }
372
+ if (!projectPath) {
373
+ throw new Error("projectPath required for project scope");
374
+ }
375
+ return path.join(projectPath, ".clinerules", "hooks");
376
+ }
377
+ async function installClineHookScripts(options) {
378
+ const hooksDir = getClineHooksDir(options.scope, options.projectPath);
379
+ await fs.mkdir(hooksDir, { recursive: true });
380
+ const preToolUsePath = path.join(hooksDir, "PreToolUse");
381
+ const userPromptPath = path.join(hooksDir, "UserPromptSubmit");
382
+ const postToolUsePath = path.join(hooksDir, "PostToolUse");
383
+ await fs.writeFile(preToolUsePath, CLINE_HOOK_WRAPPER("pre-tool-use"), { mode: 493 });
384
+ await fs.writeFile(userPromptPath, CLINE_HOOK_WRAPPER("user-prompt-submit"), { mode: 493 });
385
+ const result = {
386
+ preToolUse: preToolUsePath,
387
+ userPromptSubmit: userPromptPath
388
+ };
389
+ if (options.includePostWrite !== false) {
390
+ await fs.writeFile(postToolUsePath, CLINE_HOOK_WRAPPER("post-write"), { mode: 493 });
391
+ result.postToolUse = postToolUsePath;
392
+ }
393
+ return result;
394
+ }
395
+ function getRooCodeHooksDir(scope, projectPath) {
396
+ if (scope === "global") {
397
+ return path.join(homedir(), ".roo", "hooks");
398
+ }
399
+ if (!projectPath) {
400
+ throw new Error("projectPath required for project scope");
401
+ }
402
+ return path.join(projectPath, ".roo", "hooks");
403
+ }
404
+ async function installRooCodeHookScripts(options) {
405
+ const hooksDir = getRooCodeHooksDir(options.scope, options.projectPath);
406
+ await fs.mkdir(hooksDir, { recursive: true });
407
+ const preToolUsePath = path.join(hooksDir, "PreToolUse");
408
+ const userPromptPath = path.join(hooksDir, "UserPromptSubmit");
409
+ const postToolUsePath = path.join(hooksDir, "PostToolUse");
410
+ await fs.writeFile(preToolUsePath, CLINE_HOOK_WRAPPER("pre-tool-use"), { mode: 493 });
411
+ await fs.writeFile(userPromptPath, CLINE_HOOK_WRAPPER("user-prompt-submit"), { mode: 493 });
412
+ const result = {
413
+ preToolUse: preToolUsePath,
414
+ userPromptSubmit: userPromptPath
415
+ };
416
+ if (options.includePostWrite !== false) {
417
+ await fs.writeFile(postToolUsePath, CLINE_HOOK_WRAPPER("post-write"), { mode: 493 });
418
+ result.postToolUse = postToolUsePath;
419
+ }
420
+ return result;
421
+ }
422
+ function getKiloCodeHooksDir(scope, projectPath) {
423
+ if (scope === "global") {
424
+ return path.join(homedir(), ".kilocode", "hooks");
425
+ }
426
+ if (!projectPath) {
427
+ throw new Error("projectPath required for project scope");
428
+ }
429
+ return path.join(projectPath, ".kilocode", "hooks");
430
+ }
431
+ async function installKiloCodeHookScripts(options) {
432
+ const hooksDir = getKiloCodeHooksDir(options.scope, options.projectPath);
433
+ await fs.mkdir(hooksDir, { recursive: true });
434
+ const preToolUsePath = path.join(hooksDir, "PreToolUse");
435
+ const userPromptPath = path.join(hooksDir, "UserPromptSubmit");
436
+ const postToolUsePath = path.join(hooksDir, "PostToolUse");
437
+ await fs.writeFile(preToolUsePath, CLINE_HOOK_WRAPPER("pre-tool-use"), { mode: 493 });
438
+ await fs.writeFile(userPromptPath, CLINE_HOOK_WRAPPER("user-prompt-submit"), { mode: 493 });
439
+ const result = {
440
+ preToolUse: preToolUsePath,
441
+ userPromptSubmit: userPromptPath
442
+ };
443
+ if (options.includePostWrite !== false) {
444
+ await fs.writeFile(postToolUsePath, CLINE_HOOK_WRAPPER("post-write"), { mode: 493 });
445
+ result.postToolUse = postToolUsePath;
446
+ }
447
+ return result;
448
+ }
449
+ function getCursorHooksConfigPath(scope, projectPath) {
450
+ if (scope === "global") {
451
+ return path.join(homedir(), ".cursor", "hooks.json");
68
452
  }
69
- if (options?.additionalRules) {
70
- content += "\n\n## Project-Specific Rules\n\n" + options.additionalRules;
453
+ if (!projectPath) {
454
+ throw new Error("projectPath required for project scope");
455
+ }
456
+ return path.join(projectPath, ".cursor", "hooks.json");
457
+ }
458
+ function getCursorHooksDir(scope, projectPath) {
459
+ if (scope === "global") {
460
+ return path.join(homedir(), ".cursor", "hooks");
71
461
  }
72
- if (editor.toLowerCase() === "claude") {
73
- content = applyMcpToolPrefix(content, `mcp__${DEFAULT_CLAUDE_MCP_SERVER_NAME}__`);
462
+ if (!projectPath) {
463
+ throw new Error("projectPath required for project scope");
464
+ }
465
+ return path.join(projectPath, ".cursor", "hooks");
466
+ }
467
+ async function readCursorHooksConfig(scope, projectPath) {
468
+ const configPath = getCursorHooksConfigPath(scope, projectPath);
469
+ try {
470
+ const content = await fs.readFile(configPath, "utf-8");
471
+ return JSON.parse(content);
472
+ } catch {
473
+ return { version: 1, hooks: {} };
74
474
  }
475
+ }
476
+ async function writeCursorHooksConfig(config, scope, projectPath) {
477
+ const configPath = getCursorHooksConfigPath(scope, projectPath);
478
+ const dir = path.dirname(configPath);
479
+ await fs.mkdir(dir, { recursive: true });
480
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
481
+ }
482
+ async function installCursorHookScripts(options) {
483
+ const hooksDir = getCursorHooksDir(options.scope, options.projectPath);
484
+ await fs.mkdir(hooksDir, { recursive: true });
485
+ const existingConfig = await readCursorHooksConfig(options.scope, options.projectPath);
486
+ const filterContextStreamHooks = (hooks) => {
487
+ if (!hooks) return [];
488
+ return hooks.filter((h) => {
489
+ const hook = h;
490
+ return !hook.command?.includes("contextstream");
491
+ });
492
+ };
493
+ const filteredPreToolUse = filterContextStreamHooks(existingConfig.hooks.preToolUse);
494
+ const filteredBeforeSubmit = filterContextStreamHooks(existingConfig.hooks.beforeSubmitPrompt);
495
+ const config = {
496
+ version: 1,
497
+ hooks: {
498
+ ...existingConfig.hooks,
499
+ preToolUse: [
500
+ ...filteredPreToolUse,
501
+ {
502
+ command: "npx @contextstream/mcp-server hook pre-tool-use",
503
+ type: "command",
504
+ timeout: 5,
505
+ matcher: { tool_name: "Glob|Grep|search_files|list_files|ripgrep" }
506
+ }
507
+ ],
508
+ beforeSubmitPrompt: [
509
+ ...filteredBeforeSubmit,
510
+ {
511
+ command: "npx @contextstream/mcp-server hook user-prompt-submit",
512
+ type: "command",
513
+ timeout: 5
514
+ }
515
+ ]
516
+ }
517
+ };
518
+ await writeCursorHooksConfig(config, options.scope, options.projectPath);
519
+ const configPath = getCursorHooksConfigPath(options.scope, options.projectPath);
75
520
  return {
76
- filename: template.filename,
77
- content: content.trim() + "\n"
521
+ preToolUse: "npx @contextstream/mcp-server hook pre-tool-use",
522
+ beforeSubmitPrompt: "npx @contextstream/mcp-server hook user-prompt-submit",
523
+ config: configPath
78
524
  };
79
525
  }
80
- function generateAllRuleFiles(options) {
81
- return getAvailableEditors().map((editor) => {
82
- const result = generateRuleContent(editor, options);
83
- if (!result) return null;
84
- return { editor, ...result };
85
- }).filter((r) => r !== null);
526
+ async function installEditorHooks(options) {
527
+ const { editor, scope, projectPath, includePreCompact, includePostWrite } = options;
528
+ switch (editor) {
529
+ case "claude": {
530
+ if (scope === "project" && !projectPath) {
531
+ throw new Error("projectPath required for project scope");
532
+ }
533
+ const scripts = await installHookScripts({ includePreCompact });
534
+ const hooksConfig = buildHooksConfig({ includePreCompact, includePostWrite });
535
+ const settingsScope = scope === "global" ? "user" : "project";
536
+ const existing = await readClaudeSettings(settingsScope, projectPath);
537
+ const merged = mergeHooksIntoSettings(existing, hooksConfig);
538
+ await writeClaudeSettings(merged, settingsScope, projectPath);
539
+ const installed = [scripts.preToolUse, scripts.userPrompt];
540
+ if (scripts.preCompact) installed.push(scripts.preCompact);
541
+ return {
542
+ editor: "claude",
543
+ installed,
544
+ hooksDir: getHooksDir()
545
+ };
546
+ }
547
+ case "cline": {
548
+ const scripts = await installClineHookScripts({ scope, projectPath, includePostWrite });
549
+ const installed = [scripts.preToolUse, scripts.userPromptSubmit];
550
+ if (scripts.postToolUse) installed.push(scripts.postToolUse);
551
+ return {
552
+ editor: "cline",
553
+ installed,
554
+ hooksDir: getClineHooksDir(scope, projectPath)
555
+ };
556
+ }
557
+ case "roo": {
558
+ const scripts = await installRooCodeHookScripts({ scope, projectPath, includePostWrite });
559
+ const installed = [scripts.preToolUse, scripts.userPromptSubmit];
560
+ if (scripts.postToolUse) installed.push(scripts.postToolUse);
561
+ return {
562
+ editor: "roo",
563
+ installed,
564
+ hooksDir: getRooCodeHooksDir(scope, projectPath)
565
+ };
566
+ }
567
+ case "kilo": {
568
+ const scripts = await installKiloCodeHookScripts({ scope, projectPath, includePostWrite });
569
+ const installed = [scripts.preToolUse, scripts.userPromptSubmit];
570
+ if (scripts.postToolUse) installed.push(scripts.postToolUse);
571
+ return {
572
+ editor: "kilo",
573
+ installed,
574
+ hooksDir: getKiloCodeHooksDir(scope, projectPath)
575
+ };
576
+ }
577
+ case "cursor": {
578
+ const scripts = await installCursorHookScripts({ scope, projectPath });
579
+ return {
580
+ editor: "cursor",
581
+ installed: [scripts.preToolUse, scripts.beforeSubmitPrompt],
582
+ hooksDir: getCursorHooksDir(scope, projectPath)
583
+ };
584
+ }
585
+ default:
586
+ throw new Error(`Unsupported editor: ${editor}`);
587
+ }
86
588
  }
87
- var DEFAULT_CLAUDE_MCP_SERVER_NAME, RULES_VERSION, CONTEXTSTREAM_TOOL_NAMES, CONTEXTSTREAM_RULES_DYNAMIC, CONTEXTSTREAM_RULES_FULL, CONTEXTSTREAM_RULES_MINIMAL, TEMPLATES;
88
- var init_rules_templates = __esm({
89
- "src/rules-templates.ts"() {
90
- "use strict";
91
- init_version();
92
- DEFAULT_CLAUDE_MCP_SERVER_NAME = "contextstream";
93
- RULES_VERSION = VERSION === "unknown" ? "0.0.0" : VERSION;
94
- CONTEXTSTREAM_TOOL_NAMES = [
95
- // Standalone tools (always present)
96
- "init",
97
- // Renamed from session_init - initialize conversation session
98
- "context",
99
- // Renamed from context_smart - get relevant context every message
100
- "context_feedback",
101
- "generate_rules",
102
- // Consolidated domain tools (v0.4.x default)
103
- "search",
104
- // Modes: semantic, hybrid, keyword, pattern
105
- "session",
106
- // Actions: capture, capture_lesson, get_lessons, recall, remember, user_context, summary, compress, delta, smart_search, decision_trace
107
- "memory",
108
- // Actions: create_event, get_event, update_event, delete_event, list_events, distill_event, create_node, get_node, update_node, delete_node, list_nodes, supersede_node, search, decisions, timeline, summary
109
- "graph",
110
- // Actions: dependencies, impact, call_path, related, path, decisions, ingest, circular_dependencies, unused_code, contradictions
111
- "project",
112
- // Actions: list, get, create, update, index, overview, statistics, files, index_status, ingest_local
113
- "workspace",
114
- // Actions: list, get, associate, bootstrap
115
- "reminder",
116
- // Actions: list, active, create, snooze, complete, dismiss
117
- "integration",
118
- // Provider: slack, github, all; Actions: status, search, stats, activity, contributors, knowledge, summary, channels, discussions, sync_users, repos, issues
119
- "help"
120
- // Actions: tools, auth, version, editor_rules, enable_bundle
121
- ];
122
- CONTEXTSTREAM_RULES_DYNAMIC = `
123
- ## ContextStream Dynamic Rules (Powered by our SmartRouter)
124
-
125
- All rules are served dynamically via \`context\`. This file is minimal by design.
126
-
127
- ### Every Message Protocol
128
-
129
- | Message | What to Call |
130
- |---------|--------------|
131
- | **1st message** | \`init()\` then \`context(user_message="<msg>")\` |
132
- | **2nd+ messages** | \`context(user_message="<msg>")\` |
133
-
134
- Note: \`init()\` auto-detects folder from IDE. Only pass \`folder_path\` if auto-detection fails.
135
-
136
- ### Follow the Instructions Field
137
-
138
- The \`context\` response includes an \`instructions\` field with context-aware guidance.
139
- **Follow these instructions.** They are dynamically matched to your query and include:
140
- - Search guidance (when/how to search)
141
- - Git workflow rules (commit, PR, safety)
142
- - Planning rules (use ContextStream plans, not file-based)
143
- - Media/code analysis guidance
144
- - Lessons from past mistakes
145
- - And more...
146
-
147
- ### Notices
148
-
149
- Handle notices from \`context\` response:
150
- - **[VERSION_NOTICE]**: Tell user to update MCP
151
- - **[RULES_NOTICE]**: Run \`generate_rules()\`
152
- - **[LESSONS_WARNING]**: Apply lessons immediately
153
-
154
- Rules Version: ${RULES_VERSION}
155
- `.trim();
156
- CONTEXTSTREAM_RULES_FULL = `
157
- ## ContextStream Rules
158
-
159
- <contextstream_rules>
160
- 1. **SIMPLE UTILITY?** (list/show/version) \u2192 Just execute, skip context calls
161
- 2. **CODING TASK?** \u2192 init \u2192 context \u2192 work \u2192 capture
162
- 3. **SEARCH FIRST:** Before Glob/Grep/Read \u2192 \`search(mode="hybrid")\` FIRST
163
- </contextstream_rules>
164
-
165
- **Display this block at the start of responses to keep rules in context.**
166
-
167
- ---
168
-
169
- ## \u{1F6A8} CRITICAL RULE #1 - CONTEXTSTREAM SEARCH FIRST \u{1F6A8}
170
-
171
- **BEFORE using Glob, Grep, Search, Read (for discovery), Explore, or ANY local file scanning:**
172
- \`\`\`
173
- STOP \u2192 Call search(mode="hybrid", query="...") FIRST
174
- \`\`\`
175
-
176
- **Note:** PreToolUse hooks block these tools when ContextStream is available.
177
- **Claude Code users:** Your tool names are \`mcp__contextstream__search\`, \`mcp__contextstream__init\`, etc.
178
-
179
- \u274C **NEVER DO THIS:**
180
- - \`Glob("**/*.ts")\` \u2192 Use \`search(mode="pattern", query="*.ts")\` instead
181
- - \`Grep("functionName")\` \u2192 Use \`search(mode="keyword", query="functionName")\` instead
182
- - \`Read(file)\` for discovery \u2192 Use \`search(mode="hybrid", query="...")\` instead
183
- - \`Task(subagent_type="Explore")\` \u2192 Use \`search(mode="hybrid")\` instead
184
-
185
- \u2705 **ALWAYS DO THIS:**
186
- 1. \`search(mode="hybrid", query="what you're looking for")\`
187
- 2. Only use local tools (Glob/Grep/Read) if ContextStream returns **0 results**
188
- 3. Use Read ONLY for exact file edits after you know the file path
189
-
190
- This applies to **EVERY search** throughout the **ENTIRE conversation**, not just the first message.
191
-
192
- ---
193
-
194
- ## \u{1F6A8} CRITICAL RULE #2 - AUTO-INDEXING \u{1F6A8}
195
-
196
- **ContextStream auto-indexes your project on \`init\`.** You do NOT need to:
197
- - Ask the user to index
198
- - Manually trigger ingestion
199
- - Check index_status before every search
200
-
201
- **When \`init\` returns \`indexing_status: "started"\` or \`"refreshing"\`:**
202
- - Background indexing is running automatically
203
- - Search results will be available within seconds to minutes
204
- - **DO NOT fall back to local tools** - wait for ContextStream search to work
205
- - If search returns 0 results initially, try again after a moment
206
-
207
- **Only manually trigger indexing if:**
208
- - \`init\` returned \`ingest_recommendation.recommended: true\` (rare edge case)
209
- - User explicitly asks to re-index
210
-
211
- ---
212
-
213
- ## \u{1F6A8} CRITICAL RULE #3 - LESSONS (PAST MISTAKES) \u{1F6A8}
214
-
215
- **Lessons are past mistakes that MUST inform your work.** Ignoring lessons leads to repeated failures.
216
-
217
- ### On \`init\`:
218
- - Check for \`lessons\` and \`lessons_warning\` in the response
219
- - If present, **READ THEM IMMEDIATELY** before doing any work
220
- - These are high-priority lessons (critical/high severity) relevant to your context
221
- - **Apply the prevention steps** from each lesson to avoid repeating mistakes
222
-
223
- ### On \`context\`:
224
- - Check for \`[LESSONS_WARNING]\` tag in the response
225
- - If present, you **MUST** tell the user about the lessons before proceeding
226
- - Lessons are proactively fetched when risky actions are detected (refactor, migrate, deploy, etc.)
227
- - **Do not skip or bury this warning** - lessons represent real past mistakes
228
-
229
- ### Before ANY Non-Trivial Work:
230
- **ALWAYS call \`session(action="get_lessons", query="<topic>")\`** where \`<topic>\` matches what you're about to do:
231
- - Before refactoring \u2192 \`session(action="get_lessons", query="refactoring")\`
232
- - Before API changes \u2192 \`session(action="get_lessons", query="API changes")\`
233
- - Before database work \u2192 \`session(action="get_lessons", query="database migrations")\`
234
- - Before deployments \u2192 \`session(action="get_lessons", query="deployment")\`
235
-
236
- ### When Lessons Are Found:
237
- 1. **Summarize the lessons** to the user before proceeding
238
- 2. **Explicitly state how you will avoid the past mistakes**
239
- 3. If a lesson conflicts with the current approach, **warn the user**
240
-
241
- **Failing to check lessons before risky work is a critical error.**
242
-
243
- ---
244
-
245
- ## ContextStream v0.4.x Integration (Enhanced)
246
-
247
- You have access to ContextStream MCP tools for persistent memory and context.
248
- v0.4.x uses **~11 consolidated domain tools** for ~75% token reduction vs previous versions.
249
- Rules Version: ${RULES_VERSION}
250
-
251
- ## TL;DR - WHEN TO USE CONTEXT
252
-
253
- | Request Type | What to Do |
254
- |--------------|------------|
255
- | **\u{1F680} Simple utility** (list workspaces, show version) | **Just execute directly** - skip init, context, capture |
256
- | **\u{1F4BB} Coding task** (edit, create, refactor) | Full context: init \u2192 context \u2192 work \u2192 capture |
257
- | **\u{1F50D} Code search/discovery** | init \u2192 context \u2192 search() |
258
- | **\u26A0\uFE0F Risky work** (deploy, migrate, refactor) | Check lessons first: \`session(action="get_lessons")\` |
259
- | **User frustration/correction** | Capture lesson: \`session(action="capture_lesson", ...)\` |
260
-
261
- ### Simple Utility Operations - FAST PATH
262
-
263
- **For simple queries, just execute and respond:**
264
- - "list workspaces" \u2192 \`workspace(action="list")\` \u2192 done
265
- - "list projects" \u2192 \`project(action="list")\` \u2192 done
266
- - "show version" \u2192 \`help(action="version")\` \u2192 done
267
- - "what reminders do I have" \u2192 \`reminder(action="list")\` \u2192 done
268
-
269
- **No init. No context. No capture.** These add noise, not value.
270
-
271
- ### Coding Tasks - FULL CONTEXT
272
-
273
- | Step | What to Call |
274
- |------|--------------|
275
- | **1st message** | \`init(folder_path="...", context_hint="<msg>")\`, then \`context(...)\` |
276
- | **2nd+ messages** | \`context(user_message="<msg>", format="minified", max_tokens=400)\` |
277
- | **Code search** | \`search(mode="hybrid", query="...")\` \u2014 BEFORE Glob/Grep/Read |
278
- | **After significant work** | \`session(action="capture", event_type="decision", ...)\` |
279
- | **User correction** | \`session(action="capture_lesson", ...)\` |
280
- | **\u26A0\uFE0F When warnings received** | **STOP**, acknowledge, explain mitigation, then proceed |
281
-
282
- **How to detect simple utility operations:**
283
- - Single-word commands: "list", "show", "version", "help"
284
- - Data retrieval with no context dependency: "list my workspaces", "what projects do I have"
285
- - Status checks: "am I authenticated?", "what's the server version?"
286
-
287
- **First message rule (for coding tasks):** After \`init\`:
288
- 1. Check for \`lessons\` in response - if present, READ and SUMMARIZE them to user
289
- 2. Then call \`context\` before any other tool or response
290
-
291
- **Context Pack (Pro+):** If enabled, use \`context(..., mode="pack", distill=true)\` for code/file queries. If unavailable or disabled, omit \`mode\` and proceed with standard \`context\` (the API will fall back).
292
-
293
- **Tool naming:** Use the exact tool names exposed by your MCP client. Claude Code typically uses \`mcp__<server>__<tool>\` where \`<server>\` matches your MCP config (often \`contextstream\`). If a tool call fails with "No such tool available", refresh rules and match the tool list.
294
-
295
- ---
296
-
297
- ## Consolidated Domain Tools Architecture
298
-
299
- v0.4.x consolidates ~58 individual tools into ~11 domain tools with action/mode dispatch:
300
-
301
- ### Standalone Tools
302
- - **\`init\`** - Initialize session with workspace detection + context (skip for simple utility operations)
303
- - **\`context\`** - Semantic search for relevant context (skip for simple utility operations)
304
-
305
- ### Domain Tools (Use action/mode parameter)
306
-
307
- | Domain | Actions/Modes | Example |
308
- |--------|---------------|---------|
309
- | **\`search\`** | mode: semantic, hybrid, keyword, pattern | \`search(mode="hybrid", query="auth implementation", limit=3)\` |
310
- | **\`session\`** | action: capture, capture_lesson, get_lessons, recall, remember, user_context, summary, compress, delta, smart_search, decision_trace | \`session(action="capture", event_type="decision", title="Use JWT", content="...")\` |
311
- | **\`memory\`** | action: create_event, get_event, update_event, delete_event, list_events, distill_event, create_node, get_node, update_node, delete_node, list_nodes, supersede_node, search, decisions, timeline, summary | \`memory(action="list_events", limit=10)\` |
312
- | **\`graph\`** | action: dependencies, impact, call_path, related, path, decisions, ingest, circular_dependencies, unused_code, contradictions | \`graph(action="impact", symbol_name="AuthService")\` |
313
- | **\`project\`** | action: list, get, create, update, index, overview, statistics, files, index_status, ingest_local | \`project(action="statistics")\` |
314
- | **\`workspace\`** | action: list, get, associate, bootstrap | \`workspace(action="list")\` |
315
- | **\`reminder\`** | action: list, active, create, snooze, complete, dismiss | \`reminder(action="active")\` |
316
- | **\`integration\`** | provider: slack/github/all; action: status, search, stats, activity, contributors, knowledge, summary, channels, discussions, sync_users, repos, issues | \`integration(provider="github", action="search", query="...")\` |
317
- | **\`help\`** | action: tools, auth, version, editor_rules, enable_bundle | \`help(action="tools")\` |
318
-
319
- ---
320
-
321
- ### Why context is Required (Even After init)
322
-
323
- **Common mistake:** "init already gave me context, I don't need context"
324
-
325
- **This is WRONG. Here's why:**
326
- - \`init\` returns the last ~10 items **BY TIME** (chronological)
327
- - \`context\` **SEARCHES** for items **RELEVANT to THIS message** (semantic)
328
-
329
- **Example failure:**
330
- - User asks: "how should I implement authentication?"
331
- - Auth decisions were made 20 conversations ago
332
- - \`init\` won't have it (too old, not in recent 10)
333
- - \`context\` FINDS it via semantic search
334
-
335
- **Without context, you WILL miss relevant older context.**
336
-
337
- ---
338
-
339
- ### Recommended Token Budgets
340
-
341
- - For trivial/local edits: \`context(..., max_tokens=200)\`
342
- - Default: \`context(..., max_tokens=400)\`
343
- - Deep debugging/architecture: \`context(..., max_tokens=800)\`
344
- - Keep \`format="minified"\` (default) unless debugging
345
-
346
- If context still feels missing, use \`session(action="recall", query="...")\` for focused deep lookup.
347
-
348
- ---
349
-
350
- ### Rules, Version & Lessons Notices
351
-
352
- **[RULES_NOTICE]** - Update rules via \`generate_rules()\` (or rerun setup).
353
-
354
- **[VERSION_NOTICE]** - You **MUST IMMEDIATELY** tell the user:
355
- 1. A new MCP server version is available
356
- 2. The exact update command to run
357
- 3. That they need to restart their AI tool after updating
358
- **Do not skip this** - users often miss stderr warnings.
359
-
360
- **[LESSONS_WARNING]** - You **MUST** before proceeding:
361
- 1. Read all lessons listed
362
- 2. Tell the user about relevant lessons
363
- 3. Explain how you will avoid each past mistake
364
- **This is critical** - ignoring lessons leads to repeated failures.
365
-
366
- ---
367
-
368
- ### Preferences & Lessons (Use Early)
589
+ async function installAllEditorHooks(options) {
590
+ const editors = options.editors || ["claude", "cline", "roo", "kilo", "cursor"];
591
+ const results = [];
592
+ for (const editor of editors) {
593
+ try {
594
+ const result = await installEditorHooks({
595
+ editor,
596
+ scope: options.scope,
597
+ projectPath: options.projectPath,
598
+ includePreCompact: options.includePreCompact,
599
+ includePostWrite: options.includePostWrite
600
+ });
601
+ results.push(result);
602
+ } catch (error) {
603
+ console.error(`Failed to install hooks for ${editor}:`, error);
604
+ }
605
+ }
606
+ return results;
607
+ }
608
+ function generateAllHooksDocumentation() {
609
+ return `
610
+ ## Editor Hooks Support (ContextStream)
369
611
 
370
- - If preferences/style matter: \`session(action="user_context")\`
371
- - Before risky changes: \`session(action="get_lessons", query="<topic>")\`
372
- - On frustration/corrections: \`session(action="capture_lesson", title="...", trigger="...", impact="...", prevention="...")\`
612
+ ContextStream can install hooks for multiple AI code editors to enforce ContextStream-first behavior.
373
613
 
374
- ---
614
+ ### Supported Editors
375
615
 
376
- ### Context Pressure & Compaction Awareness
616
+ | Editor | Hooks Location | Hook Types |
617
+ |--------|---------------|------------|
618
+ | **Claude Code** | \`~/.claude/hooks/\` | PreToolUse, UserPromptSubmit, PreCompact |
619
+ | **Cursor** | \`~/.cursor/hooks/\` | preToolUse, beforeSubmit |
620
+ | **Cline** | \`~/Documents/Cline/Rules/Hooks/\` | PreToolUse, UserPromptSubmit |
621
+ | **Roo Code** | \`~/.roo/hooks/\` | PreToolUse, UserPromptSubmit |
622
+ | **Kilo Code** | \`~/.kilocode/hooks/\` | PreToolUse, UserPromptSubmit |
377
623
 
378
- ContextStream tracks context pressure to help you stay ahead of conversation compaction:
624
+ ### Claude Code Hooks
379
625
 
380
- **Automatic tracking:** Token usage is tracked automatically. \`context\` returns \`context_pressure\` when usage is high.
626
+ ${generateHooksDocumentation()}
381
627
 
382
- **When \`context\` returns \`context_pressure\` with high/critical level:**
383
- 1. Review the \`suggested_action\` field:
384
- - \`prepare_save\`: Start thinking about saving important state
385
- - \`save_now\`: Immediately call \`session(action="capture", event_type="session_snapshot")\` to preserve state
628
+ ### Cursor Hooks
386
629
 
387
- **PreCompact Hook (Optional):** If enabled, Claude Code will inject a reminder to save state before compaction.
388
- Enable with: \`generate_rules(install_hooks=true, include_pre_compact=true)\`
630
+ Cursor uses a \`hooks.json\` configuration file:
631
+ - **preToolUse**: Blocks discovery tools before execution
632
+ - **beforeSubmitPrompt**: Injects ContextStream rules reminder
389
633
 
390
- **Before compaction happens (when warned):**
634
+ #### Output Format
635
+ \`\`\`json
636
+ { "decision": "allow" }
391
637
  \`\`\`
392
- session(action="capture", event_type="session_snapshot", title="Pre-compaction snapshot", content="{
393
- \\"conversation_summary\\": \\"<summarize what we've been doing>\\",
394
- \\"current_goal\\": \\"<the main task>\\",
395
- \\"active_files\\": [\\"file1.ts\\", \\"file2.ts\\"],
396
- \\"recent_decisions\\": [{title: \\"...\\", rationale: \\"...\\"}],
397
- \\"unfinished_work\\": [{task: \\"...\\", status: \\"...\\", next_steps: \\"...\\"}]
398
- }")
638
+ or
639
+ \`\`\`json
640
+ { "decision": "deny", "reason": "Use ContextStream search instead" }
399
641
  \`\`\`
400
642
 
401
- **After compaction (when context seems lost):**
402
- 1. Call \`init(folder_path="...", is_post_compact=true)\` - this auto-restores the most recent snapshot
403
- 2. Or call \`session_restore_context()\` directly to get the saved state
404
- 3. Review the \`restored_context\` to understand prior work
405
- 4. Acknowledge to the user what was restored and continue
406
-
407
- ---
408
-
409
- ### Index Status (Auto-Managed)
410
-
411
- **Indexing is automatic.** After \`init\`, the project is auto-indexed in the background.
412
-
413
- **You do NOT need to manually check index_status before every search.** Just use \`search()\`.
414
-
415
- **If search returns 0 results and you expected matches:**
416
- 1. Check if \`init\` returned \`indexing_status: "started"\` - indexing may still be in progress
417
- 2. Wait a moment and retry \`search()\`
418
- 3. Only as a last resort: \`project(action="index_status")\` to check
419
-
420
- **Graph data:** If graph queries (\`dependencies\`, \`impact\`) return empty, run \`graph(action="ingest")\` once.
421
-
422
- **NEVER fall back to local tools (Glob/Grep/Read) just because search returned 0 results on first try.** Retry first.
423
-
424
- ### Enhanced Context (Server-Side Warnings)
425
-
426
- \`context\` now includes **intelligent server-side filtering** that proactively surfaces relevant warnings:
427
-
428
- **Response fields:**
429
- - \`warnings\`: Array of warning strings (displayed with \u26A0\uFE0F prefix)
643
+ ### Cline/Roo/Kilo Code Hooks
430
644
 
431
- **What triggers warnings:**
432
- - **Lessons**: Past mistakes relevant to the current query (via semantic matching)
433
- - **Risky actions**: Detected high-risk operations (deployments, migrations, destructive commands)
434
- - **Breaking changes**: When modifications may impact other parts of the codebase
645
+ These editors use the same hook format (JSON output):
646
+ - **PreToolUse**: Blocks discovery tools, redirects to ContextStream search
647
+ - **UserPromptSubmit**: Injects ContextStream rules reminder
435
648
 
436
- **When you receive warnings:**
437
- 1. **STOP** and read each warning carefully
438
- 2. **Acknowledge** the warning to the user
439
- 3. **Explain** how you will avoid the issue
440
- 4. Only proceed after addressing the warnings
649
+ Hooks are executable scripts named after the hook type (no extension).
441
650
 
442
- ### Search & Code Intelligence (ContextStream-first)
443
-
444
- \u26A0\uFE0F **STOP: Before using Search/Glob/Grep/Read/Explore** \u2192 Call \`search(mode="hybrid")\` FIRST. Use local tools ONLY if ContextStream returns 0 results.
445
-
446
- **\u274C WRONG workflow (wastes tokens, slow):**
447
- \`\`\`
448
- Grep "function" \u2192 Read file1.ts \u2192 Read file2.ts \u2192 Read file3.ts \u2192 finally understand
449
- \`\`\`
450
-
451
- **\u2705 CORRECT workflow (fast, complete):**
452
- \`\`\`
453
- search(mode="hybrid", query="function implementation") \u2192 done (results include context)
651
+ #### Output Format
652
+ \`\`\`json
653
+ {
654
+ "cancel": true,
655
+ "errorMessage": "Use ContextStream search instead",
656
+ "contextModification": "[CONTEXTSTREAM] Use search tool first"
657
+ }
454
658
  \`\`\`
455
659
 
456
- **Why?** ContextStream search returns semantic matches + context + file locations in ONE call. Local tools require multiple round-trips.
457
-
458
- **Search order:**
459
- 1. \`session(action="smart_search", query="...")\` - context-enriched
460
- 2. \`search(mode="hybrid", query="...", limit=3)\` or \`search(mode="keyword", query="<filename>", limit=3)\`
461
- 3. \`project(action="files")\` - file tree/list (only when needed)
462
- 4. \`graph(action="dependencies", ...)\` - code structure
463
- 5. Local repo scans (rg/ls/find) - ONLY if ContextStream returns no results, errors, or the user explicitly asks
464
-
465
- **Search Mode Selection:**
466
-
467
- | Need | Mode | Example |
468
- |------|------|---------|
469
- | Find code by meaning | \`hybrid\` | "authentication logic", "error handling" |
470
- | Exact string/symbol | \`keyword\` | "UserAuthService", "API_KEY" |
471
- | File patterns | \`pattern\` | "*.sql", "test_*.py" |
472
- | ALL matches (grep-like) | \`exhaustive\` | "TODO", "FIXME" (find all occurrences) |
473
- | Symbol renaming | \`refactor\` | "oldFunctionName" (word-boundary matching) |
474
- | Conceptual search | \`semantic\` | "how does caching work" |
475
-
476
- **Token Efficiency:** Use \`output_format\` to reduce response size:
477
- - \`full\` (default): Full content for understanding code
478
- - \`paths\`: File paths only (80% token savings) - use for file listings
479
- - \`minimal\`: Compact format (60% savings) - use for refactoring
480
- - \`count\`: Match counts only (90% savings) - use for quick checks
481
-
482
- **When to use \`output_format=count\`:**
483
- - User asks "how many X" or "count of X" \u2192 \`search(..., output_format="count")\`
484
- - Checking if something exists \u2192 count > 0 is sufficient
485
- - Large exhaustive searches \u2192 get count first, then fetch if needed
486
-
487
- **Auto-suggested formats:** Search responses include \`query_interpretation.suggested_output_format\` when the API detects an optimal format:
488
- - Symbol queries (e.g., "authOptions") \u2192 suggests \`minimal\` (path + line + snippet)
489
- - Count queries (e.g., "how many") \u2192 suggests \`count\`
490
- **USE the suggested format** on subsequent searches for best token efficiency.
491
-
492
- **Search defaults:** \`search\` returns the top 3 results with compact snippets. Use \`limit\` + \`offset\` for pagination, and \`content_max_chars\` to expand snippets when needed.
493
-
494
- If ContextStream returns results, stop and use them. NEVER use local Search/Explore/Read unless you need exact code edits or ContextStream returned 0 results.
495
-
496
- **Code Analysis:**
497
- - Dependencies: \`graph(action="dependencies", file_path="...")\`
498
- - Change impact: \`graph(action="impact", symbol_name="...")\`
499
- - Call path: \`graph(action="call_path", from_symbol="...", to_symbol="...")\`
500
- - Build graph: \`graph(action="ingest")\` - async, can take a few minutes
501
-
502
- ---
503
-
504
- ### Distillation & Memory Hygiene
660
+ ### Installation
505
661
 
506
- - Quick context: \`session(action="summary")\`
507
- - Long chat: \`session(action="compress", content="...")\`
508
- - Memory summary: \`memory(action="summary")\`
509
- - Condense noisy entries: \`memory(action="distill_event", event_id="...")\`
662
+ 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.
510
663
 
511
- ---
664
+ ### Disabling Hooks
512
665
 
513
- ### When to Capture
514
-
515
- | When | Call | Example |
516
- |------|------|---------|
517
- | User makes decision | \`session(action="capture", event_type="decision", ...)\` | "Let's use PostgreSQL" |
518
- | User states preference | \`session(action="capture", event_type="preference", ...)\` | "I prefer TypeScript" |
519
- | Complete significant task | \`session(action="capture", event_type="task", ...)\` | Capture what was done |
520
- | Need past context | \`session(action="recall", query="...")\` | "What did we decide about X?" |
521
-
522
- **DO NOT capture utility operations:**
523
- - \u274C "Listed workspaces" - not meaningful context
524
- - \u274C "Showed version" - not a decision
525
- - \u274C "Listed projects" - just data retrieval
526
-
527
- **DO capture meaningful work:**
528
- - \u2705 Decisions, preferences, completed features
529
- - \u2705 Lessons from mistakes
530
- - \u2705 Insights about architecture or patterns
531
-
532
- ---
533
-
534
- ### \u{1F6A8} Plans & Tasks - USE CONTEXTSTREAM, NOT FILE-BASED PLANS \u{1F6A8}
535
-
536
- **CRITICAL: When the user requests planning, implementation plans, roadmaps, task breakdowns, or step-by-step approaches:**
537
-
538
- \u274C **DO NOT** use built-in plan mode (EnterPlanMode tool)
539
- \u274C **DO NOT** write plans to markdown files or plan documents
540
- \u274C **DO NOT** ask "should I create a plan file?"
541
-
542
- \u2705 **ALWAYS** use ContextStream's plan/task system instead
543
-
544
- **Trigger phrases to detect (use ContextStream immediately):**
545
- - "create a plan", "make a plan", "plan this", "plan for"
546
- - "implementation plan", "roadmap", "milestones"
547
- - "break down", "breakdown", "break this into steps"
548
- - "what are the steps", "step by step", "outline the approach"
549
- - "task list", "todo list", "action items"
550
- - "how should we approach", "implementation strategy"
551
-
552
- **When detected, immediately:**
553
-
554
- 1. **Create the plan in ContextStream:**
555
- \`\`\`
556
- session(action="capture_plan", title="<descriptive title>", description="<what this plan accomplishes>", goals=["goal1", "goal2"], steps=[{id: "1", title: "Step 1", order: 1, description: "..."}, ...])
557
- \`\`\`
558
-
559
- 2. **Create tasks for each step:**
560
- \`\`\`
561
- memory(action="create_task", title="<task title>", plan_id="<plan_id from step 1>", priority="high|medium|low", description="<detailed task description>")
562
- \`\`\`
563
-
564
- **Why ContextStream plans are better:**
565
- - Plans persist across sessions and are searchable
566
- - Tasks track status (pending/in_progress/completed/blocked)
567
- - Context is preserved with workspace/project association
568
- - Can be retrieved with \`session(action="get_plan", plan_id="...", include_tasks=true)\`
569
- - Future sessions can continue from where you left off
570
-
571
- **Managing plans/tasks:**
572
- - List plans: \`session(action="list_plans")\`
573
- - Get plan with tasks: \`session(action="get_plan", plan_id="<uuid>", include_tasks=true)\`
574
- - List tasks: \`memory(action="list_tasks", plan_id="<uuid>")\` or \`memory(action="list_tasks")\` for all
575
- - Update task status: \`memory(action="update_task", task_id="<uuid>", task_status="pending|in_progress|completed|blocked")\`
576
- - Link task to plan: \`memory(action="update_task", task_id="<uuid>", plan_id="<plan_uuid>")\`
577
- - Unlink task from plan: \`memory(action="update_task", task_id="<uuid>", plan_id=null)\`
578
- - Delete: \`memory(action="delete_task", task_id="<uuid>")\` or \`memory(action="delete_event", event_id="<plan_uuid>")\`
579
-
580
- ---
581
-
582
- ### Complete Action Reference
583
-
584
- **session actions:**
585
- - \`capture\` - Save decision/insight/task (requires: event_type, title, content)
586
- - \`capture_lesson\` - Save lesson from mistake (requires: title, category, trigger, impact, prevention)
587
- - \`get_lessons\` - Retrieve relevant lessons (optional: query, category, severity)
588
- - \`recall\` - Natural language memory recall (requires: query)
589
- - \`remember\` - Quick save to memory (requires: content)
590
- - \`user_context\` - Get user preferences/style
591
- - \`summary\` - Workspace summary
592
- - \`compress\` - Compress long conversation
593
- - \`delta\` - Changes since timestamp
594
- - \`smart_search\` - Context-enriched search
595
- - \`decision_trace\` - Trace decision provenance
596
-
597
- **memory actions:**
598
- - Event CRUD: \`create_event\`, \`get_event\`, \`update_event\`, \`delete_event\`, \`list_events\`, \`distill_event\`
599
- - Node CRUD: \`create_node\`, \`get_node\`, \`update_node\`, \`delete_node\`, \`list_nodes\`, \`supersede_node\`
600
- - Query: \`search\`, \`decisions\`, \`timeline\`, \`summary\`
601
-
602
- **graph actions:**
603
- - Analysis: \`dependencies\`, \`impact\`, \`call_path\`, \`related\`, \`path\`
604
- - Quality: \`circular_dependencies\`, \`unused_code\`, \`contradictions\`
605
- - Management: \`ingest\`, \`decisions\`
606
-
607
- See full documentation: https://contextstream.io/docs/mcp/tools
666
+ Set environment variables:
667
+ - \`CONTEXTSTREAM_HOOK_ENABLED=false\` - Disable PreToolUse blocking
668
+ - \`CONTEXTSTREAM_REMINDER_ENABLED=false\` - Disable UserPromptSubmit reminders
608
669
  `.trim();
609
- CONTEXTSTREAM_RULES_MINIMAL = `
610
- ## ContextStream Rules
611
-
612
- <contextstream_rules>
613
- 1. **SIMPLE UTILITY?** (list/show/version) \u2192 Just execute, skip context calls
614
- 2. **CODING TASK?** \u2192 init \u2192 context \u2192 work \u2192 capture
615
- 3. **SEARCH FIRST:** Before Glob/Grep/Read \u2192 \`search(mode="hybrid")\` FIRST
616
- </contextstream_rules>
617
-
618
- **Display this block at the start of responses to keep rules in context.**
619
-
620
- ---
621
-
622
- ## ContextStream v0.4.x (Hooks Enforced)
623
-
624
- Rules Version: ${RULES_VERSION}
625
- **Note:** PreToolUse hooks block Glob/Grep/Search when ContextStream is available.
626
-
627
- ### For Coding Tasks
628
-
629
- | Action | Tool Call |
630
- |--------|-----------|
631
- | **1st message** | \`init(folder_path="<cwd>", context_hint="<msg>")\` then \`context(...)\` |
632
- | **2nd+ messages** | \`context(user_message="<msg>", format="minified", max_tokens=400)\` |
633
- | **Code search** | \`search(mode="hybrid", query="...")\` \u2014 BEFORE any local tools |
634
- | **Save decisions** | \`session(action="capture", event_type="decision", ...)\` |
635
-
636
- ### Search Modes
637
-
638
- | Mode | Use Case |
639
- |------|----------|
640
- | \`hybrid\` | General code search (default) |
641
- | \`keyword\` | Exact symbol/string match |
642
- | \`exhaustive\` | Find ALL matches (grep-like) |
643
- | \`semantic\` | Conceptual questions |
644
-
645
- ### Why ContextStream First?
646
-
647
- \u274C **WRONG:** \`Grep \u2192 Read \u2192 Read \u2192 Read\` (4+ tool calls, slow)
648
- \u2705 **CORRECT:** \`search(mode="hybrid")\` (1 call, returns context)
649
-
650
- ContextStream search is **indexed** and returns semantic matches + context in ONE call.
651
-
652
- ### Quick Reference
653
-
654
- | Tool | Example |
655
- |------|---------|
656
- | \`search\` | \`search(mode="hybrid", query="auth", limit=3)\` |
657
- | \`session\` | \`session(action="capture", event_type="decision", title="...", content="...")\` |
658
- | \`memory\` | \`memory(action="list_events", limit=10)\` |
659
- | \`graph\` | \`graph(action="dependencies", file_path="...")\` |
660
-
661
- ### \u{1F680} FAST PATH: Simple Utility Operations
662
-
663
- **For simple utility commands, SKIP the ceremony and just execute directly:**
664
-
665
- | Command Type | Just Call | Skip |
666
- |--------------|-----------|------|
667
- | List workspaces | \`workspace(action="list")\` | init, context, capture |
668
- | List projects | \`project(action="list")\` | init, context, capture |
669
- | Show version | \`help(action="version")\` | init, context, capture |
670
- | List reminders | \`reminder(action="list")\` | init, context, capture |
671
- | Check auth | \`help(action="auth")\` | init, context, capture |
672
-
673
- **Detect simple operations by these patterns:**
674
- - "list ...", "show ...", "what are my ...", "get ..."
675
- - Single-action queries with no context dependency
676
- - User just wants data, not analysis or coding help
677
-
678
- **DO NOT add overhead for utility operations:**
679
- - \u274C Don't call init just to list workspaces
680
- - \u274C Don't call context for simple queries
681
- - \u274C Don't capture "listed workspaces" as an event (that's noise)
682
-
683
- **Use full context ceremony ONLY for:**
684
- - Coding tasks (edit, create, refactor, debug)
685
- - Search/discovery (finding code, understanding architecture)
686
- - Tasks where past decisions or lessons matter
687
-
688
- ### Lessons (Past Mistakes)
689
-
690
- - After \`init\`: Check for \`lessons\` field and apply before work
691
- - Before risky work: \`session(action="get_lessons", query="<topic>")\`
692
- - On mistakes: \`session(action="capture_lesson", title="...", trigger="...", impact="...", prevention="...")\`
693
-
694
- ### Context Pressure & Compaction
695
-
696
- - If \`context\` returns high/critical \`context_pressure\`: call \`session(action="capture", ...)\` to save state
697
- - PreCompact hooks automatically save snapshots before compaction (if installed)
698
-
699
- ### Enhanced Context (Warnings)
670
+ }
671
+ 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;
672
+ var init_hooks_config = __esm({
673
+ "src/hooks-config.ts"() {
674
+ "use strict";
675
+ PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
676
+ """
677
+ ContextStream PreToolUse Hook for Claude Code
678
+ Blocks Grep/Glob/Search/Task(Explore)/EnterPlanMode and redirects to ContextStream.
679
+
680
+ Only blocks if the current project is indexed in ContextStream.
681
+ If not indexed, allows local tools through with a suggestion to index.
682
+ """
683
+
684
+ import json
685
+ import sys
686
+ import os
687
+ from pathlib import Path
688
+ from datetime import datetime, timedelta
689
+
690
+ ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
691
+ INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
692
+ # Consider index stale after 7 days
693
+ STALE_THRESHOLD_DAYS = 7
694
+
695
+ DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
696
+
697
+ def is_discovery_glob(pattern):
698
+ pattern_lower = pattern.lower()
699
+ for p in DISCOVERY_PATTERNS:
700
+ if p in pattern_lower:
701
+ return True
702
+ if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
703
+ return True
704
+ if "**" in pattern or "*/" in pattern:
705
+ return True
706
+ return False
707
+
708
+ def is_discovery_grep(file_path):
709
+ if not file_path or file_path in [".", "./", "*", "**"]:
710
+ return True
711
+ if "*" in file_path or "**" in file_path:
712
+ return True
713
+ return False
714
+
715
+ def is_project_indexed(cwd: str) -> tuple[bool, bool]:
716
+ """
717
+ Check if the current directory is in an indexed project.
718
+ Returns (is_indexed, is_stale).
719
+ """
720
+ if not INDEX_STATUS_FILE.exists():
721
+ return False, False
722
+
723
+ try:
724
+ with open(INDEX_STATUS_FILE, "r") as f:
725
+ data = json.load(f)
726
+ except:
727
+ return False, False
728
+
729
+ projects = data.get("projects", {})
730
+ cwd_path = Path(cwd).resolve()
731
+
732
+ # Check if cwd is within any indexed project
733
+ for project_path, info in projects.items():
734
+ try:
735
+ indexed_path = Path(project_path).resolve()
736
+ # Check if cwd is the project or a subdirectory
737
+ if cwd_path == indexed_path or indexed_path in cwd_path.parents:
738
+ # Check if stale
739
+ indexed_at = info.get("indexed_at")
740
+ if indexed_at:
741
+ try:
742
+ indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
743
+ if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
744
+ return True, True # Indexed but stale
745
+ except:
746
+ pass
747
+ return True, False # Indexed and fresh
748
+ except:
749
+ continue
750
+
751
+ return False, False
752
+
753
+ def main():
754
+ if not ENABLED:
755
+ sys.exit(0)
756
+
757
+ try:
758
+ data = json.load(sys.stdin)
759
+ except:
760
+ sys.exit(0)
761
+
762
+ tool = data.get("tool_name", "")
763
+ inp = data.get("tool_input", {})
764
+ cwd = data.get("cwd", os.getcwd())
765
+
766
+ # Check if project is indexed
767
+ is_indexed, is_stale = is_project_indexed(cwd)
768
+
769
+ if not is_indexed:
770
+ # Project not indexed - allow local tools but suggest indexing
771
+ # Don't block, just exit successfully
772
+ sys.exit(0)
773
+
774
+ if is_stale:
775
+ # Index is stale - allow with warning (printed but not blocking)
776
+ # Still allow the tool but remind about re-indexing
777
+ pass # Continue to blocking logic but could add warning
778
+
779
+ if tool == "Glob":
780
+ pattern = inp.get("pattern", "")
781
+ if is_discovery_glob(pattern):
782
+ print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of Glob.", file=sys.stderr)
783
+ sys.exit(2)
784
+
785
+ elif tool == "Grep" or tool == "Search":
786
+ # Block ALL Grep/Search operations - use ContextStream search or Read for specific files
787
+ pattern = inp.get("pattern", "")
788
+ path = inp.get("path", "")
789
+ if pattern:
790
+ if path and not is_discovery_grep(path):
791
+ # Specific file - suggest Read instead
792
+ print(f"STOP: Use Read(\\"{path}\\") to view file content, or mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") for codebase search.", file=sys.stderr)
793
+ else:
794
+ print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}.", file=sys.stderr)
795
+ sys.exit(2)
796
+
797
+ elif tool == "Task":
798
+ if inp.get("subagent_type", "").lower() == "explore":
799
+ print("STOP: Use mcp__contextstream__search(mode=\\"hybrid\\") instead of Task(Explore).", file=sys.stderr)
800
+ sys.exit(2)
801
+ if inp.get("subagent_type", "").lower() == "plan":
802
+ print("STOP: Use mcp__contextstream__session(action=\\"capture_plan\\") for planning. ContextStream plans persist across sessions.", file=sys.stderr)
803
+ sys.exit(2)
804
+
805
+ elif tool == "EnterPlanMode":
806
+ 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)
807
+ sys.exit(2)
808
+
809
+ sys.exit(0)
810
+
811
+ if __name__ == "__main__":
812
+ main()
813
+ `;
814
+ USER_PROMPT_HOOK_SCRIPT = `#!/usr/bin/env python3
815
+ """
816
+ ContextStream UserPromptSubmit Hook - Injects reminder on every message.
817
+ """
700
818
 
701
- \`context\` returns server-side \`warnings\` for lessons, risky actions, and breaking changes.
702
- When warnings are present: **STOP**, acknowledge them, explain mitigation, then proceed.
819
+ import json
820
+ import sys
821
+ import os
703
822
 
704
- ### Automatic Context Restoration
823
+ ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
705
824
 
706
- **Context restoration is now enabled by default.** Every \`init\` call automatically:
707
- - Restores context from recent snapshots (if available)
708
- - Returns \`restored_context\` field with snapshot data
709
- - Sets \`is_post_compact=true\` in response when restoration occurs
825
+ REMINDER = """[CONTEXTSTREAM RULES]
826
+ 1. BEFORE Glob/Grep/Read/Search: mcp__contextstream__search(mode="hybrid") FIRST
827
+ 2. Call context_smart at start of EVERY response
828
+ 3. Local tools ONLY if ContextStream returns 0 results
829
+ [END RULES]"""
710
830
 
711
- **No special handling needed after compaction** - just call \`init\` normally.
831
+ def main():
832
+ if not ENABLED:
833
+ sys.exit(0)
712
834
 
713
- To disable automatic restoration:
714
- - Pass \`is_post_compact=false\` in the API call
715
- - Or set \`CONTEXTSTREAM_RESTORE_CONTEXT=false\` environment variable
835
+ try:
836
+ json.load(sys.stdin)
837
+ except:
838
+ sys.exit(0)
716
839
 
717
- ### Notices - MUST HANDLE IMMEDIATELY
840
+ print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": REMINDER}}))
841
+ sys.exit(0)
718
842
 
719
- - **[VERSION_NOTICE]**: Tell the user about the update and command to run
720
- - **[RULES_NOTICE]**: Run \`generate_rules(overwrite_existing=true)\` to update
721
- - **[LESSONS_WARNING]**: Read lessons, tell user about them, explain how you'll avoid past mistakes
843
+ if __name__ == "__main__":
844
+ main()
845
+ `;
846
+ MEDIA_AWARE_HOOK_SCRIPT = `#!/usr/bin/env python3
847
+ """
848
+ ContextStream Media-Aware Hook for Claude Code
849
+
850
+ Detects media-related prompts and injects context about the media tool.
851
+ """
852
+
853
+ import json
854
+ import sys
855
+ import os
856
+ import re
857
+
858
+ ENABLED = os.environ.get("CONTEXTSTREAM_MEDIA_HOOK_ENABLED", "true").lower() == "true"
859
+
860
+ # Media patterns (case-insensitive)
861
+ PATTERNS = [
862
+ r"\\b(video|videos|clip|clips|footage|keyframe)s?\\b",
863
+ r"\\b(remotion|timeline|video\\s*edit)\\b",
864
+ r"\\b(image|images|photo|photos|picture|thumbnail)s?\\b",
865
+ r"\\b(audio|podcast|transcript|transcription|voice)\\b",
866
+ r"\\b(media|asset|assets|creative|b-roll)\\b",
867
+ r"\\b(find|search|show).*(clip|video|image|audio|footage|media)\\b",
868
+ ]
869
+
870
+ COMPILED = [re.compile(p, re.IGNORECASE) for p in PATTERNS]
871
+
872
+ MEDIA_CONTEXT = """[MEDIA TOOLS AVAILABLE]
873
+ Your workspace may have indexed media. Use ContextStream media tools:
874
+
875
+ - **Search**: \`mcp__contextstream__media(action="search", query="description")\`
876
+ - **Get clip**: \`mcp__contextstream__media(action="get_clip", content_id="...", start="1:34", end="2:15", output_format="remotion|ffmpeg|raw")\`
877
+ - **List assets**: \`mcp__contextstream__media(action="list")\`
878
+ - **Index**: \`mcp__contextstream__media(action="index", file_path="...", content_type="video|audio|image|document")\`
879
+
880
+ For Remotion: use \`output_format="remotion"\` to get frame-based props.
881
+ [END MEDIA TOOLS]"""
882
+
883
+ def matches(text):
884
+ return any(p.search(text) for p in COMPILED)
885
+
886
+ def main():
887
+ if not ENABLED:
888
+ sys.exit(0)
889
+
890
+ try:
891
+ data = json.load(sys.stdin)
892
+ except:
893
+ sys.exit(0)
894
+
895
+ prompt = data.get("prompt", "")
896
+ if not prompt:
897
+ session = data.get("session", {})
898
+ for msg in reversed(session.get("messages", [])):
899
+ if msg.get("role") == "user":
900
+ content = msg.get("content", "")
901
+ prompt = content if isinstance(content, str) else ""
902
+ if isinstance(content, list):
903
+ for b in content:
904
+ if isinstance(b, dict) and b.get("type") == "text":
905
+ prompt = b.get("text", "")
906
+ break
907
+ break
908
+
909
+ if not prompt or not matches(prompt):
910
+ sys.exit(0)
911
+
912
+ print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": MEDIA_CONTEXT}}))
913
+ sys.exit(0)
914
+
915
+ if __name__ == "__main__":
916
+ main()
917
+ `;
918
+ PRECOMPACT_HOOK_SCRIPT = `#!/usr/bin/env python3
919
+ """
920
+ ContextStream PreCompact Hook for Claude Code
921
+
922
+ Runs BEFORE conversation context is compacted (manual via /compact or automatic).
923
+ Automatically saves conversation state to ContextStream by parsing the transcript.
924
+
925
+ Input (via stdin):
926
+ {
927
+ "session_id": "...",
928
+ "transcript_path": "/path/to/transcript.jsonl",
929
+ "permission_mode": "default",
930
+ "hook_event_name": "PreCompact",
931
+ "trigger": "manual" | "auto",
932
+ "custom_instructions": "..."
933
+ }
722
934
 
723
- ### Plans & Tasks
935
+ Output (to stdout):
936
+ {
937
+ "hookSpecificOutput": {
938
+ "hookEventName": "PreCompact",
939
+ "additionalContext": "... status message ..."
940
+ }
941
+ }
942
+ """
943
+
944
+ import json
945
+ import sys
946
+ import os
947
+ import re
948
+ import urllib.request
949
+ import urllib.error
950
+
951
+ ENABLED = os.environ.get("CONTEXTSTREAM_PRECOMPACT_ENABLED", "true").lower() == "true"
952
+ AUTO_SAVE = os.environ.get("CONTEXTSTREAM_PRECOMPACT_AUTO_SAVE", "true").lower() == "true"
953
+ API_URL = os.environ.get("CONTEXTSTREAM_API_URL", "https://api.contextstream.io")
954
+ API_KEY = os.environ.get("CONTEXTSTREAM_API_KEY", "")
955
+
956
+ WORKSPACE_ID = None
957
+
958
+ def load_config_from_mcp_json(cwd):
959
+ """Load API config from .mcp.json if env vars not set."""
960
+ global API_URL, API_KEY, WORKSPACE_ID
961
+
962
+ # Try to find .mcp.json and .contextstream/config.json in cwd or parent directories
963
+ search_dir = cwd
964
+ for _ in range(5): # Search up to 5 levels
965
+ # Load API config from .mcp.json
966
+ if not API_KEY:
967
+ mcp_path = os.path.join(search_dir, ".mcp.json")
968
+ if os.path.exists(mcp_path):
969
+ try:
970
+ with open(mcp_path, 'r') as f:
971
+ config = json.load(f)
972
+ servers = config.get("mcpServers", {})
973
+ cs_config = servers.get("contextstream", {})
974
+ env = cs_config.get("env", {})
975
+ if env.get("CONTEXTSTREAM_API_KEY"):
976
+ API_KEY = env["CONTEXTSTREAM_API_KEY"]
977
+ if env.get("CONTEXTSTREAM_API_URL"):
978
+ API_URL = env["CONTEXTSTREAM_API_URL"]
979
+ except:
980
+ pass
981
+
982
+ # Load workspace_id from .contextstream/config.json
983
+ if not WORKSPACE_ID:
984
+ cs_config_path = os.path.join(search_dir, ".contextstream", "config.json")
985
+ if os.path.exists(cs_config_path):
986
+ try:
987
+ with open(cs_config_path, 'r') as f:
988
+ cs_config = json.load(f)
989
+ if cs_config.get("workspace_id"):
990
+ WORKSPACE_ID = cs_config["workspace_id"]
991
+ except:
992
+ pass
993
+
994
+ parent = os.path.dirname(search_dir)
995
+ if parent == search_dir:
996
+ break
997
+ search_dir = parent
998
+
999
+ def parse_transcript(transcript_path):
1000
+ """Parse transcript to extract active files, decisions, and context."""
1001
+ active_files = set()
1002
+ recent_messages = []
1003
+ tool_calls = []
1004
+
1005
+ try:
1006
+ with open(transcript_path, 'r') as f:
1007
+ for line in f:
1008
+ try:
1009
+ entry = json.loads(line.strip())
1010
+ msg_type = entry.get("type", "")
1011
+
1012
+ # Extract files from tool calls
1013
+ if msg_type == "tool_use":
1014
+ tool_name = entry.get("name", "")
1015
+ tool_input = entry.get("input", {})
1016
+ tool_calls.append({"name": tool_name, "input": tool_input})
1017
+
1018
+ # Extract file paths from common tools
1019
+ if tool_name in ["Read", "Write", "Edit", "NotebookEdit"]:
1020
+ file_path = tool_input.get("file_path") or tool_input.get("notebook_path")
1021
+ if file_path:
1022
+ active_files.add(file_path)
1023
+ elif tool_name == "Glob":
1024
+ pattern = tool_input.get("pattern", "")
1025
+ if pattern:
1026
+ active_files.add(f"[glob:{pattern}]")
1027
+
1028
+ # Collect recent assistant messages for summary
1029
+ if msg_type == "assistant" and entry.get("content"):
1030
+ content = entry.get("content", "")
1031
+ if isinstance(content, str) and len(content) > 50:
1032
+ recent_messages.append(content[:500])
1033
+
1034
+ except json.JSONDecodeError:
1035
+ continue
1036
+ except Exception as e:
1037
+ pass
1038
+
1039
+ return {
1040
+ "active_files": list(active_files)[-20:], # Last 20 files
1041
+ "tool_call_count": len(tool_calls),
1042
+ "message_count": len(recent_messages),
1043
+ "last_tools": [t["name"] for t in tool_calls[-10:]], # Last 10 tool names
1044
+ }
724
1045
 
725
- When user asks for a plan, use ContextStream (not EnterPlanMode):
726
- 1. \`session(action="capture_plan", title="...", steps=[...])\`
727
- 2. \`memory(action="create_task", title="...", plan_id="<id>")\`
1046
+ def save_snapshot(session_id, transcript_data, trigger):
1047
+ """Save snapshot to ContextStream API."""
1048
+ if not API_KEY:
1049
+ return False, "No API key configured"
1050
+
1051
+ snapshot_content = {
1052
+ "session_id": session_id,
1053
+ "trigger": trigger,
1054
+ "captured_at": None, # API will set timestamp
1055
+ "active_files": transcript_data.get("active_files", []),
1056
+ "tool_call_count": transcript_data.get("tool_call_count", 0),
1057
+ "last_tools": transcript_data.get("last_tools", []),
1058
+ "auto_captured": True,
1059
+ }
728
1060
 
729
- ### Workspace-Only Mode (Multi-Project Folders)
1061
+ payload = {
1062
+ "event_type": "session_snapshot",
1063
+ "title": f"Auto Pre-compaction Snapshot ({trigger})",
1064
+ "content": json.dumps(snapshot_content),
1065
+ "importance": "high",
1066
+ "tags": ["session_snapshot", "pre_compaction", "auto_captured"],
1067
+ "source_type": "hook",
1068
+ }
730
1069
 
731
- If working in a parent folder containing multiple projects:
732
- \`\`\`
733
- init(folder_path="...", skip_project_creation=true)
734
- \`\`\`
1070
+ # Add workspace_id if available
1071
+ if WORKSPACE_ID:
1072
+ payload["workspace_id"] = WORKSPACE_ID
1073
+
1074
+ try:
1075
+ req = urllib.request.Request(
1076
+ f"{API_URL}/api/v1/memory/events",
1077
+ data=json.dumps(payload).encode('utf-8'),
1078
+ headers={
1079
+ "Content-Type": "application/json",
1080
+ "X-API-Key": API_KEY,
1081
+ },
1082
+ method="POST"
1083
+ )
1084
+ with urllib.request.urlopen(req, timeout=5) as resp:
1085
+ return True, "Snapshot saved"
1086
+ except urllib.error.URLError as e:
1087
+ return False, str(e)
1088
+ except Exception as e:
1089
+ return False, str(e)
1090
+
1091
+ def main():
1092
+ if not ENABLED:
1093
+ sys.exit(0)
1094
+
1095
+ try:
1096
+ data = json.load(sys.stdin)
1097
+ except:
1098
+ sys.exit(0)
1099
+
1100
+ # Load config from .mcp.json if env vars not set
1101
+ cwd = data.get("cwd", os.getcwd())
1102
+ load_config_from_mcp_json(cwd)
1103
+
1104
+ session_id = data.get("session_id", "unknown")
1105
+ transcript_path = data.get("transcript_path", "")
1106
+ trigger = data.get("trigger", "unknown")
1107
+ custom_instructions = data.get("custom_instructions", "")
1108
+
1109
+ # Parse transcript for context
1110
+ transcript_data = {}
1111
+ if transcript_path and os.path.exists(transcript_path):
1112
+ transcript_data = parse_transcript(transcript_path)
1113
+
1114
+ # Auto-save snapshot if enabled
1115
+ auto_save_status = ""
1116
+ if AUTO_SAVE and API_KEY:
1117
+ success, msg = save_snapshot(session_id, transcript_data, trigger)
1118
+ if success:
1119
+ auto_save_status = f"\\n[ContextStream: Auto-saved snapshot with {len(transcript_data.get('active_files', []))} active files]"
1120
+ else:
1121
+ auto_save_status = f"\\n[ContextStream: Auto-save failed - {msg}]"
1122
+
1123
+ # Build context injection for the AI (backup in case auto-save fails)
1124
+ files_list = ", ".join(transcript_data.get("active_files", [])[:5]) or "none detected"
1125
+ context = f"""[CONTEXT COMPACTION - {trigger.upper()}]{auto_save_status}
1126
+
1127
+ Active files detected: {files_list}
1128
+ Tool calls in session: {transcript_data.get('tool_call_count', 0)}
1129
+
1130
+ After compaction, call session_init(is_post_compact=true) to restore context.
1131
+ {f"User instructions: {custom_instructions}" if custom_instructions else ""}"""
1132
+
1133
+ output = {
1134
+ "hookSpecificOutput": {
1135
+ "hookEventName": "PreCompact",
1136
+ "additionalContext": context
1137
+ }
1138
+ }
735
1139
 
736
- This enables workspace-level memory and context without project-specific indexing.
737
- Use for monorepos or folders with multiple independent projects.
1140
+ print(json.dumps(output))
1141
+ sys.exit(0)
738
1142
 
739
- Full docs: https://contextstream.io/docs/mcp/tools
740
- `.trim();
741
- TEMPLATES = {
742
- codex: {
743
- filename: "AGENTS.md",
744
- description: "Codex CLI agent instructions",
745
- build: (rules) => `# Codex CLI Instructions
746
- ${rules}
747
- `
748
- },
749
- cursor: {
750
- filename: ".cursorrules",
751
- description: "Cursor AI rules",
752
- build: (rules) => `# Cursor Rules
753
- ${rules}
754
- `
755
- },
756
- cline: {
757
- filename: ".clinerules",
758
- description: "Cline AI rules",
759
- build: (rules) => `# Cline Rules
760
- ${rules}
761
- `
762
- },
763
- kilo: {
764
- filename: ".kilocode/rules/contextstream.md",
765
- description: "Kilo Code AI rules",
766
- build: (rules) => `# Kilo Code Rules
767
- ${rules}
768
- `
769
- },
770
- roo: {
771
- filename: ".roo/rules/contextstream.md",
772
- description: "Roo Code AI rules",
773
- build: (rules) => `# Roo Code Rules
774
- ${rules}
775
- `
776
- },
777
- claude: {
778
- filename: "CLAUDE.md",
779
- description: "Claude Code instructions",
780
- build: (rules) => `# Claude Code Instructions
781
- ${rules}
782
- `
783
- },
784
- aider: {
785
- filename: ".aider.conf.yml",
786
- description: "Aider configuration with system prompt",
787
- build: (rules) => `# Aider Configuration
788
- # Note: Aider uses different config format - this adds to the system prompt
789
-
790
- # Add ContextStream guidance to conventions
791
- conventions: |
792
- ${rules.split("\n").map((line) => " " + line).join("\n")}
793
- `
794
- },
795
- antigravity: {
796
- filename: "GEMINI.md",
797
- description: "Google Antigravity AI rules",
798
- build: (rules) => `# Antigravity Agent Rules
799
- ${rules}
800
- `
801
- }
802
- };
1143
+ if __name__ == "__main__":
1144
+ main()
1145
+ `;
1146
+ CLINE_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
1147
+ """
1148
+ ContextStream PreToolUse Hook for Cline
1149
+ Blocks discovery tools and redirects to ContextStream search.
1150
+
1151
+ Cline hooks use JSON output format:
1152
+ {
1153
+ "cancel": true/false,
1154
+ "errorMessage": "optional error description",
1155
+ "contextModification": "optional text to inject"
1156
+ }
1157
+ """
1158
+
1159
+ import json
1160
+ import sys
1161
+ import os
1162
+ from pathlib import Path
1163
+ from datetime import datetime, timedelta
1164
+
1165
+ ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
1166
+ INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
1167
+ STALE_THRESHOLD_DAYS = 7
1168
+
1169
+ DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
1170
+
1171
+ def is_discovery_glob(pattern):
1172
+ pattern_lower = pattern.lower()
1173
+ for p in DISCOVERY_PATTERNS:
1174
+ if p in pattern_lower:
1175
+ return True
1176
+ if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
1177
+ return True
1178
+ if "**" in pattern or "*/" in pattern:
1179
+ return True
1180
+ return False
1181
+
1182
+ def is_discovery_grep(file_path):
1183
+ if not file_path or file_path in [".", "./", "*", "**"]:
1184
+ return True
1185
+ if "*" in file_path or "**" in file_path:
1186
+ return True
1187
+ return False
1188
+
1189
+ def is_project_indexed(workspace_roots):
1190
+ """Check if any workspace root is in an indexed project."""
1191
+ if not INDEX_STATUS_FILE.exists():
1192
+ return False, False
1193
+
1194
+ try:
1195
+ with open(INDEX_STATUS_FILE, "r") as f:
1196
+ data = json.load(f)
1197
+ except:
1198
+ return False, False
1199
+
1200
+ projects = data.get("projects", {})
1201
+
1202
+ for workspace in workspace_roots:
1203
+ cwd_path = Path(workspace).resolve()
1204
+ for project_path, info in projects.items():
1205
+ try:
1206
+ indexed_path = Path(project_path).resolve()
1207
+ if cwd_path == indexed_path or indexed_path in cwd_path.parents:
1208
+ indexed_at = info.get("indexed_at")
1209
+ if indexed_at:
1210
+ try:
1211
+ indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
1212
+ if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
1213
+ return True, True
1214
+ except:
1215
+ pass
1216
+ return True, False
1217
+ except:
1218
+ continue
1219
+ return False, False
1220
+
1221
+ def output_allow(context_mod=None):
1222
+ result = {"cancel": False}
1223
+ if context_mod:
1224
+ result["contextModification"] = context_mod
1225
+ print(json.dumps(result))
1226
+ sys.exit(0)
1227
+
1228
+ def output_block(error_msg, context_mod=None):
1229
+ result = {"cancel": True, "errorMessage": error_msg}
1230
+ if context_mod:
1231
+ result["contextModification"] = context_mod
1232
+ print(json.dumps(result))
1233
+ sys.exit(0)
1234
+
1235
+ def main():
1236
+ if not ENABLED:
1237
+ output_allow()
1238
+
1239
+ try:
1240
+ data = json.load(sys.stdin)
1241
+ except:
1242
+ output_allow()
1243
+
1244
+ hook_name = data.get("hookName", "")
1245
+ if hook_name != "PreToolUse":
1246
+ output_allow()
1247
+
1248
+ tool = data.get("toolName", "")
1249
+ params = data.get("toolParameters", {})
1250
+ workspace_roots = data.get("workspaceRoots", [])
1251
+
1252
+ # Check if project is indexed
1253
+ is_indexed, is_stale = is_project_indexed(workspace_roots)
1254
+ if not is_indexed:
1255
+ output_allow()
1256
+
1257
+ # Check for discovery patterns
1258
+ if tool == "list_files" or tool == "search_files":
1259
+ pattern = params.get("path", "") or params.get("regex", "")
1260
+ if is_discovery_glob(pattern) or is_discovery_grep(pattern):
1261
+ output_block(
1262
+ f"Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}. "
1263
+ "ContextStream search is indexed and faster. Only use local tools if ContextStream returns 0 results.",
1264
+ "[CONTEXTSTREAM] Use ContextStream search for code discovery."
1265
+ )
1266
+
1267
+ elif tool == "read_file":
1268
+ # Allow read_file by default - blocking discovery at search level is enough
1269
+ pass
1270
+
1271
+ output_allow()
1272
+
1273
+ if __name__ == "__main__":
1274
+ main()
1275
+ `;
1276
+ CLINE_USER_PROMPT_HOOK_SCRIPT = `#!/usr/bin/env python3
1277
+ """
1278
+ ContextStream UserPromptSubmit Hook for Cline
1279
+ Injects reminder about ContextStream rules on every message.
1280
+ """
1281
+
1282
+ import json
1283
+ import sys
1284
+ import os
1285
+
1286
+ ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
1287
+
1288
+ REMINDER = """[CONTEXTSTREAM RULES]
1289
+ 1. BEFORE list_files/search_files/read_file: mcp__contextstream__search(mode="hybrid") FIRST
1290
+ 2. Call context_smart at start of EVERY response
1291
+ 3. Local tools ONLY if ContextStream returns 0 results
1292
+ [END RULES]"""
1293
+
1294
+ def main():
1295
+ if not ENABLED:
1296
+ print(json.dumps({"cancel": False}))
1297
+ sys.exit(0)
1298
+
1299
+ try:
1300
+ json.load(sys.stdin)
1301
+ except:
1302
+ print(json.dumps({"cancel": False}))
1303
+ sys.exit(0)
1304
+
1305
+ print(json.dumps({
1306
+ "cancel": False,
1307
+ "contextModification": REMINDER
1308
+ }))
1309
+ sys.exit(0)
1310
+
1311
+ if __name__ == "__main__":
1312
+ main()
1313
+ `;
1314
+ CLINE_POSTTOOLUSE_HOOK_SCRIPT = `#!/bin/bash
1315
+ # ContextStream PostToolUse Hook for Cline/Roo/Kilo Code
1316
+ # Indexes files after Edit/Write/NotebookEdit operations for real-time search updates.
1317
+ #
1318
+ # The hook receives JSON on stdin with tool_name and toolParameters.
1319
+ # Only runs for write operations (write_to_file, edit_file).
1320
+
1321
+ TOOL_NAME=$(cat | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('toolName', d.get('tool_name', '')))" 2>/dev/null)
1322
+
1323
+ case "$TOOL_NAME" in
1324
+ write_to_file|edit_file|Write|Edit|NotebookEdit)
1325
+ npx @contextstream/mcp-server hook post-write
1326
+ ;;
1327
+ esac
1328
+
1329
+ exit 0
1330
+ `;
1331
+ CLINE_HOOK_WRAPPER = (hookName) => `#!/bin/bash
1332
+ # ContextStream ${hookName} Hook Wrapper for Cline/Roo/Kilo Code
1333
+ # Calls the Node.js hook via npx
1334
+ exec npx @contextstream/mcp-server hook ${hookName}
1335
+ `;
1336
+ CURSOR_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
1337
+ """
1338
+ ContextStream PreToolUse Hook for Cursor
1339
+ Blocks discovery tools and redirects to ContextStream search.
1340
+
1341
+ Cursor hooks use JSON output format:
1342
+ {
1343
+ "decision": "allow" | "deny",
1344
+ "reason": "optional error description"
1345
+ }
1346
+ """
1347
+
1348
+ import json
1349
+ import sys
1350
+ import os
1351
+ from pathlib import Path
1352
+ from datetime import datetime, timedelta
1353
+
1354
+ ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
1355
+ INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
1356
+ STALE_THRESHOLD_DAYS = 7
1357
+
1358
+ DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
1359
+
1360
+ def is_discovery_glob(pattern):
1361
+ pattern_lower = pattern.lower()
1362
+ for p in DISCOVERY_PATTERNS:
1363
+ if p in pattern_lower:
1364
+ return True
1365
+ if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
1366
+ return True
1367
+ if "**" in pattern or "*/" in pattern:
1368
+ return True
1369
+ return False
1370
+
1371
+ def is_discovery_grep(file_path):
1372
+ if not file_path or file_path in [".", "./", "*", "**"]:
1373
+ return True
1374
+ if "*" in file_path or "**" in file_path:
1375
+ return True
1376
+ return False
1377
+
1378
+ def is_project_indexed(workspace_roots):
1379
+ """Check if any workspace root is in an indexed project."""
1380
+ if not INDEX_STATUS_FILE.exists():
1381
+ return False, False
1382
+
1383
+ try:
1384
+ with open(INDEX_STATUS_FILE, "r") as f:
1385
+ data = json.load(f)
1386
+ except:
1387
+ return False, False
1388
+
1389
+ projects = data.get("projects", {})
1390
+
1391
+ for workspace in workspace_roots:
1392
+ cwd_path = Path(workspace).resolve()
1393
+ for project_path, info in projects.items():
1394
+ try:
1395
+ indexed_path = Path(project_path).resolve()
1396
+ if cwd_path == indexed_path or indexed_path in cwd_path.parents:
1397
+ indexed_at = info.get("indexed_at")
1398
+ if indexed_at:
1399
+ try:
1400
+ indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
1401
+ if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
1402
+ return True, True
1403
+ except:
1404
+ pass
1405
+ return True, False
1406
+ except:
1407
+ continue
1408
+ return False, False
1409
+
1410
+ def output_allow():
1411
+ print(json.dumps({"decision": "allow"}))
1412
+ sys.exit(0)
1413
+
1414
+ def output_deny(reason):
1415
+ print(json.dumps({"decision": "deny", "reason": reason}))
1416
+ sys.exit(0)
1417
+
1418
+ def main():
1419
+ if not ENABLED:
1420
+ output_allow()
1421
+
1422
+ try:
1423
+ data = json.load(sys.stdin)
1424
+ except:
1425
+ output_allow()
1426
+
1427
+ hook_name = data.get("hook_event_name", "")
1428
+ if hook_name != "preToolUse":
1429
+ output_allow()
1430
+
1431
+ tool = data.get("tool_name", "")
1432
+ params = data.get("tool_input", {}) or data.get("parameters", {})
1433
+ workspace_roots = data.get("workspace_roots", [])
1434
+
1435
+ # Check if project is indexed
1436
+ is_indexed, _ = is_project_indexed(workspace_roots)
1437
+ if not is_indexed:
1438
+ output_allow()
1439
+
1440
+ # Check for Cursor tools
1441
+ if tool in ["Glob", "glob", "list_files"]:
1442
+ pattern = params.get("pattern", "") or params.get("path", "")
1443
+ if is_discovery_glob(pattern):
1444
+ output_deny(
1445
+ f"Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}. "
1446
+ "ContextStream search is indexed and faster."
1447
+ )
1448
+
1449
+ elif tool in ["Grep", "grep", "search_files", "ripgrep"]:
1450
+ pattern = params.get("pattern", "") or params.get("regex", "")
1451
+ file_path = params.get("path", "")
1452
+ if is_discovery_grep(file_path):
1453
+ output_deny(
1454
+ f"Use mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") instead of {tool}. "
1455
+ "ContextStream search is indexed and faster."
1456
+ )
1457
+
1458
+ output_allow()
1459
+
1460
+ if __name__ == "__main__":
1461
+ main()
1462
+ `;
1463
+ CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT = `#!/usr/bin/env python3
1464
+ """
1465
+ ContextStream BeforeSubmitPrompt Hook for Cursor
1466
+ Injects reminder about ContextStream rules.
1467
+ """
1468
+
1469
+ import json
1470
+ import sys
1471
+ import os
1472
+
1473
+ ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
1474
+
1475
+ def main():
1476
+ if not ENABLED:
1477
+ print(json.dumps({"continue": True}))
1478
+ sys.exit(0)
1479
+
1480
+ try:
1481
+ json.load(sys.stdin)
1482
+ except:
1483
+ print(json.dumps({"continue": True}))
1484
+ sys.exit(0)
1485
+
1486
+ print(json.dumps({
1487
+ "continue": True,
1488
+ "user_message": "[CONTEXTSTREAM] Search with mcp__contextstream__search before using Glob/Grep/Read"
1489
+ }))
1490
+ sys.exit(0)
1491
+
1492
+ if __name__ == "__main__":
1493
+ main()
1494
+ `;
803
1495
  }
804
1496
  });
805
1497
 
806
1498
  // src/hooks/auto-rules.ts
807
- import * as fs from "node:fs";
808
- import * as path from "node:path";
809
- import { homedir } from "node:os";
1499
+ import * as fs2 from "node:fs";
1500
+ import * as path2 from "node:path";
1501
+ import { homedir as homedir2 } from "node:os";
810
1502
  var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
811
1503
  var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
812
1504
  var ENABLED = process.env.CONTEXTSTREAM_AUTO_RULES !== "false";
813
- var MARKER_FILE = path.join(homedir(), ".contextstream", ".auto-rules-ran");
1505
+ var MARKER_FILE = path2.join(homedir2(), ".contextstream", ".auto-rules-ran");
814
1506
  var COOLDOWN_MS = 4 * 60 * 60 * 1e3;
815
1507
  function hasRunRecently() {
816
1508
  try {
817
- if (!fs.existsSync(MARKER_FILE)) return false;
818
- const stat = fs.statSync(MARKER_FILE);
1509
+ if (!fs2.existsSync(MARKER_FILE)) return false;
1510
+ const stat = fs2.statSync(MARKER_FILE);
819
1511
  const age = Date.now() - stat.mtimeMs;
820
1512
  return age < COOLDOWN_MS;
821
1513
  } catch {
@@ -824,11 +1516,11 @@ function hasRunRecently() {
824
1516
  }
825
1517
  function markAsRan() {
826
1518
  try {
827
- const dir = path.dirname(MARKER_FILE);
828
- if (!fs.existsSync(dir)) {
829
- fs.mkdirSync(dir, { recursive: true });
1519
+ const dir = path2.dirname(MARKER_FILE);
1520
+ if (!fs2.existsSync(dir)) {
1521
+ fs2.mkdirSync(dir, { recursive: true });
830
1522
  }
831
- fs.writeFileSync(MARKER_FILE, (/* @__PURE__ */ new Date()).toISOString());
1523
+ fs2.writeFileSync(MARKER_FILE, (/* @__PURE__ */ new Date()).toISOString());
832
1524
  } catch {
833
1525
  }
834
1526
  }
@@ -855,14 +1547,49 @@ function extractCwd(input) {
855
1547
  if (input.cwd) return input.cwd;
856
1548
  return process.cwd();
857
1549
  }
858
- async function generateRulesForFolder(folderPath) {
859
- const { generateAllRuleFiles: generateAllRuleFiles2 } = await Promise.resolve().then(() => (init_rules_templates(), rules_templates_exports));
860
- await generateAllRuleFiles2({
861
- folderPath,
862
- editors: ["cursor", "cline", "kilo", "roo", "claude", "aider", "codex"],
863
- overwriteExisting: true,
864
- mode: "minimal"
865
- // Use minimal mode for auto-updates
1550
+ function hasPythonHooks(settingsPath) {
1551
+ try {
1552
+ if (!fs2.existsSync(settingsPath)) return false;
1553
+ const content = fs2.readFileSync(settingsPath, "utf-8");
1554
+ const settings = JSON.parse(content);
1555
+ const hooks = settings.hooks;
1556
+ if (!hooks) return false;
1557
+ for (const hookType of Object.keys(hooks)) {
1558
+ const matchers = hooks[hookType];
1559
+ if (!Array.isArray(matchers)) continue;
1560
+ for (const matcher of matchers) {
1561
+ const hookList = matcher.hooks;
1562
+ if (!Array.isArray(hookList)) continue;
1563
+ for (const hook of hookList) {
1564
+ const cmd = hook.command || "";
1565
+ if (cmd.includes("python3") && cmd.includes("contextstream")) {
1566
+ return true;
1567
+ }
1568
+ }
1569
+ }
1570
+ }
1571
+ return false;
1572
+ } catch {
1573
+ return false;
1574
+ }
1575
+ }
1576
+ function detectPythonHooks(cwd) {
1577
+ const globalSettingsPath = path2.join(homedir2(), ".claude", "settings.json");
1578
+ const projectSettingsPath = path2.join(cwd, ".claude", "settings.json");
1579
+ return {
1580
+ global: hasPythonHooks(globalSettingsPath),
1581
+ project: hasPythonHooks(projectSettingsPath)
1582
+ };
1583
+ }
1584
+ async function upgradeHooksForFolder(folderPath) {
1585
+ const { installClaudeCodeHooks: installClaudeCodeHooks2 } = await Promise.resolve().then(() => (init_hooks_config(), hooks_config_exports));
1586
+ await installClaudeCodeHooks2({
1587
+ scope: "both",
1588
+ projectPath: folderPath,
1589
+ includePreCompact: true,
1590
+ includeMediaAware: true,
1591
+ includePostWrite: true,
1592
+ includeAutoRules: true
866
1593
  });
867
1594
  }
868
1595
  async function runAutoRulesHook() {
@@ -890,14 +1617,17 @@ async function runAutoRulesHook() {
890
1617
  if (!isContextTool) {
891
1618
  process.exit(0);
892
1619
  }
1620
+ const cwd = extractCwd(input);
1621
+ const pythonHooks = detectPythonHooks(cwd);
1622
+ const hasPythonHooksToUpgrade = pythonHooks.global || pythonHooks.project;
893
1623
  const rulesNotice = extractRulesNotice(input);
894
- if (!rulesNotice || rulesNotice.status === "current") {
1624
+ const rulesNeedUpdate = rulesNotice && rulesNotice.status !== "current";
1625
+ if (!hasPythonHooksToUpgrade && !rulesNeedUpdate) {
895
1626
  process.exit(0);
896
1627
  }
897
- const cwd = extractCwd(input);
898
- const folderPath = rulesNotice.update_args?.folder_path || cwd;
1628
+ const folderPath = rulesNotice?.update_args?.folder_path || cwd;
899
1629
  try {
900
- await generateRulesForFolder(folderPath);
1630
+ await upgradeHooksForFolder(folderPath);
901
1631
  markAsRan();
902
1632
  } catch {
903
1633
  }