@contextstream/mcp-server 0.4.81 → 1.0.0

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.
@@ -1,3384 +0,0 @@
1
- #!/usr/bin/env node
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
- };
11
- var __commonJS = (cb, mod) => function __require() {
12
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
13
- };
14
- var __export = (target, all) => {
15
- for (var name in all)
16
- __defProp(target, name, { get: all[name], enumerable: true });
17
- };
18
- var __copyProps = (to, from, except, desc) => {
19
- if (from && typeof from === "object" || typeof from === "function") {
20
- for (let key of __getOwnPropNames(from))
21
- if (!__hasOwnProp.call(to, key) && key !== except)
22
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
- }
24
- return to;
25
- };
26
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
27
- // If the importer is in node compatibility mode or this is not an ESM
28
- // file that has been converted to a CommonJS file using a Babel-
29
- // compatible transform (i.e. "__esModule" has not been set), then set
30
- // "default" to the CommonJS "module.exports" for node compatibility.
31
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
32
- mod
33
- ));
34
-
35
- // node_modules/ignore/index.js
36
- var require_ignore = __commonJS({
37
- "node_modules/ignore/index.js"(exports, module) {
38
- function makeArray(subject) {
39
- return Array.isArray(subject) ? subject : [subject];
40
- }
41
- var UNDEFINED = void 0;
42
- var EMPTY = "";
43
- var SPACE = " ";
44
- var ESCAPE = "\\";
45
- var REGEX_TEST_BLANK_LINE = /^\s+$/;
46
- var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
47
- var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
48
- var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
49
- var REGEX_SPLITALL_CRLF = /\r?\n/g;
50
- var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
51
- var REGEX_TEST_TRAILING_SLASH = /\/$/;
52
- var SLASH = "/";
53
- var TMP_KEY_IGNORE = "node-ignore";
54
- if (typeof Symbol !== "undefined") {
55
- TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
56
- }
57
- var KEY_IGNORE = TMP_KEY_IGNORE;
58
- var define = (object, key, value) => {
59
- Object.defineProperty(object, key, { value });
60
- return value;
61
- };
62
- var REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
63
- var RETURN_FALSE = () => false;
64
- var sanitizeRange = (range) => range.replace(
65
- REGEX_REGEXP_RANGE,
66
- (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match : EMPTY
67
- );
68
- var cleanRangeBackSlash = (slashes) => {
69
- const { length } = slashes;
70
- return slashes.slice(0, length - length % 2);
71
- };
72
- var REPLACERS = [
73
- [
74
- // Remove BOM
75
- // TODO:
76
- // Other similar zero-width characters?
77
- /^\uFEFF/,
78
- () => EMPTY
79
- ],
80
- // > Trailing spaces are ignored unless they are quoted with backslash ("\")
81
- [
82
- // (a\ ) -> (a )
83
- // (a ) -> (a)
84
- // (a ) -> (a)
85
- // (a \ ) -> (a )
86
- /((?:\\\\)*?)(\\?\s+)$/,
87
- (_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY)
88
- ],
89
- // Replace (\ ) with ' '
90
- // (\ ) -> ' '
91
- // (\\ ) -> '\\ '
92
- // (\\\ ) -> '\\ '
93
- [
94
- /(\\+?)\s/g,
95
- (_, m1) => {
96
- const { length } = m1;
97
- return m1.slice(0, length - length % 2) + SPACE;
98
- }
99
- ],
100
- // Escape metacharacters
101
- // which is written down by users but means special for regular expressions.
102
- // > There are 12 characters with special meanings:
103
- // > - the backslash \,
104
- // > - the caret ^,
105
- // > - the dollar sign $,
106
- // > - the period or dot .,
107
- // > - the vertical bar or pipe symbol |,
108
- // > - the question mark ?,
109
- // > - the asterisk or star *,
110
- // > - the plus sign +,
111
- // > - the opening parenthesis (,
112
- // > - the closing parenthesis ),
113
- // > - and the opening square bracket [,
114
- // > - the opening curly brace {,
115
- // > These special characters are often called "metacharacters".
116
- [
117
- /[\\$.|*+(){^]/g,
118
- (match) => `\\${match}`
119
- ],
120
- [
121
- // > a question mark (?) matches a single character
122
- /(?!\\)\?/g,
123
- () => "[^/]"
124
- ],
125
- // leading slash
126
- [
127
- // > A leading slash matches the beginning of the pathname.
128
- // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
129
- // A leading slash matches the beginning of the pathname
130
- /^\//,
131
- () => "^"
132
- ],
133
- // replace special metacharacter slash after the leading slash
134
- [
135
- /\//g,
136
- () => "\\/"
137
- ],
138
- [
139
- // > A leading "**" followed by a slash means match in all directories.
140
- // > For example, "**/foo" matches file or directory "foo" anywhere,
141
- // > the same as pattern "foo".
142
- // > "**/foo/bar" matches file or directory "bar" anywhere that is directly
143
- // > under directory "foo".
144
- // Notice that the '*'s have been replaced as '\\*'
145
- /^\^*\\\*\\\*\\\//,
146
- // '**/foo' <-> 'foo'
147
- () => "^(?:.*\\/)?"
148
- ],
149
- // starting
150
- [
151
- // there will be no leading '/'
152
- // (which has been replaced by section "leading slash")
153
- // If starts with '**', adding a '^' to the regular expression also works
154
- /^(?=[^^])/,
155
- function startingReplacer() {
156
- return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^";
157
- }
158
- ],
159
- // two globstars
160
- [
161
- // Use lookahead assertions so that we could match more than one `'/**'`
162
- /\\\/\\\*\\\*(?=\\\/|$)/g,
163
- // Zero, one or several directories
164
- // should not use '*', or it will be replaced by the next replacer
165
- // Check if it is not the last `'/**'`
166
- (_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
167
- ],
168
- // normal intermediate wildcards
169
- [
170
- // Never replace escaped '*'
171
- // ignore rule '\*' will match the path '*'
172
- // 'abc.*/' -> go
173
- // 'abc.*' -> skip this rule,
174
- // coz trailing single wildcard will be handed by [trailing wildcard]
175
- /(^|[^\\]+)(\\\*)+(?=.+)/g,
176
- // '*.js' matches '.js'
177
- // '*.js' doesn't match 'abc'
178
- (_, p1, p2) => {
179
- const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
180
- return p1 + unescaped;
181
- }
182
- ],
183
- [
184
- // unescape, revert step 3 except for back slash
185
- // For example, if a user escape a '\\*',
186
- // after step 3, the result will be '\\\\\\*'
187
- /\\\\\\(?=[$.|*+(){^])/g,
188
- () => ESCAPE
189
- ],
190
- [
191
- // '\\\\' -> '\\'
192
- /\\\\/g,
193
- () => ESCAPE
194
- ],
195
- [
196
- // > The range notation, e.g. [a-zA-Z],
197
- // > can be used to match one of the characters in a range.
198
- // `\` is escaped by step 3
199
- /(\\)?\[([^\]/]*?)(\\*)($|\])/g,
200
- (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]"
201
- ],
202
- // ending
203
- [
204
- // 'js' will not match 'js.'
205
- // 'ab' will not match 'abc'
206
- /(?:[^*])$/,
207
- // WTF!
208
- // https://git-scm.com/docs/gitignore
209
- // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
210
- // which re-fixes #24, #38
211
- // > If there is a separator at the end of the pattern then the pattern
212
- // > will only match directories, otherwise the pattern can match both
213
- // > files and directories.
214
- // 'js*' will not match 'a.js'
215
- // 'js/' will not match 'a.js'
216
- // 'js' will match 'a.js' and 'a.js/'
217
- (match) => /\/$/.test(match) ? `${match}$` : `${match}(?=$|\\/$)`
218
- ]
219
- ];
220
- var REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/;
221
- var MODE_IGNORE = "regex";
222
- var MODE_CHECK_IGNORE = "checkRegex";
223
- var UNDERSCORE = "_";
224
- var TRAILING_WILD_CARD_REPLACERS = {
225
- [MODE_IGNORE](_, p1) {
226
- const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
227
- return `${prefix}(?=$|\\/$)`;
228
- },
229
- [MODE_CHECK_IGNORE](_, p1) {
230
- const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
231
- return `${prefix}(?=$|\\/$)`;
232
- }
233
- };
234
- var makeRegexPrefix = (pattern) => REPLACERS.reduce(
235
- (prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)),
236
- pattern
237
- );
238
- var isString = (subject) => typeof subject === "string";
239
- var checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0;
240
- var splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean);
241
- var IgnoreRule = class {
242
- constructor(pattern, mark, body, ignoreCase, negative, prefix) {
243
- this.pattern = pattern;
244
- this.mark = mark;
245
- this.negative = negative;
246
- define(this, "body", body);
247
- define(this, "ignoreCase", ignoreCase);
248
- define(this, "regexPrefix", prefix);
249
- }
250
- get regex() {
251
- const key = UNDERSCORE + MODE_IGNORE;
252
- if (this[key]) {
253
- return this[key];
254
- }
255
- return this._make(MODE_IGNORE, key);
256
- }
257
- get checkRegex() {
258
- const key = UNDERSCORE + MODE_CHECK_IGNORE;
259
- if (this[key]) {
260
- return this[key];
261
- }
262
- return this._make(MODE_CHECK_IGNORE, key);
263
- }
264
- _make(mode, key) {
265
- const str = this.regexPrefix.replace(
266
- REGEX_REPLACE_TRAILING_WILDCARD,
267
- // It does not need to bind pattern
268
- TRAILING_WILD_CARD_REPLACERS[mode]
269
- );
270
- const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
271
- return define(this, key, regex);
272
- }
273
- };
274
- var createRule = ({
275
- pattern,
276
- mark
277
- }, ignoreCase) => {
278
- let negative = false;
279
- let body = pattern;
280
- if (body.indexOf("!") === 0) {
281
- negative = true;
282
- body = body.substr(1);
283
- }
284
- body = body.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
285
- const regexPrefix = makeRegexPrefix(body);
286
- return new IgnoreRule(
287
- pattern,
288
- mark,
289
- body,
290
- ignoreCase,
291
- negative,
292
- regexPrefix
293
- );
294
- };
295
- var RuleManager = class {
296
- constructor(ignoreCase) {
297
- this._ignoreCase = ignoreCase;
298
- this._rules = [];
299
- }
300
- _add(pattern) {
301
- if (pattern && pattern[KEY_IGNORE]) {
302
- this._rules = this._rules.concat(pattern._rules._rules);
303
- this._added = true;
304
- return;
305
- }
306
- if (isString(pattern)) {
307
- pattern = {
308
- pattern
309
- };
310
- }
311
- if (checkPattern(pattern.pattern)) {
312
- const rule = createRule(pattern, this._ignoreCase);
313
- this._added = true;
314
- this._rules.push(rule);
315
- }
316
- }
317
- // @param {Array<string> | string | Ignore} pattern
318
- add(pattern) {
319
- this._added = false;
320
- makeArray(
321
- isString(pattern) ? splitPattern(pattern) : pattern
322
- ).forEach(this._add, this);
323
- return this._added;
324
- }
325
- // Test one single path without recursively checking parent directories
326
- //
327
- // - checkUnignored `boolean` whether should check if the path is unignored,
328
- // setting `checkUnignored` to `false` could reduce additional
329
- // path matching.
330
- // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
331
- // @returns {TestResult} true if a file is ignored
332
- test(path5, checkUnignored, mode) {
333
- let ignored = false;
334
- let unignored = false;
335
- let matchedRule;
336
- this._rules.forEach((rule) => {
337
- const { negative } = rule;
338
- if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
339
- return;
340
- }
341
- const matched = rule[mode].test(path5);
342
- if (!matched) {
343
- return;
344
- }
345
- ignored = !negative;
346
- unignored = negative;
347
- matchedRule = negative ? UNDEFINED : rule;
348
- });
349
- const ret = {
350
- ignored,
351
- unignored
352
- };
353
- if (matchedRule) {
354
- ret.rule = matchedRule;
355
- }
356
- return ret;
357
- }
358
- };
359
- var throwError = (message, Ctor) => {
360
- throw new Ctor(message);
361
- };
362
- var checkPath = (path5, originalPath, doThrow) => {
363
- if (!isString(path5)) {
364
- return doThrow(
365
- `path must be a string, but got \`${originalPath}\``,
366
- TypeError
367
- );
368
- }
369
- if (!path5) {
370
- return doThrow(`path must not be empty`, TypeError);
371
- }
372
- if (checkPath.isNotRelative(path5)) {
373
- const r = "`path.relative()`d";
374
- return doThrow(
375
- `path should be a ${r} string, but got "${originalPath}"`,
376
- RangeError
377
- );
378
- }
379
- return true;
380
- };
381
- var isNotRelative = (path5) => REGEX_TEST_INVALID_PATH.test(path5);
382
- checkPath.isNotRelative = isNotRelative;
383
- checkPath.convert = (p) => p;
384
- var Ignore2 = class {
385
- constructor({
386
- ignorecase = true,
387
- ignoreCase = ignorecase,
388
- allowRelativePaths = false
389
- } = {}) {
390
- define(this, KEY_IGNORE, true);
391
- this._rules = new RuleManager(ignoreCase);
392
- this._strictPathCheck = !allowRelativePaths;
393
- this._initCache();
394
- }
395
- _initCache() {
396
- this._ignoreCache = /* @__PURE__ */ Object.create(null);
397
- this._testCache = /* @__PURE__ */ Object.create(null);
398
- }
399
- add(pattern) {
400
- if (this._rules.add(pattern)) {
401
- this._initCache();
402
- }
403
- return this;
404
- }
405
- // legacy
406
- addPattern(pattern) {
407
- return this.add(pattern);
408
- }
409
- // @returns {TestResult}
410
- _test(originalPath, cache, checkUnignored, slices) {
411
- const path5 = originalPath && checkPath.convert(originalPath);
412
- checkPath(
413
- path5,
414
- originalPath,
415
- this._strictPathCheck ? throwError : RETURN_FALSE
416
- );
417
- return this._t(path5, cache, checkUnignored, slices);
418
- }
419
- checkIgnore(path5) {
420
- if (!REGEX_TEST_TRAILING_SLASH.test(path5)) {
421
- return this.test(path5);
422
- }
423
- const slices = path5.split(SLASH).filter(Boolean);
424
- slices.pop();
425
- if (slices.length) {
426
- const parent = this._t(
427
- slices.join(SLASH) + SLASH,
428
- this._testCache,
429
- true,
430
- slices
431
- );
432
- if (parent.ignored) {
433
- return parent;
434
- }
435
- }
436
- return this._rules.test(path5, false, MODE_CHECK_IGNORE);
437
- }
438
- _t(path5, cache, checkUnignored, slices) {
439
- if (path5 in cache) {
440
- return cache[path5];
441
- }
442
- if (!slices) {
443
- slices = path5.split(SLASH).filter(Boolean);
444
- }
445
- slices.pop();
446
- if (!slices.length) {
447
- return cache[path5] = this._rules.test(path5, checkUnignored, MODE_IGNORE);
448
- }
449
- const parent = this._t(
450
- slices.join(SLASH) + SLASH,
451
- cache,
452
- checkUnignored,
453
- slices
454
- );
455
- return cache[path5] = parent.ignored ? parent : this._rules.test(path5, checkUnignored, MODE_IGNORE);
456
- }
457
- ignores(path5) {
458
- return this._test(path5, this._ignoreCache, false).ignored;
459
- }
460
- createFilter() {
461
- return (path5) => !this.ignores(path5);
462
- }
463
- filter(paths) {
464
- return makeArray(paths).filter(this.createFilter());
465
- }
466
- // @returns {TestResult}
467
- test(path5) {
468
- return this._test(path5, this._testCache, true);
469
- }
470
- };
471
- var factory = (options) => new Ignore2(options);
472
- var isPathValid = (path5) => checkPath(path5 && checkPath.convert(path5), path5, RETURN_FALSE);
473
- var setupWindows = () => {
474
- const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
475
- checkPath.convert = makePosix;
476
- const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
477
- checkPath.isNotRelative = (path5) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path5) || isNotRelative(path5);
478
- };
479
- if (
480
- // Detect `process` so that it can run in browsers.
481
- typeof process !== "undefined" && process.platform === "win32"
482
- ) {
483
- setupWindows();
484
- }
485
- module.exports = factory;
486
- factory.default = factory;
487
- module.exports.isPathValid = isPathValid;
488
- define(module.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
489
- }
490
- });
491
-
492
- // src/ignore.ts
493
- import * as fs from "fs";
494
- import * as path from "path";
495
- async function loadIgnorePatterns(projectRoot) {
496
- const ig = (0, import_ignore.default)();
497
- const patterns = [...DEFAULT_IGNORE_PATTERNS];
498
- ig.add(DEFAULT_IGNORE_PATTERNS);
499
- const ignoreFilePath = path.join(projectRoot, IGNORE_FILENAME);
500
- let hasUserPatterns = false;
501
- try {
502
- const content = await fs.promises.readFile(ignoreFilePath, "utf-8");
503
- const userPatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
504
- if (userPatterns.length > 0) {
505
- ig.add(userPatterns);
506
- patterns.push(...userPatterns);
507
- hasUserPatterns = true;
508
- }
509
- } catch {
510
- }
511
- return {
512
- ignores: (pathname) => ig.ignores(pathname),
513
- patterns,
514
- hasUserPatterns
515
- };
516
- }
517
- var import_ignore, IGNORE_FILENAME, DEFAULT_IGNORE_PATTERNS;
518
- var init_ignore = __esm({
519
- "src/ignore.ts"() {
520
- "use strict";
521
- import_ignore = __toESM(require_ignore(), 1);
522
- IGNORE_FILENAME = ".contextstream/ignore";
523
- DEFAULT_IGNORE_PATTERNS = [
524
- // Version control
525
- ".git/",
526
- ".svn/",
527
- ".hg/",
528
- // Package managers / dependencies
529
- "node_modules/",
530
- "vendor/",
531
- ".pnpm/",
532
- // Build outputs
533
- "target/",
534
- "dist/",
535
- "build/",
536
- "out/",
537
- ".next/",
538
- ".nuxt/",
539
- ".svelte-kit/",
540
- ".parcel-cache/",
541
- ".turbo/",
542
- ".gradle/",
543
- ".cache/",
544
- "bin/",
545
- "obj/",
546
- // Python
547
- "__pycache__/",
548
- ".pytest_cache/",
549
- ".mypy_cache/",
550
- "venv/",
551
- ".venv/",
552
- "env/",
553
- ".env/",
554
- // IDE
555
- ".idea/",
556
- ".vscode/",
557
- ".vs/",
558
- // Coverage
559
- "coverage/",
560
- ".coverage/",
561
- // Lock files
562
- "package-lock.json",
563
- "yarn.lock",
564
- "pnpm-lock.yaml",
565
- "Cargo.lock",
566
- "poetry.lock",
567
- "Gemfile.lock",
568
- "composer.lock",
569
- "*.min.js",
570
- "*.min.css",
571
- // OS files
572
- ".DS_Store",
573
- "Thumbs.db"
574
- ];
575
- }
576
- });
577
-
578
- // src/files.ts
579
- var files_exports = {};
580
- __export(files_exports, {
581
- clearGitContextCache: () => clearGitContextCache,
582
- countIndexableFiles: () => countIndexableFiles,
583
- deleteHashManifest: () => deleteHashManifest,
584
- detectLanguage: () => detectLanguage,
585
- readAllFilesInBatches: () => readAllFilesInBatches,
586
- readChangedFilesInBatches: () => readChangedFilesInBatches,
587
- readFilesFromDirectory: () => readFilesFromDirectory,
588
- readHashManifest: () => readHashManifest,
589
- sha256Hex: () => sha256Hex,
590
- writeHashManifest: () => writeHashManifest
591
- });
592
- import * as fs2 from "fs";
593
- import * as path2 from "path";
594
- import { exec } from "child_process";
595
- import { promisify } from "util";
596
- import * as os from "os";
597
- import * as crypto from "crypto";
598
- function getMachineId() {
599
- const hostname2 = os.hostname();
600
- const hash = crypto.createHash("sha256").update(hostname2).digest("hex");
601
- return hash.substring(0, 12);
602
- }
603
- async function isGitRepo(rootPath) {
604
- try {
605
- await execAsync("git rev-parse --is-inside-work-tree", {
606
- cwd: rootPath,
607
- timeout: 5e3
608
- });
609
- return true;
610
- } catch {
611
- return false;
612
- }
613
- }
614
- async function getGitBranch(rootPath) {
615
- try {
616
- const { stdout } = await execAsync("git branch --show-current", {
617
- cwd: rootPath,
618
- timeout: 5e3
619
- });
620
- const branch = stdout.trim();
621
- return branch || void 0;
622
- } catch {
623
- return void 0;
624
- }
625
- }
626
- async function getGitDefaultBranch(rootPath) {
627
- try {
628
- const { stdout } = await execAsync(
629
- "git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null",
630
- { cwd: rootPath, timeout: 5e3 }
631
- );
632
- const match = stdout.trim().match(/refs\/remotes\/origin\/(.+)/);
633
- if (match) return match[1];
634
- } catch {
635
- }
636
- try {
637
- const { stdout } = await execAsync(
638
- "git config --get init.defaultBranch 2>/dev/null",
639
- { cwd: rootPath, timeout: 5e3 }
640
- );
641
- const branch = stdout.trim();
642
- if (branch) return branch;
643
- } catch {
644
- }
645
- try {
646
- const { stdout } = await execAsync("git branch --list main master", {
647
- cwd: rootPath,
648
- timeout: 5e3
649
- });
650
- const branches = stdout.trim().split("\n").map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
651
- if (branches.includes("main")) return "main";
652
- if (branches.includes("master")) return "master";
653
- } catch {
654
- }
655
- return void 0;
656
- }
657
- async function getFileGitInfo(rootPath, relativePath) {
658
- try {
659
- const { stdout } = await execAsync(
660
- `git log -1 --format="%H %ct" -- "${relativePath}"`,
661
- { cwd: rootPath, timeout: 5e3 }
662
- );
663
- const parts = stdout.trim().split(" ");
664
- if (parts.length >= 2) {
665
- const sha = parts[0];
666
- const unixTimestamp = parseInt(parts[1], 10);
667
- if (sha && !isNaN(unixTimestamp)) {
668
- const timestamp = new Date(unixTimestamp * 1e3).toISOString();
669
- return { sha, timestamp };
670
- }
671
- }
672
- } catch {
673
- }
674
- return void 0;
675
- }
676
- async function getGitContext(rootPath) {
677
- const cached = gitContextCache.get(rootPath);
678
- if (cached) return cached;
679
- const machineId = getMachineId();
680
- const isRepo = await isGitRepo(rootPath);
681
- if (!isRepo) {
682
- const context2 = { isGitRepo: false, machineId };
683
- gitContextCache.set(rootPath, context2);
684
- return context2;
685
- }
686
- const [branch, defaultBranch] = await Promise.all([
687
- getGitBranch(rootPath),
688
- getGitDefaultBranch(rootPath)
689
- ]);
690
- const context = {
691
- isGitRepo: true,
692
- branch,
693
- defaultBranch,
694
- isDefaultBranch: branch !== void 0 && branch === defaultBranch,
695
- machineId
696
- };
697
- gitContextCache.set(rootPath, context);
698
- return context;
699
- }
700
- function clearGitContextCache() {
701
- gitContextCache.clear();
702
- }
703
- async function readFilesFromDirectory(rootPath, options = {}) {
704
- const maxFiles = options.maxFiles ?? MAX_FILES_PER_BATCH;
705
- const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
706
- const files = [];
707
- const ig = options.ignoreInstance ?? await loadIgnorePatterns(rootPath);
708
- const gitCtx = await getGitContext(rootPath);
709
- async function walkDir(dir, relativePath = "") {
710
- if (files.length >= maxFiles) return;
711
- let entries;
712
- try {
713
- entries = await fs2.promises.readdir(dir, { withFileTypes: true });
714
- } catch {
715
- return;
716
- }
717
- entries.sort((a, b) => a.name.localeCompare(b.name));
718
- for (const entry of entries) {
719
- if (files.length >= maxFiles) break;
720
- const fullPath = path2.join(dir, entry.name);
721
- const relPath = path2.join(relativePath, entry.name);
722
- if (entry.isDirectory()) {
723
- if (IGNORE_DIRS.has(entry.name)) continue;
724
- if (ig.ignores(relPath + "/")) continue;
725
- await walkDir(fullPath, relPath);
726
- } else if (entry.isFile()) {
727
- if (IGNORE_FILES.has(entry.name)) continue;
728
- if (ig.ignores(relPath)) continue;
729
- const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
730
- if (!CODE_EXTENSIONS.has(ext)) continue;
731
- try {
732
- const stat = await fs2.promises.stat(fullPath);
733
- if (stat.size > maxFileSize) continue;
734
- const content = await fs2.promises.readFile(fullPath, "utf-8");
735
- const file = {
736
- path: relPath,
737
- content,
738
- language: detectLanguage(relPath),
739
- // Always include machine_id and source_modified_at
740
- machine_id: gitCtx.machineId,
741
- source_modified_at: stat.mtime.toISOString()
742
- };
743
- if (gitCtx.isGitRepo) {
744
- file.git_branch = gitCtx.branch;
745
- file.git_default_branch = gitCtx.defaultBranch;
746
- file.is_default_branch = gitCtx.isDefaultBranch;
747
- const gitInfo = await getFileGitInfo(rootPath, relPath);
748
- if (gitInfo) {
749
- file.git_commit_sha = gitInfo.sha;
750
- file.git_commit_timestamp = gitInfo.timestamp;
751
- }
752
- }
753
- files.push(file);
754
- } catch {
755
- }
756
- }
757
- }
758
- }
759
- await walkDir(rootPath);
760
- return files;
761
- }
762
- async function* readAllFilesInBatches(rootPath, options = {}) {
763
- const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
764
- const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
765
- const maxFilesPerBatch = options.maxFilesPerBatch ?? options.batchSize ?? MAX_FILES_PER_BATCH;
766
- const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
767
- const ig = options.ignoreInstance ?? await loadIgnorePatterns(rootPath);
768
- const gitCtx = await getGitContext(rootPath);
769
- let batch = [];
770
- let currentBatchBytes = 0;
771
- async function* walkDir(dir, relativePath = "") {
772
- let entries;
773
- try {
774
- entries = await fs2.promises.readdir(dir, { withFileTypes: true });
775
- } catch {
776
- return;
777
- }
778
- entries.sort((a, b) => a.name.localeCompare(b.name));
779
- for (const entry of entries) {
780
- const fullPath = path2.join(dir, entry.name);
781
- const relPath = path2.join(relativePath, entry.name);
782
- if (entry.isDirectory()) {
783
- if (IGNORE_DIRS.has(entry.name)) continue;
784
- if (ig.ignores(relPath + "/")) continue;
785
- yield* walkDir(fullPath, relPath);
786
- } else if (entry.isFile()) {
787
- if (IGNORE_FILES.has(entry.name)) continue;
788
- if (ig.ignores(relPath)) continue;
789
- const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
790
- if (!CODE_EXTENSIONS.has(ext)) continue;
791
- try {
792
- const stat = await fs2.promises.stat(fullPath);
793
- if (stat.size > maxFileSize) continue;
794
- const content = await fs2.promises.readFile(fullPath, "utf-8");
795
- const file = {
796
- path: relPath,
797
- content,
798
- language: detectLanguage(relPath),
799
- sizeBytes: stat.size,
800
- // Always include machine_id and source_modified_at
801
- machine_id: gitCtx.machineId,
802
- source_modified_at: stat.mtime.toISOString()
803
- };
804
- if (gitCtx.isGitRepo) {
805
- file.git_branch = gitCtx.branch;
806
- file.git_default_branch = gitCtx.defaultBranch;
807
- file.is_default_branch = gitCtx.isDefaultBranch;
808
- const gitInfo = await getFileGitInfo(rootPath, relPath);
809
- if (gitInfo) {
810
- file.git_commit_sha = gitInfo.sha;
811
- file.git_commit_timestamp = gitInfo.timestamp;
812
- }
813
- }
814
- yield file;
815
- } catch {
816
- }
817
- }
818
- }
819
- }
820
- for await (const file of walkDir(rootPath)) {
821
- if (file.sizeBytes > largeFileThreshold) {
822
- if (batch.length > 0) {
823
- yield batch.map(({ sizeBytes: sizeBytes2, ...rest }) => rest);
824
- batch = [];
825
- currentBatchBytes = 0;
826
- }
827
- const { sizeBytes, ...fileData } = file;
828
- yield [fileData];
829
- continue;
830
- }
831
- const wouldExceedBytes = currentBatchBytes + file.sizeBytes > maxBatchBytes;
832
- const wouldExceedFiles = batch.length >= maxFilesPerBatch;
833
- if (wouldExceedBytes || wouldExceedFiles) {
834
- if (batch.length > 0) {
835
- yield batch.map(({ sizeBytes, ...rest }) => rest);
836
- batch = [];
837
- currentBatchBytes = 0;
838
- }
839
- }
840
- batch.push(file);
841
- currentBatchBytes += file.sizeBytes;
842
- }
843
- if (batch.length > 0) {
844
- yield batch.map(({ sizeBytes, ...rest }) => rest);
845
- }
846
- }
847
- async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}) {
848
- const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
849
- const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
850
- const maxFilesPerBatch = options.maxFilesPerBatch ?? options.batchSize ?? MAX_FILES_PER_BATCH;
851
- const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
852
- const sinceMs = sinceTimestamp.getTime();
853
- const ig = options.ignoreInstance ?? await loadIgnorePatterns(rootPath);
854
- const gitCtx = await getGitContext(rootPath);
855
- let batch = [];
856
- let currentBatchBytes = 0;
857
- let filesScanned = 0;
858
- let filesChanged = 0;
859
- async function* walkDir(dir, relativePath = "") {
860
- let entries;
861
- try {
862
- entries = await fs2.promises.readdir(dir, { withFileTypes: true });
863
- } catch {
864
- return;
865
- }
866
- entries.sort((a, b) => a.name.localeCompare(b.name));
867
- for (const entry of entries) {
868
- const fullPath = path2.join(dir, entry.name);
869
- const relPath = path2.join(relativePath, entry.name);
870
- if (entry.isDirectory()) {
871
- if (IGNORE_DIRS.has(entry.name)) continue;
872
- if (ig.ignores(relPath + "/")) continue;
873
- yield* walkDir(fullPath, relPath);
874
- } else if (entry.isFile()) {
875
- if (IGNORE_FILES.has(entry.name)) continue;
876
- if (ig.ignores(relPath)) continue;
877
- const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
878
- if (!CODE_EXTENSIONS.has(ext)) continue;
879
- try {
880
- const stat = await fs2.promises.stat(fullPath);
881
- filesScanned++;
882
- if (stat.mtimeMs <= sinceMs) continue;
883
- if (stat.size > maxFileSize) continue;
884
- const content = await fs2.promises.readFile(fullPath, "utf-8");
885
- filesChanged++;
886
- const file = {
887
- path: relPath,
888
- content,
889
- language: detectLanguage(relPath),
890
- sizeBytes: stat.size,
891
- // Always include machine_id and source_modified_at
892
- machine_id: gitCtx.machineId,
893
- source_modified_at: stat.mtime.toISOString()
894
- };
895
- if (gitCtx.isGitRepo) {
896
- file.git_branch = gitCtx.branch;
897
- file.git_default_branch = gitCtx.defaultBranch;
898
- file.is_default_branch = gitCtx.isDefaultBranch;
899
- const gitInfo = await getFileGitInfo(rootPath, relPath);
900
- if (gitInfo) {
901
- file.git_commit_sha = gitInfo.sha;
902
- file.git_commit_timestamp = gitInfo.timestamp;
903
- }
904
- }
905
- yield file;
906
- } catch {
907
- }
908
- }
909
- }
910
- }
911
- for await (const file of walkDir(rootPath)) {
912
- if (file.sizeBytes > largeFileThreshold) {
913
- if (batch.length > 0) {
914
- yield batch.map(({ sizeBytes: sizeBytes2, ...rest }) => rest);
915
- batch = [];
916
- currentBatchBytes = 0;
917
- }
918
- const { sizeBytes, ...fileData } = file;
919
- yield [fileData];
920
- continue;
921
- }
922
- const wouldExceedBytes = currentBatchBytes + file.sizeBytes > maxBatchBytes;
923
- const wouldExceedFiles = batch.length >= maxFilesPerBatch;
924
- if (wouldExceedBytes || wouldExceedFiles) {
925
- if (batch.length > 0) {
926
- yield batch.map(({ sizeBytes, ...rest }) => rest);
927
- batch = [];
928
- currentBatchBytes = 0;
929
- }
930
- }
931
- batch.push(file);
932
- currentBatchBytes += file.sizeBytes;
933
- }
934
- if (batch.length > 0) {
935
- yield batch.map(({ sizeBytes, ...rest }) => rest);
936
- }
937
- console.error(
938
- `[ContextStream] Incremental scan: ${filesChanged} changed files out of ${filesScanned} scanned (since ${sinceTimestamp.toISOString()})`
939
- );
940
- }
941
- async function countIndexableFiles(rootPath, options = {}) {
942
- const maxFiles = options.maxFiles ?? 1;
943
- const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
944
- const ig = options.ignoreInstance ?? await loadIgnorePatterns(rootPath);
945
- let count = 0;
946
- let stopped = false;
947
- async function walkDir(dir, relativePath = "") {
948
- if (count >= maxFiles) {
949
- stopped = true;
950
- return;
951
- }
952
- let entries;
953
- try {
954
- entries = await fs2.promises.readdir(dir, { withFileTypes: true });
955
- } catch {
956
- return;
957
- }
958
- entries.sort((a, b) => a.name.localeCompare(b.name));
959
- for (const entry of entries) {
960
- if (count >= maxFiles) {
961
- stopped = true;
962
- return;
963
- }
964
- const fullPath = path2.join(dir, entry.name);
965
- const relPath = path2.join(relativePath, entry.name);
966
- if (entry.isDirectory()) {
967
- if (IGNORE_DIRS.has(entry.name)) continue;
968
- if (ig.ignores(relPath + "/")) continue;
969
- await walkDir(fullPath, relPath);
970
- } else if (entry.isFile()) {
971
- if (IGNORE_FILES.has(entry.name)) continue;
972
- if (ig.ignores(relPath)) continue;
973
- const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
974
- if (!CODE_EXTENSIONS.has(ext)) continue;
975
- try {
976
- const stat = await fs2.promises.stat(fullPath);
977
- if (stat.size > maxFileSize) continue;
978
- count++;
979
- if (count >= maxFiles) {
980
- stopped = true;
981
- return;
982
- }
983
- } catch {
984
- }
985
- }
986
- }
987
- }
988
- await walkDir(rootPath);
989
- return { count, stopped };
990
- }
991
- function detectLanguage(filePath) {
992
- const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
993
- const langMap = {
994
- rs: "rust",
995
- ts: "typescript",
996
- tsx: "typescript",
997
- js: "javascript",
998
- jsx: "javascript",
999
- py: "python",
1000
- go: "go",
1001
- java: "java",
1002
- kt: "kotlin",
1003
- c: "c",
1004
- h: "c",
1005
- cpp: "cpp",
1006
- hpp: "cpp",
1007
- cs: "csharp",
1008
- rb: "ruby",
1009
- php: "php",
1010
- swift: "swift",
1011
- scala: "scala",
1012
- sql: "sql",
1013
- dart: "dart",
1014
- md: "markdown",
1015
- json: "json",
1016
- yaml: "yaml",
1017
- yml: "yaml",
1018
- toml: "toml",
1019
- html: "html",
1020
- css: "css",
1021
- sh: "shell"
1022
- };
1023
- return langMap[ext] ?? "unknown";
1024
- }
1025
- function sha256Hex(content) {
1026
- return crypto.createHash("sha256").update(content).digest("hex");
1027
- }
1028
- function hashManifestPath(projectId) {
1029
- return path2.join(os.homedir(), ".contextstream", "file-hashes", `${projectId}.json`);
1030
- }
1031
- function readHashManifest(projectId) {
1032
- try {
1033
- const content = fs2.readFileSync(hashManifestPath(projectId), "utf-8");
1034
- const parsed = JSON.parse(content);
1035
- return new Map(Object.entries(parsed));
1036
- } catch {
1037
- return /* @__PURE__ */ new Map();
1038
- }
1039
- }
1040
- function writeHashManifest(projectId, hashes) {
1041
- const filePath = hashManifestPath(projectId);
1042
- try {
1043
- const dir = path2.dirname(filePath);
1044
- fs2.mkdirSync(dir, { recursive: true });
1045
- const obj = Object.fromEntries(hashes);
1046
- fs2.writeFileSync(filePath, JSON.stringify(obj, null, 2));
1047
- } catch {
1048
- }
1049
- }
1050
- function deleteHashManifest(projectId) {
1051
- try {
1052
- fs2.unlinkSync(hashManifestPath(projectId));
1053
- } catch {
1054
- }
1055
- }
1056
- var execAsync, CODE_EXTENSIONS, IGNORE_DIRS, IGNORE_FILES, MAX_FILE_SIZE, MAX_BATCH_BYTES, LARGE_FILE_THRESHOLD, MAX_FILES_PER_BATCH, gitContextCache;
1057
- var init_files = __esm({
1058
- "src/files.ts"() {
1059
- "use strict";
1060
- init_ignore();
1061
- execAsync = promisify(exec);
1062
- CODE_EXTENSIONS = /* @__PURE__ */ new Set([
1063
- // Rust
1064
- "rs",
1065
- // TypeScript/JavaScript
1066
- "ts",
1067
- "tsx",
1068
- "js",
1069
- "jsx",
1070
- "mjs",
1071
- "cjs",
1072
- // Python
1073
- "py",
1074
- "pyi",
1075
- // Go
1076
- "go",
1077
- // Java/Kotlin
1078
- "java",
1079
- "kt",
1080
- "kts",
1081
- // C/C++
1082
- "c",
1083
- "h",
1084
- "cpp",
1085
- "hpp",
1086
- "cc",
1087
- "cxx",
1088
- // C#
1089
- "cs",
1090
- // Ruby
1091
- "rb",
1092
- // PHP
1093
- "php",
1094
- // Swift
1095
- "swift",
1096
- // Scala
1097
- "scala",
1098
- // Shell
1099
- "sh",
1100
- "bash",
1101
- "zsh",
1102
- // Config/Data
1103
- "json",
1104
- "yaml",
1105
- "yml",
1106
- "toml",
1107
- "xml",
1108
- // SQL
1109
- "sql",
1110
- // Markdown/Docs
1111
- "md",
1112
- "markdown",
1113
- "rst",
1114
- "txt",
1115
- // HTML/CSS
1116
- "html",
1117
- "htm",
1118
- "css",
1119
- "scss",
1120
- "sass",
1121
- "less",
1122
- // Other
1123
- "graphql",
1124
- "proto",
1125
- "dockerfile",
1126
- // Dart/Flutter
1127
- "dart"
1128
- ]);
1129
- IGNORE_DIRS = /* @__PURE__ */ new Set([
1130
- "node_modules",
1131
- ".git",
1132
- ".svn",
1133
- ".hg",
1134
- "target",
1135
- "dist",
1136
- "build",
1137
- "out",
1138
- ".next",
1139
- ".nuxt",
1140
- ".svelte-kit",
1141
- ".parcel-cache",
1142
- ".turbo",
1143
- ".gradle",
1144
- ".cache",
1145
- "__pycache__",
1146
- ".pytest_cache",
1147
- ".mypy_cache",
1148
- "venv",
1149
- ".venv",
1150
- "env",
1151
- ".env",
1152
- "vendor",
1153
- "coverage",
1154
- ".coverage",
1155
- "bin",
1156
- "obj",
1157
- ".idea",
1158
- ".vscode",
1159
- ".vs"
1160
- ]);
1161
- IGNORE_FILES = /* @__PURE__ */ new Set([
1162
- ".DS_Store",
1163
- "Thumbs.db",
1164
- ".gitignore",
1165
- ".gitattributes",
1166
- "package-lock.json",
1167
- "yarn.lock",
1168
- "pnpm-lock.yaml",
1169
- "Cargo.lock",
1170
- "poetry.lock",
1171
- "Gemfile.lock",
1172
- "composer.lock"
1173
- ]);
1174
- MAX_FILE_SIZE = 5 * 1024 * 1024;
1175
- MAX_BATCH_BYTES = 1024 * 1024;
1176
- LARGE_FILE_THRESHOLD = 2 * 1024 * 1024;
1177
- MAX_FILES_PER_BATCH = 20;
1178
- gitContextCache = /* @__PURE__ */ new Map();
1179
- }
1180
- });
1181
-
1182
- // src/hooks-config.ts
1183
- var hooks_config_exports = {};
1184
- __export(hooks_config_exports, {
1185
- CLAUDE_ENFORCEMENT_CRITICAL_HOOKS: () => CLAUDE_ENFORCEMENT_CRITICAL_HOOKS,
1186
- CLINE_POSTTOOLUSE_HOOK_SCRIPT: () => CLINE_POSTTOOLUSE_HOOK_SCRIPT,
1187
- CLINE_PRETOOLUSE_HOOK_SCRIPT: () => CLINE_PRETOOLUSE_HOOK_SCRIPT,
1188
- CLINE_USER_PROMPT_HOOK_SCRIPT: () => CLINE_USER_PROMPT_HOOK_SCRIPT,
1189
- CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT: () => CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT,
1190
- CURSOR_ENFORCEMENT_CRITICAL_HOOKS: () => CURSOR_ENFORCEMENT_CRITICAL_HOOKS,
1191
- CURSOR_PRETOOLUSE_HOOK_SCRIPT: () => CURSOR_PRETOOLUSE_HOOK_SCRIPT,
1192
- MEDIA_AWARE_HOOK_SCRIPT: () => MEDIA_AWARE_HOOK_SCRIPT,
1193
- PRECOMPACT_HOOK_SCRIPT: () => PRECOMPACT_HOOK_SCRIPT,
1194
- PRETOOLUSE_HOOK_SCRIPT: () => PRETOOLUSE_HOOK_SCRIPT,
1195
- USER_PROMPT_HOOK_SCRIPT: () => USER_PROMPT_HOOK_SCRIPT,
1196
- buildHooksConfig: () => buildHooksConfig,
1197
- clearProjectIndex: () => clearProjectIndex,
1198
- generateAllHooksDocumentation: () => generateAllHooksDocumentation,
1199
- generateHooksDocumentation: () => generateHooksDocumentation,
1200
- getClaudeSettingsPath: () => getClaudeSettingsPath,
1201
- getClineHooksDir: () => getClineHooksDir,
1202
- getCursorHooksConfigPath: () => getCursorHooksConfigPath,
1203
- getCursorHooksDir: () => getCursorHooksDir,
1204
- getHookCommand: () => getHookCommand,
1205
- getHooksDir: () => getHooksDir,
1206
- getIndexStatusPath: () => getIndexStatusPath,
1207
- getKiloCodeHooksDir: () => getKiloCodeHooksDir,
1208
- getRooCodeHooksDir: () => getRooCodeHooksDir,
1209
- getWindsurfHooksConfigPath: () => getWindsurfHooksConfigPath,
1210
- installAllEditorHooks: () => installAllEditorHooks,
1211
- installClaudeCodeHooks: () => installClaudeCodeHooks,
1212
- installClineHookScripts: () => installClineHookScripts,
1213
- installCursorHookScripts: () => installCursorHookScripts,
1214
- installEditorHooks: () => installEditorHooks,
1215
- installHookScripts: () => installHookScripts,
1216
- installKiloCodeHookScripts: () => installKiloCodeHookScripts,
1217
- installRooCodeHookScripts: () => installRooCodeHookScripts,
1218
- installWindsurfHookScripts: () => installWindsurfHookScripts,
1219
- markProjectIndexed: () => markProjectIndexed,
1220
- mergeHooksIntoSettings: () => mergeHooksIntoSettings,
1221
- readClaudeSettings: () => readClaudeSettings,
1222
- readCursorHooksConfig: () => readCursorHooksConfig,
1223
- readIndexStatus: () => readIndexStatus,
1224
- readWindsurfHooksConfig: () => readWindsurfHooksConfig,
1225
- unmarkProjectIndexed: () => unmarkProjectIndexed,
1226
- writeClaudeSettings: () => writeClaudeSettings,
1227
- writeCursorHooksConfig: () => writeCursorHooksConfig,
1228
- writeIndexStatus: () => writeIndexStatus,
1229
- writeWindsurfHooksConfig: () => writeWindsurfHooksConfig
1230
- });
1231
- import * as fs3 from "node:fs/promises";
1232
- import * as fsSync from "node:fs";
1233
- import * as path3 from "node:path";
1234
- import { homedir as homedir2 } from "node:os";
1235
- import { fileURLToPath } from "node:url";
1236
- function getHookCommand(hookName) {
1237
- const isWindows = process.platform === "win32";
1238
- if (isWindows) {
1239
- const localAppData = process.env.LOCALAPPDATA;
1240
- if (localAppData) {
1241
- const windowsBinaryPath = path3.join(localAppData, "ContextStream", "contextstream-mcp.exe");
1242
- if (fsSync.existsSync(windowsBinaryPath)) {
1243
- return `"${windowsBinaryPath}" hook ${hookName}`;
1244
- }
1245
- }
1246
- } else {
1247
- const unixBinaryPath = "/usr/local/bin/contextstream-mcp";
1248
- if (fsSync.existsSync(unixBinaryPath)) {
1249
- return `${unixBinaryPath} hook ${hookName}`;
1250
- }
1251
- }
1252
- try {
1253
- const __dirname = path3.dirname(fileURLToPath(import.meta.url));
1254
- const indexPath = path3.join(__dirname, "index.js");
1255
- if (fsSync.existsSync(indexPath)) {
1256
- return `node "${indexPath}" hook ${hookName}`;
1257
- }
1258
- } catch {
1259
- }
1260
- return `npx @contextstream/mcp-server hook ${hookName}`;
1261
- }
1262
- function getClaudeSettingsPath(scope, projectPath) {
1263
- if (scope === "user") {
1264
- return path3.join(homedir2(), ".claude", "settings.json");
1265
- }
1266
- if (!projectPath) {
1267
- throw new Error("projectPath required for project scope");
1268
- }
1269
- return path3.join(projectPath, ".claude", "settings.json");
1270
- }
1271
- function getHooksDir() {
1272
- return path3.join(homedir2(), ".claude", "hooks");
1273
- }
1274
- function buildHooksConfig(options) {
1275
- const userPromptHooks = [
1276
- {
1277
- matcher: "*",
1278
- hooks: [
1279
- {
1280
- type: "command",
1281
- command: getHookCommand("user-prompt-submit"),
1282
- timeout: 5
1283
- }
1284
- ]
1285
- }
1286
- ];
1287
- if (options?.includeOnSaveIntent !== false) {
1288
- userPromptHooks.push({
1289
- matcher: "*",
1290
- hooks: [
1291
- {
1292
- type: "command",
1293
- command: getHookCommand("on-save-intent"),
1294
- timeout: 5
1295
- }
1296
- ]
1297
- });
1298
- }
1299
- if (options?.includeMediaAware === true) {
1300
- userPromptHooks.push({
1301
- matcher: "*",
1302
- hooks: [
1303
- {
1304
- type: "command",
1305
- command: getHookCommand("media-aware"),
1306
- timeout: 5
1307
- }
1308
- ]
1309
- });
1310
- }
1311
- const config = {
1312
- PreToolUse: [
1313
- {
1314
- matcher: "*",
1315
- hooks: [
1316
- {
1317
- type: "command",
1318
- command: getHookCommand("pre-tool-use"),
1319
- timeout: 5
1320
- }
1321
- ]
1322
- }
1323
- ],
1324
- UserPromptSubmit: userPromptHooks
1325
- };
1326
- if (options?.includePreCompact !== false) {
1327
- config.PreCompact = [
1328
- {
1329
- matcher: "*",
1330
- hooks: [
1331
- {
1332
- type: "command",
1333
- command: getHookCommand("pre-compact"),
1334
- timeout: 10
1335
- }
1336
- ]
1337
- }
1338
- ];
1339
- config.PostCompact = [
1340
- {
1341
- matcher: "*",
1342
- hooks: [
1343
- {
1344
- type: "command",
1345
- command: getHookCommand("post-compact"),
1346
- timeout: 15
1347
- }
1348
- ]
1349
- }
1350
- ];
1351
- }
1352
- if (options?.includeSessionInit !== false) {
1353
- config.SessionStart = [
1354
- {
1355
- matcher: "startup|resume|compact",
1356
- hooks: [
1357
- {
1358
- type: "command",
1359
- command: getHookCommand("session-start"),
1360
- timeout: 15
1361
- }
1362
- ]
1363
- }
1364
- ];
1365
- config.InstructionsLoaded = [
1366
- {
1367
- matcher: "*",
1368
- hooks: [
1369
- {
1370
- type: "command",
1371
- command: getHookCommand("instructions-loaded"),
1372
- timeout: 10
1373
- }
1374
- ]
1375
- }
1376
- ];
1377
- config.ConfigChange = [
1378
- {
1379
- matcher: "*",
1380
- hooks: [
1381
- {
1382
- type: "command",
1383
- command: getHookCommand("config-change"),
1384
- timeout: 10
1385
- }
1386
- ]
1387
- }
1388
- ];
1389
- config.CwdChanged = [
1390
- {
1391
- matcher: "*",
1392
- hooks: [
1393
- {
1394
- type: "command",
1395
- command: getHookCommand("cwd-changed"),
1396
- timeout: 10
1397
- }
1398
- ]
1399
- }
1400
- ];
1401
- config.FileChanged = [
1402
- {
1403
- matcher: ".*",
1404
- hooks: [
1405
- {
1406
- type: "command",
1407
- command: getHookCommand("file-changed"),
1408
- timeout: 10
1409
- }
1410
- ]
1411
- }
1412
- ];
1413
- config.WorktreeCreate = [
1414
- {
1415
- matcher: "*",
1416
- hooks: [
1417
- {
1418
- type: "command",
1419
- command: getHookCommand("worktree-create"),
1420
- timeout: 15
1421
- }
1422
- ]
1423
- }
1424
- ];
1425
- config.WorktreeRemove = [
1426
- {
1427
- matcher: "*",
1428
- hooks: [
1429
- {
1430
- type: "command",
1431
- command: getHookCommand("worktree-remove"),
1432
- timeout: 15
1433
- }
1434
- ]
1435
- }
1436
- ];
1437
- config.Elicitation = [
1438
- {
1439
- matcher: ".*",
1440
- hooks: [
1441
- {
1442
- type: "command",
1443
- command: getHookCommand("elicitation"),
1444
- timeout: 10
1445
- }
1446
- ]
1447
- }
1448
- ];
1449
- config.ElicitationResult = [
1450
- {
1451
- matcher: ".*",
1452
- hooks: [
1453
- {
1454
- type: "command",
1455
- command: getHookCommand("elicitation-result"),
1456
- timeout: 10
1457
- }
1458
- ]
1459
- }
1460
- ];
1461
- }
1462
- if (options?.includeSessionEnd !== false) {
1463
- config.Stop = [
1464
- {
1465
- matcher: "*",
1466
- hooks: [
1467
- {
1468
- type: "command",
1469
- command: getHookCommand("stop"),
1470
- timeout: 15
1471
- }
1472
- ]
1473
- }
1474
- ];
1475
- config.SessionEnd = [
1476
- {
1477
- matcher: "*",
1478
- hooks: [
1479
- {
1480
- type: "command",
1481
- command: getHookCommand("session-end"),
1482
- timeout: 10
1483
- }
1484
- ]
1485
- }
1486
- ];
1487
- config.StopFailure = [
1488
- {
1489
- matcher: "*",
1490
- hooks: [
1491
- {
1492
- type: "command",
1493
- command: getHookCommand("stop-failure"),
1494
- timeout: 10
1495
- }
1496
- ]
1497
- }
1498
- ];
1499
- }
1500
- const postToolUseHooks = [];
1501
- if (options?.includePostWrite !== false) {
1502
- postToolUseHooks.push({
1503
- matcher: "Edit|Write|NotebookEdit",
1504
- hooks: [
1505
- {
1506
- type: "command",
1507
- command: getHookCommand("post-write"),
1508
- timeout: 10
1509
- }
1510
- ]
1511
- });
1512
- }
1513
- if (options?.includeAutoRules === true) {
1514
- postToolUseHooks.push({
1515
- matcher: "mcp__contextstream__init|mcp__contextstream__context",
1516
- hooks: [
1517
- {
1518
- type: "command",
1519
- command: getHookCommand("auto-rules"),
1520
- timeout: 15
1521
- }
1522
- ]
1523
- });
1524
- }
1525
- if (options?.includeOnBash === true) {
1526
- postToolUseHooks.push({
1527
- matcher: "Bash",
1528
- hooks: [
1529
- {
1530
- type: "command",
1531
- command: getHookCommand("on-bash"),
1532
- timeout: 5
1533
- }
1534
- ]
1535
- });
1536
- }
1537
- if (options?.includeOnTask === true) {
1538
- postToolUseHooks.push({
1539
- matcher: "Task",
1540
- hooks: [
1541
- {
1542
- type: "command",
1543
- command: getHookCommand("on-task"),
1544
- timeout: 5
1545
- }
1546
- ]
1547
- });
1548
- }
1549
- if (options?.includeOnRead === true) {
1550
- postToolUseHooks.push({
1551
- matcher: "Read|Glob|Grep",
1552
- hooks: [
1553
- {
1554
- type: "command",
1555
- command: getHookCommand("on-read"),
1556
- timeout: 5
1557
- }
1558
- ]
1559
- });
1560
- }
1561
- if (options?.includeOnWeb === true) {
1562
- postToolUseHooks.push({
1563
- matcher: "WebFetch|WebSearch",
1564
- hooks: [
1565
- {
1566
- type: "command",
1567
- command: getHookCommand("on-web"),
1568
- timeout: 5
1569
- }
1570
- ]
1571
- });
1572
- }
1573
- if (postToolUseHooks.length > 0) {
1574
- config.PostToolUse = postToolUseHooks;
1575
- }
1576
- config.PostToolUseFailure = [
1577
- {
1578
- matcher: "*",
1579
- hooks: [{ type: "command", command: getHookCommand("post-tool-use-failure"), timeout: 10 }]
1580
- }
1581
- ];
1582
- config.SubagentStart = [
1583
- {
1584
- matcher: "Explore|Plan|general-purpose|custom",
1585
- hooks: [{ type: "command", command: getHookCommand("subagent-start"), timeout: 10 }]
1586
- }
1587
- ];
1588
- config.SubagentStop = [
1589
- {
1590
- matcher: "Plan",
1591
- hooks: [{ type: "command", command: getHookCommand("subagent-stop"), timeout: 15 }]
1592
- }
1593
- ];
1594
- config.TaskCreated = [
1595
- {
1596
- matcher: "*",
1597
- hooks: [{ type: "command", command: getHookCommand("task-created"), timeout: 10 }]
1598
- }
1599
- ];
1600
- config.TaskCompleted = [
1601
- {
1602
- matcher: "*",
1603
- hooks: [{ type: "command", command: getHookCommand("task-completed"), timeout: 10 }]
1604
- }
1605
- ];
1606
- config.TeammateIdle = [
1607
- {
1608
- matcher: "*",
1609
- hooks: [{ type: "command", command: getHookCommand("teammate-idle"), timeout: 10 }]
1610
- }
1611
- ];
1612
- config.Notification = [
1613
- {
1614
- matcher: "*",
1615
- hooks: [{ type: "command", command: getHookCommand("notification"), timeout: 10 }]
1616
- }
1617
- ];
1618
- config.PermissionRequest = [
1619
- {
1620
- matcher: "*",
1621
- hooks: [{ type: "command", command: getHookCommand("permission-request"), timeout: 10 }]
1622
- }
1623
- ];
1624
- return config;
1625
- }
1626
- async function installHookScripts(options) {
1627
- const hooksDir = getHooksDir();
1628
- await fs3.mkdir(hooksDir, { recursive: true });
1629
- const result = {
1630
- preToolUse: getHookCommand("pre-tool-use"),
1631
- userPrompt: getHookCommand("user-prompt-submit")
1632
- };
1633
- if (options?.includePreCompact !== false) {
1634
- result.preCompact = getHookCommand("pre-compact");
1635
- }
1636
- if (options?.includeMediaAware !== false) {
1637
- result.mediaAware = getHookCommand("media-aware");
1638
- }
1639
- if (options?.includeAutoRules !== false) {
1640
- result.autoRules = getHookCommand("auto-rules");
1641
- }
1642
- return result;
1643
- }
1644
- async function readClaudeSettings(scope, projectPath) {
1645
- const settingsPath = getClaudeSettingsPath(scope, projectPath);
1646
- try {
1647
- const content = await fs3.readFile(settingsPath, "utf-8");
1648
- return JSON.parse(content);
1649
- } catch {
1650
- return {};
1651
- }
1652
- }
1653
- async function writeClaudeSettings(settings, scope, projectPath) {
1654
- const settingsPath = getClaudeSettingsPath(scope, projectPath);
1655
- const dir = path3.dirname(settingsPath);
1656
- await fs3.mkdir(dir, { recursive: true });
1657
- await fs3.writeFile(settingsPath, JSON.stringify(settings, null, 2));
1658
- }
1659
- function mergeHooksIntoSettings(existingSettings, newHooks) {
1660
- const settings = { ...existingSettings };
1661
- const existingHooks = settings.hooks || {};
1662
- for (const [hookType, matchers] of Object.entries(newHooks || {})) {
1663
- if (!matchers) continue;
1664
- const existing = existingHooks?.[hookType] || [];
1665
- const filtered = existing.filter((m) => {
1666
- return !m.hooks?.some((h) => h.command?.includes("contextstream"));
1667
- });
1668
- existingHooks[hookType] = [...filtered, ...matchers];
1669
- }
1670
- settings.hooks = existingHooks;
1671
- return settings;
1672
- }
1673
- async function installClaudeCodeHooks(options) {
1674
- const result = { scripts: [], settings: [] };
1675
- result.scripts.push(
1676
- getHookCommand("pre-tool-use"),
1677
- getHookCommand("user-prompt-submit"),
1678
- getHookCommand("on-save-intent"),
1679
- getHookCommand("session-start"),
1680
- getHookCommand("instructions-loaded"),
1681
- getHookCommand("config-change"),
1682
- getHookCommand("cwd-changed"),
1683
- getHookCommand("file-changed"),
1684
- getHookCommand("worktree-create"),
1685
- getHookCommand("worktree-remove"),
1686
- getHookCommand("elicitation"),
1687
- getHookCommand("elicitation-result"),
1688
- getHookCommand("stop"),
1689
- getHookCommand("stop-failure"),
1690
- getHookCommand("session-end"),
1691
- getHookCommand("post-tool-use-failure"),
1692
- getHookCommand("subagent-start"),
1693
- getHookCommand("subagent-stop"),
1694
- getHookCommand("task-created"),
1695
- getHookCommand("task-completed"),
1696
- getHookCommand("teammate-idle"),
1697
- getHookCommand("notification"),
1698
- getHookCommand("permission-request")
1699
- );
1700
- if (options.includePreCompact !== false) {
1701
- result.scripts.push(getHookCommand("pre-compact"));
1702
- result.scripts.push(getHookCommand("post-compact"));
1703
- }
1704
- if (options.includeMediaAware === true) {
1705
- result.scripts.push(getHookCommand("media-aware"));
1706
- }
1707
- if (options.includePostWrite !== false) {
1708
- result.scripts.push(getHookCommand("post-write"));
1709
- }
1710
- if (options.includeAutoRules === true) {
1711
- result.scripts.push(getHookCommand("auto-rules"));
1712
- }
1713
- const hooksConfig = buildHooksConfig({
1714
- includePreCompact: options.includePreCompact,
1715
- includeMediaAware: options.includeMediaAware,
1716
- includePostWrite: options.includePostWrite,
1717
- includeAutoRules: options.includeAutoRules
1718
- });
1719
- if (options.scope === "user" || options.scope === "both") {
1720
- const settingsPath = getClaudeSettingsPath("user");
1721
- if (!options.dryRun) {
1722
- const existing = await readClaudeSettings("user");
1723
- const merged = mergeHooksIntoSettings(existing, hooksConfig);
1724
- await writeClaudeSettings(merged, "user");
1725
- }
1726
- result.settings.push(settingsPath);
1727
- }
1728
- if ((options.scope === "project" || options.scope === "both") && options.projectPath) {
1729
- const settingsPath = getClaudeSettingsPath("project", options.projectPath);
1730
- if (!options.dryRun) {
1731
- const existing = await readClaudeSettings("project", options.projectPath);
1732
- const merged = mergeHooksIntoSettings(existing, hooksConfig);
1733
- await writeClaudeSettings(merged, "project", options.projectPath);
1734
- }
1735
- result.settings.push(settingsPath);
1736
- }
1737
- return result;
1738
- }
1739
- function generateHooksDocumentation() {
1740
- return `
1741
- ## Claude Code Hooks (ContextStream)
1742
-
1743
- ContextStream installs hooks to enforce ContextStream-first behavior.
1744
- All hooks run via Node.js - no Python dependency required.
1745
-
1746
- ### PreToolUse Hook
1747
- - **Command:** \`npx @contextstream/mcp-server hook pre-tool-use\`
1748
- - **Purpose:** Blocks Glob/Grep/Search/Explore/Task/EnterPlanMode and redirects to ContextStream
1749
- - **Blocked tools:** Glob, Grep, Search, Explore, Task(Explore), Task(Plan), EnterPlanMode
1750
- - **Disable:** Set \`CONTEXTSTREAM_HOOK_ENABLED=false\` environment variable
1751
-
1752
- ### UserPromptSubmit Hook
1753
- - **Command:** \`npx @contextstream/mcp-server hook user-prompt-submit\`
1754
- - **Purpose:** Injects a reminder about ContextStream rules on every message
1755
- - **Disable:** Set \`CONTEXTSTREAM_REMINDER_ENABLED=false\` environment variable
1756
-
1757
- ### Media-Aware Hook
1758
- - **Command:** \`npx @contextstream/mcp-server hook media-aware\`
1759
- - **Purpose:** Detects media-related prompts and injects media tool guidance
1760
- - **Triggers:** Patterns like video, clips, Remotion, image, audio, creative assets
1761
- - **Disable:** Set \`CONTEXTSTREAM_MEDIA_HOOK_ENABLED=false\` environment variable
1762
-
1763
- When Media-Aware hook detects media patterns, it injects context about:
1764
- - How to search indexed media assets
1765
- - How to get clips for Remotion (with frame-based props)
1766
- - How to index new media files
1767
-
1768
- ### PreCompact Hook
1769
- - **Command:** \`npx @contextstream/mcp-server hook pre-compact\`
1770
- - **Purpose:** Saves conversation state before context compaction
1771
- - **Triggers:** Both manual (/compact) and automatic compaction
1772
- - **Installed:** By default (disable with \`CONTEXTSTREAM_HOOK_ENABLED=false\`)
1773
-
1774
- When PreCompact runs, it:
1775
- 1. Parses the transcript for active files and tool calls
1776
- 2. Saves a session_snapshot to ContextStream API
1777
- 3. Injects context about using \`session_init(is_post_compact=true)\` after compaction
1778
-
1779
- ### PostToolUse Hook (Real-time Indexing)
1780
- - **Command:** \`npx @contextstream/mcp-server hook post-write\`
1781
- - **Purpose:** Indexes files immediately after Edit/Write/NotebookEdit operations
1782
- - **Matcher:** Edit|Write|NotebookEdit
1783
- - **Disable:** Set \`CONTEXTSTREAM_POSTWRITE_ENABLED=false\` environment variable
1784
-
1785
- ### Why Hooks?
1786
- Claude Code has strong built-in behaviors to use its default tools (Grep, Glob, Read)
1787
- and its built-in plan mode. CLAUDE.md instructions decay over long conversations.
1788
- Hooks provide:
1789
- 1. **Physical enforcement** - Blocked tools can't be used
1790
- 2. **Continuous reminders** - Rules stay in recent context
1791
- 3. **Better UX** - Faster searches via indexed ContextStream
1792
- 4. **Persistent plans** - ContextStream plans survive across sessions
1793
- 5. **Compaction awareness** - Save state before context is compacted
1794
- 6. **Real-time indexing** - Files indexed immediately after writes
1795
-
1796
- ### Manual Configuration
1797
- If you prefer to configure manually, add to \`~/.claude/settings.json\`:
1798
- \`\`\`json
1799
- {
1800
- "hooks": {
1801
- "PreToolUse": [{
1802
- "matcher": "Glob|Grep|Search|Explore|Task|EnterPlanMode",
1803
- "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook pre-tool-use"}]
1804
- }],
1805
- "UserPromptSubmit": [
1806
- {
1807
- "matcher": "*",
1808
- "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook user-prompt-submit"}]
1809
- },
1810
- {
1811
- "matcher": "*",
1812
- "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook media-aware"}]
1813
- }
1814
- ],
1815
- "PreCompact": [{
1816
- "matcher": "*",
1817
- "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook pre-compact", "timeout": 10}]
1818
- }],
1819
- "PostToolUse": [{
1820
- "matcher": "Edit|Write|NotebookEdit",
1821
- "hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook post-write", "timeout": 10}]
1822
- }]
1823
- }
1824
- }
1825
- \`\`\`
1826
- `.trim();
1827
- }
1828
- function getIndexStatusPath() {
1829
- return path3.join(homedir2(), ".contextstream", "indexed-projects.json");
1830
- }
1831
- async function readIndexStatus() {
1832
- const statusPath = getIndexStatusPath();
1833
- try {
1834
- const content = await fs3.readFile(statusPath, "utf-8");
1835
- return JSON.parse(content);
1836
- } catch {
1837
- return { version: 1, projects: {} };
1838
- }
1839
- }
1840
- async function writeIndexStatus(status) {
1841
- const statusPath = getIndexStatusPath();
1842
- const dir = path3.dirname(statusPath);
1843
- await fs3.mkdir(dir, { recursive: true });
1844
- await fs3.writeFile(statusPath, JSON.stringify(status, null, 2));
1845
- }
1846
- async function markProjectIndexed(projectPath, options) {
1847
- const status = await readIndexStatus();
1848
- const resolvedPath = path3.resolve(projectPath);
1849
- const previous = status.projects[resolvedPath];
1850
- status.projects[resolvedPath] = {
1851
- indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
1852
- project_id: options?.project_id ?? previous?.project_id,
1853
- project_name: options?.project_name ?? previous?.project_name,
1854
- git_head: options?.git_head ?? previous?.git_head,
1855
- git_remote: options?.git_remote ?? previous?.git_remote
1856
- };
1857
- await writeIndexStatus(status);
1858
- }
1859
- async function unmarkProjectIndexed(projectPath) {
1860
- const status = await readIndexStatus();
1861
- const resolvedPath = path3.resolve(projectPath);
1862
- delete status.projects[resolvedPath];
1863
- await writeIndexStatus(status);
1864
- }
1865
- async function clearProjectIndex(projectPath, projectId) {
1866
- await unmarkProjectIndexed(projectPath);
1867
- if (projectId) {
1868
- const { deleteHashManifest: deleteHashManifest2 } = await Promise.resolve().then(() => (init_files(), files_exports));
1869
- deleteHashManifest2(projectId);
1870
- }
1871
- }
1872
- function getClineHooksDir(scope, projectPath) {
1873
- if (scope === "global") {
1874
- return path3.join(homedir2(), "Documents", "Cline", "Rules", "Hooks");
1875
- }
1876
- if (!projectPath) {
1877
- throw new Error("projectPath required for project scope");
1878
- }
1879
- return path3.join(projectPath, ".clinerules", "hooks");
1880
- }
1881
- function getHookWrapperScript(hookName) {
1882
- const isWindows = process.platform === "win32";
1883
- const command = getHookCommand(hookName);
1884
- if (isWindows) {
1885
- return {
1886
- content: `@echo off\r
1887
- ${command}\r
1888
- `,
1889
- extension: ".cmd"
1890
- };
1891
- } else {
1892
- return {
1893
- content: `#!/bin/bash
1894
- # ContextStream ${hookName} Hook Wrapper for Cline/Roo/Kilo Code
1895
- exec ${command}
1896
- `,
1897
- extension: ""
1898
- };
1899
- }
1900
- }
1901
- async function installClineHookScripts(options) {
1902
- const hooksDir = getClineHooksDir(options.scope, options.projectPath);
1903
- await fs3.mkdir(hooksDir, { recursive: true });
1904
- const preToolUseWrapper = getHookWrapperScript("pre-tool-use");
1905
- const userPromptWrapper = getHookWrapperScript("user-prompt-submit");
1906
- const postWriteWrapper = getHookWrapperScript("post-write");
1907
- const preToolUsePath = path3.join(hooksDir, `PreToolUse${preToolUseWrapper.extension}`);
1908
- const userPromptPath = path3.join(hooksDir, `UserPromptSubmit${userPromptWrapper.extension}`);
1909
- const postToolUsePath = path3.join(hooksDir, `PostToolUse${postWriteWrapper.extension}`);
1910
- await fs3.writeFile(preToolUsePath, preToolUseWrapper.content, { mode: 493 });
1911
- await fs3.writeFile(userPromptPath, userPromptWrapper.content, { mode: 493 });
1912
- const result = {
1913
- preToolUse: preToolUsePath,
1914
- userPromptSubmit: userPromptPath
1915
- };
1916
- if (options.includePostWrite !== false) {
1917
- await fs3.writeFile(postToolUsePath, postWriteWrapper.content, { mode: 493 });
1918
- result.postToolUse = postToolUsePath;
1919
- }
1920
- return result;
1921
- }
1922
- function getRooCodeHooksDir(scope, projectPath) {
1923
- if (scope === "global") {
1924
- return path3.join(homedir2(), ".roo", "hooks");
1925
- }
1926
- if (!projectPath) {
1927
- throw new Error("projectPath required for project scope");
1928
- }
1929
- return path3.join(projectPath, ".roo", "hooks");
1930
- }
1931
- async function installRooCodeHookScripts(options) {
1932
- const hooksDir = getRooCodeHooksDir(options.scope, options.projectPath);
1933
- await fs3.mkdir(hooksDir, { recursive: true });
1934
- const preToolUseWrapper = getHookWrapperScript("pre-tool-use");
1935
- const userPromptWrapper = getHookWrapperScript("user-prompt-submit");
1936
- const postWriteWrapper = getHookWrapperScript("post-write");
1937
- const preToolUsePath = path3.join(hooksDir, `PreToolUse${preToolUseWrapper.extension}`);
1938
- const userPromptPath = path3.join(hooksDir, `UserPromptSubmit${userPromptWrapper.extension}`);
1939
- const postToolUsePath = path3.join(hooksDir, `PostToolUse${postWriteWrapper.extension}`);
1940
- await fs3.writeFile(preToolUsePath, preToolUseWrapper.content, { mode: 493 });
1941
- await fs3.writeFile(userPromptPath, userPromptWrapper.content, { mode: 493 });
1942
- const result = {
1943
- preToolUse: preToolUsePath,
1944
- userPromptSubmit: userPromptPath
1945
- };
1946
- if (options.includePostWrite !== false) {
1947
- await fs3.writeFile(postToolUsePath, postWriteWrapper.content, { mode: 493 });
1948
- result.postToolUse = postToolUsePath;
1949
- }
1950
- return result;
1951
- }
1952
- function getKiloCodeHooksDir(scope, projectPath) {
1953
- if (scope === "global") {
1954
- return path3.join(homedir2(), ".kilocode", "hooks");
1955
- }
1956
- if (!projectPath) {
1957
- throw new Error("projectPath required for project scope");
1958
- }
1959
- return path3.join(projectPath, ".kilocode", "hooks");
1960
- }
1961
- async function installKiloCodeHookScripts(options) {
1962
- const hooksDir = getKiloCodeHooksDir(options.scope, options.projectPath);
1963
- await fs3.mkdir(hooksDir, { recursive: true });
1964
- const preToolUseWrapper = getHookWrapperScript("pre-tool-use");
1965
- const userPromptWrapper = getHookWrapperScript("user-prompt-submit");
1966
- const postWriteWrapper = getHookWrapperScript("post-write");
1967
- const preToolUsePath = path3.join(hooksDir, `PreToolUse${preToolUseWrapper.extension}`);
1968
- const userPromptPath = path3.join(hooksDir, `UserPromptSubmit${userPromptWrapper.extension}`);
1969
- const postToolUsePath = path3.join(hooksDir, `PostToolUse${postWriteWrapper.extension}`);
1970
- await fs3.writeFile(preToolUsePath, preToolUseWrapper.content, { mode: 493 });
1971
- await fs3.writeFile(userPromptPath, userPromptWrapper.content, { mode: 493 });
1972
- const result = {
1973
- preToolUse: preToolUsePath,
1974
- userPromptSubmit: userPromptPath
1975
- };
1976
- if (options.includePostWrite !== false) {
1977
- await fs3.writeFile(postToolUsePath, postWriteWrapper.content, { mode: 493 });
1978
- result.postToolUse = postToolUsePath;
1979
- }
1980
- return result;
1981
- }
1982
- function getCursorHooksConfigPath(scope, projectPath) {
1983
- if (scope === "global") {
1984
- return path3.join(homedir2(), ".cursor", "hooks.json");
1985
- }
1986
- if (!projectPath) {
1987
- throw new Error("projectPath required for project scope");
1988
- }
1989
- return path3.join(projectPath, ".cursor", "hooks.json");
1990
- }
1991
- function getCursorHooksDir(scope, projectPath) {
1992
- if (scope === "global") {
1993
- return path3.join(homedir2(), ".cursor", "hooks");
1994
- }
1995
- if (!projectPath) {
1996
- throw new Error("projectPath required for project scope");
1997
- }
1998
- return path3.join(projectPath, ".cursor", "hooks");
1999
- }
2000
- function getWindsurfHooksConfigPath(scope, projectPath) {
2001
- if (scope === "global") {
2002
- return path3.join(homedir2(), ".codeium", "windsurf", "hooks.json");
2003
- }
2004
- if (!projectPath) {
2005
- throw new Error("projectPath required for project scope");
2006
- }
2007
- return path3.join(projectPath, ".windsurf", "hooks.json");
2008
- }
2009
- async function readCursorHooksConfig(scope, projectPath) {
2010
- const configPath = getCursorHooksConfigPath(scope, projectPath);
2011
- try {
2012
- const content = await fs3.readFile(configPath, "utf-8");
2013
- return JSON.parse(content);
2014
- } catch {
2015
- return { version: 1, hooks: {} };
2016
- }
2017
- }
2018
- async function writeCursorHooksConfig(config, scope, projectPath) {
2019
- const configPath = getCursorHooksConfigPath(scope, projectPath);
2020
- const dir = path3.dirname(configPath);
2021
- await fs3.mkdir(dir, { recursive: true });
2022
- await fs3.writeFile(configPath, JSON.stringify(config, null, 2));
2023
- }
2024
- async function installCursorHookScripts(options) {
2025
- const hooksDir = getCursorHooksDir(options.scope, options.projectPath);
2026
- await fs3.mkdir(hooksDir, { recursive: true });
2027
- const existingConfig = await readCursorHooksConfig(options.scope, options.projectPath);
2028
- const filterContextStreamHooks = (hooks) => {
2029
- if (!hooks) return [];
2030
- return hooks.filter((h) => {
2031
- const hook = h;
2032
- return !hook.command?.includes("contextstream");
2033
- });
2034
- };
2035
- const cleanedHooks = {};
2036
- for (const [eventName, entries] of Object.entries(existingConfig.hooks || {})) {
2037
- cleanedHooks[eventName] = filterContextStreamHooks(entries);
2038
- }
2039
- const cursorHookCommand = (name) => `${getHookCommand(name)} --editor=cursor`;
2040
- const preToolUseCommand = cursorHookCommand("pre-tool-use");
2041
- const userPromptCommand = cursorHookCommand("user-prompt-submit");
2042
- const saveIntentCommand = cursorHookCommand("on-save-intent");
2043
- const postToolUseCommand = cursorHookCommand("post-tool-use");
2044
- const postToolUseFailureCommand = cursorHookCommand("post-tool-use-failure");
2045
- const preCompactCommand = cursorHookCommand("pre-compact");
2046
- const sessionStartCommand = cursorHookCommand("session-start");
2047
- const sessionEndCommand = cursorHookCommand("session-end");
2048
- const stopCommand = cursorHookCommand("stop");
2049
- const notificationCommand = cursorHookCommand("notification");
2050
- const permissionRequestCommand = cursorHookCommand("permission-request");
2051
- const subagentStartCommand = cursorHookCommand("subagent-start");
2052
- const subagentStopCommand = cursorHookCommand("subagent-stop");
2053
- const taskCompletedCommand = cursorHookCommand("task-completed");
2054
- const teammateIdleCommand = cursorHookCommand("teammate-idle");
2055
- const config = {
2056
- version: 1,
2057
- hooks: {
2058
- ...cleanedHooks,
2059
- preToolUse: [
2060
- ...cleanedHooks.preToolUse || [],
2061
- {
2062
- command: preToolUseCommand,
2063
- type: "command",
2064
- timeout: 5
2065
- }
2066
- ],
2067
- beforeSubmitPrompt: [
2068
- ...cleanedHooks.beforeSubmitPrompt || [],
2069
- {
2070
- command: userPromptCommand,
2071
- type: "command",
2072
- timeout: 5
2073
- },
2074
- {
2075
- command: saveIntentCommand,
2076
- type: "command",
2077
- timeout: 5
2078
- }
2079
- ],
2080
- beforeMCPExecution: [
2081
- ...cleanedHooks.beforeMCPExecution || [],
2082
- { command: preToolUseCommand, type: "command", timeout: 5 }
2083
- ],
2084
- afterMCPExecution: [
2085
- ...cleanedHooks.afterMCPExecution || [],
2086
- { command: postToolUseCommand, type: "command", timeout: 10 }
2087
- ],
2088
- beforeShellExecution: [
2089
- ...cleanedHooks.beforeShellExecution || [],
2090
- { command: preToolUseCommand, type: "command", timeout: 5 }
2091
- ],
2092
- afterShellExecution: [
2093
- ...cleanedHooks.afterShellExecution || [],
2094
- { command: postToolUseFailureCommand, type: "command", timeout: 10 }
2095
- ],
2096
- beforeReadFile: [
2097
- ...cleanedHooks.beforeReadFile || [],
2098
- { command: preToolUseCommand, type: "command", timeout: 5 }
2099
- ],
2100
- afterFileEdit: [
2101
- ...cleanedHooks.afterFileEdit || [],
2102
- { command: postToolUseCommand, type: "command", timeout: 10 }
2103
- ],
2104
- preCompact: [
2105
- ...cleanedHooks.preCompact || [],
2106
- { command: preCompactCommand, type: "command", timeout: 15 }
2107
- ],
2108
- sessionStart: [
2109
- ...cleanedHooks.sessionStart || [],
2110
- { command: sessionStartCommand, type: "command", timeout: 15 }
2111
- ],
2112
- sessionEnd: [
2113
- ...cleanedHooks.sessionEnd || [],
2114
- { command: sessionEndCommand, type: "command", timeout: 10 }
2115
- ],
2116
- stop: [
2117
- ...cleanedHooks.stop || [],
2118
- { command: stopCommand, type: "command", timeout: 15 }
2119
- ],
2120
- subagentStart: [
2121
- ...cleanedHooks.subagentStart || [],
2122
- { command: subagentStartCommand, type: "command", timeout: 10 }
2123
- ],
2124
- subagentStop: [
2125
- ...cleanedHooks.subagentStop || [],
2126
- { command: subagentStopCommand, type: "command", timeout: 10 }
2127
- ],
2128
- taskCompleted: [
2129
- ...cleanedHooks.taskCompleted || [],
2130
- { command: taskCompletedCommand, type: "command", timeout: 10 }
2131
- ],
2132
- teammateIdle: [
2133
- ...cleanedHooks.teammateIdle || [],
2134
- { command: teammateIdleCommand, type: "command", timeout: 10 }
2135
- ],
2136
- notification: [
2137
- ...cleanedHooks.notification || [],
2138
- { command: notificationCommand, type: "command", timeout: 10 }
2139
- ],
2140
- permissionRequest: [
2141
- ...cleanedHooks.permissionRequest || [],
2142
- { command: permissionRequestCommand, type: "command", timeout: 10 }
2143
- ]
2144
- }
2145
- };
2146
- await writeCursorHooksConfig(config, options.scope, options.projectPath);
2147
- const configPath = getCursorHooksConfigPath(options.scope, options.projectPath);
2148
- return {
2149
- preToolUse: preToolUseCommand,
2150
- beforeSubmitPrompt: `${userPromptCommand}, ${saveIntentCommand}`,
2151
- config: configPath
2152
- };
2153
- }
2154
- async function readWindsurfHooksConfig(scope, projectPath) {
2155
- const configPath = getWindsurfHooksConfigPath(scope, projectPath);
2156
- try {
2157
- const content = await fs3.readFile(configPath, "utf-8");
2158
- return JSON.parse(content);
2159
- } catch {
2160
- return { hooks: {} };
2161
- }
2162
- }
2163
- async function writeWindsurfHooksConfig(config, scope, projectPath) {
2164
- const configPath = getWindsurfHooksConfigPath(scope, projectPath);
2165
- const dir = path3.dirname(configPath);
2166
- await fs3.mkdir(dir, { recursive: true });
2167
- await fs3.writeFile(configPath, JSON.stringify(config, null, 2));
2168
- }
2169
- async function installWindsurfHookScripts(options) {
2170
- const existingConfig = await readWindsurfHooksConfig(options.scope, options.projectPath);
2171
- const filterContextStreamHooks = (hooks) => {
2172
- if (!hooks) return [];
2173
- return hooks.filter((h) => {
2174
- const hook = h;
2175
- return !hook.command?.toLowerCase().includes("contextstream");
2176
- });
2177
- };
2178
- const cleanedHooks = {};
2179
- for (const [eventName, entries] of Object.entries(existingConfig.hooks || {})) {
2180
- cleanedHooks[eventName] = filterContextStreamHooks(entries);
2181
- }
2182
- const preToolUseCommand = getHookCommand("pre-tool-use");
2183
- const userPromptCommand = getHookCommand("user-prompt-submit");
2184
- const postToolUseCommand = getHookCommand("post-tool-use");
2185
- const sessionEndCommand = getHookCommand("session-end");
2186
- const config = {
2187
- hooks: {
2188
- ...cleanedHooks,
2189
- pre_mcp_tool_use: [
2190
- ...cleanedHooks.pre_mcp_tool_use || [],
2191
- { command: preToolUseCommand, show_output: true }
2192
- ],
2193
- pre_user_prompt: [
2194
- ...cleanedHooks.pre_user_prompt || [],
2195
- { command: userPromptCommand }
2196
- ],
2197
- pre_read_code: [
2198
- ...cleanedHooks.pre_read_code || [],
2199
- { command: preToolUseCommand, show_output: true }
2200
- ],
2201
- pre_write_code: [
2202
- ...cleanedHooks.pre_write_code || [],
2203
- { command: preToolUseCommand, show_output: true }
2204
- ],
2205
- pre_run_command: [
2206
- ...cleanedHooks.pre_run_command || [],
2207
- { command: preToolUseCommand, show_output: true }
2208
- ],
2209
- post_write_code: [
2210
- ...cleanedHooks.post_write_code || [],
2211
- { command: postToolUseCommand, show_output: false }
2212
- ],
2213
- post_mcp_tool_use: [
2214
- ...cleanedHooks.post_mcp_tool_use || [],
2215
- { command: postToolUseCommand, show_output: false }
2216
- ],
2217
- post_cascade_response_with_transcript: [
2218
- ...cleanedHooks.post_cascade_response_with_transcript || [],
2219
- { command: sessionEndCommand }
2220
- ]
2221
- }
2222
- };
2223
- await writeWindsurfHooksConfig(config, options.scope, options.projectPath);
2224
- const configPath = getWindsurfHooksConfigPath(options.scope, options.projectPath);
2225
- return {
2226
- preMcpToolUse: preToolUseCommand,
2227
- preUserPrompt: userPromptCommand,
2228
- config: configPath
2229
- };
2230
- }
2231
- async function installEditorHooks(options) {
2232
- const { editor, scope, projectPath, includePreCompact, includePostWrite } = options;
2233
- switch (editor) {
2234
- case "claude": {
2235
- if (scope === "project" && !projectPath) {
2236
- throw new Error("projectPath required for project scope");
2237
- }
2238
- const scripts = await installHookScripts({ includePreCompact });
2239
- const hooksConfig = buildHooksConfig({ includePreCompact, includePostWrite });
2240
- const settingsScope = scope === "global" ? "user" : "project";
2241
- const existing = await readClaudeSettings(settingsScope, projectPath);
2242
- const merged = mergeHooksIntoSettings(existing, hooksConfig);
2243
- await writeClaudeSettings(merged, settingsScope, projectPath);
2244
- const installed = [scripts.preToolUse, scripts.userPrompt];
2245
- if (scripts.preCompact) installed.push(scripts.preCompact);
2246
- return {
2247
- editor: "claude",
2248
- installed,
2249
- hooksDir: getHooksDir()
2250
- };
2251
- }
2252
- case "cline": {
2253
- const scripts = await installClineHookScripts({ scope, projectPath, includePostWrite });
2254
- const installed = [scripts.preToolUse, scripts.userPromptSubmit];
2255
- if (scripts.postToolUse) installed.push(scripts.postToolUse);
2256
- return {
2257
- editor: "cline",
2258
- installed,
2259
- hooksDir: getClineHooksDir(scope, projectPath)
2260
- };
2261
- }
2262
- case "roo": {
2263
- const scripts = await installRooCodeHookScripts({ scope, projectPath, includePostWrite });
2264
- const installed = [scripts.preToolUse, scripts.userPromptSubmit];
2265
- if (scripts.postToolUse) installed.push(scripts.postToolUse);
2266
- return {
2267
- editor: "roo",
2268
- installed,
2269
- hooksDir: getRooCodeHooksDir(scope, projectPath)
2270
- };
2271
- }
2272
- case "kilo": {
2273
- const scripts = await installKiloCodeHookScripts({ scope, projectPath, includePostWrite });
2274
- const installed = [scripts.preToolUse, scripts.userPromptSubmit];
2275
- if (scripts.postToolUse) installed.push(scripts.postToolUse);
2276
- return {
2277
- editor: "kilo",
2278
- installed,
2279
- hooksDir: getKiloCodeHooksDir(scope, projectPath)
2280
- };
2281
- }
2282
- case "cursor": {
2283
- const scripts = await installCursorHookScripts({ scope, projectPath });
2284
- return {
2285
- editor: "cursor",
2286
- installed: [scripts.preToolUse, scripts.beforeSubmitPrompt],
2287
- hooksDir: getCursorHooksDir(scope, projectPath)
2288
- };
2289
- }
2290
- case "windsurf": {
2291
- const scripts = await installWindsurfHookScripts({ scope, projectPath });
2292
- return {
2293
- editor: "windsurf",
2294
- installed: [scripts.preMcpToolUse, scripts.preUserPrompt, scripts.config],
2295
- hooksDir: path3.dirname(scripts.config)
2296
- };
2297
- }
2298
- default:
2299
- throw new Error(`Unsupported editor: ${editor}`);
2300
- }
2301
- }
2302
- async function installAllEditorHooks(options) {
2303
- const editors = options.editors || ["claude", "cline", "roo", "kilo", "cursor", "windsurf"];
2304
- const results = [];
2305
- for (const editor of editors) {
2306
- try {
2307
- const result = await installEditorHooks({
2308
- editor,
2309
- scope: options.scope,
2310
- projectPath: options.projectPath,
2311
- includePreCompact: options.includePreCompact,
2312
- includePostWrite: options.includePostWrite
2313
- });
2314
- results.push(result);
2315
- } catch (error) {
2316
- console.error(`Failed to install hooks for ${editor}:`, error);
2317
- }
2318
- }
2319
- return results;
2320
- }
2321
- function generateAllHooksDocumentation() {
2322
- return `
2323
- ## Editor Hooks Support (ContextStream)
2324
-
2325
- ContextStream can install hooks for multiple AI code editors to enforce ContextStream-first behavior.
2326
-
2327
- ### Supported Editors
2328
-
2329
- | Editor | Hooks Location | Hook Types |
2330
- |--------|---------------|------------|
2331
- | **Claude Code** | \`~/.claude/hooks/\` | PreToolUse, UserPromptSubmit, SessionStart, Pre/PostCompact, PostToolUse |
2332
- | **Cursor** | \`~/.cursor/hooks/\` | preToolUse, beforeSubmitPrompt, before/after MCP+Shell, beforeReadFile, afterFileEdit |
2333
- | **Windsurf** | \`~/.codeium/windsurf/hooks.json\` | pre_mcp_tool_use, pre_user_prompt, pre/post code + command hooks |
2334
- | **Cline** | \`~/Documents/Cline/Rules/Hooks/\` | PreToolUse, UserPromptSubmit |
2335
- | **Roo Code** | \`~/.roo/hooks/\` | PreToolUse, UserPromptSubmit |
2336
- | **Kilo Code** | \`~/.kilocode/hooks/\` | PreToolUse, UserPromptSubmit |
2337
-
2338
- ### Claude Code Hooks
2339
-
2340
- ${generateHooksDocumentation()}
2341
-
2342
- ### Cursor Hooks
2343
-
2344
- Cursor uses a \`hooks.json\` configuration file with enforcement + lifecycle hooks:
2345
- - **preToolUse / beforeMCPExecution / beforeShellExecution / beforeReadFile**: blocks or redirects non-ContextStream-first flows
2346
- - **beforeSubmitPrompt**: injects ContextStream rules reminder + save-intent capture
2347
- - **afterMCPExecution / afterShellExecution / afterFileEdit**: post-action bookkeeping/index updates
2348
- - **sessionStart / preCompact / stop / sessionEnd**: lifecycle persistence and reliability
2349
-
2350
- ### Windsurf Hooks
2351
-
2352
- Windsurf uses a \`hooks.json\` configuration file:
2353
- - **pre_mcp_tool_use / pre_read_code / pre_write_code / pre_run_command**: pre-action gating and enforcement
2354
- - **pre_user_prompt**: injects ContextStream-first guidance each prompt
2355
- - **post_write_code / post_mcp_tool_use**: post-action indexing bookkeeping
2356
- - **post_cascade_response_with_transcript**: end-of-response/session capture
2357
-
2358
- #### Output Format
2359
- \`\`\`json
2360
- { "decision": "allow" }
2361
- \`\`\`
2362
- or
2363
- \`\`\`json
2364
- { "decision": "deny", "reason": "Use ContextStream search instead" }
2365
- \`\`\`
2366
-
2367
- ### Cline/Roo/Kilo Code Hooks
2368
-
2369
- These editors use the same hook format (JSON output):
2370
- - **PreToolUse**: Blocks discovery tools, redirects to ContextStream search
2371
- - **UserPromptSubmit**: Injects ContextStream rules reminder
2372
-
2373
- Hooks are executable scripts named after the hook type (no extension).
2374
-
2375
- #### Output Format
2376
- \`\`\`json
2377
- {
2378
- "cancel": true,
2379
- "errorMessage": "Use ContextStream search instead",
2380
- "contextModification": "[CONTEXTSTREAM] Use search tool first"
2381
- }
2382
- \`\`\`
2383
-
2384
- ### Installation
2385
-
2386
- Use \`generate_rules(install_hooks=true, editors=["claude", "cursor", "windsurf", "cline", "roo", "kilo"])\` to install hooks for specific editors, or omit \`editors\` to install for all.
2387
-
2388
- ### Disabling Hooks
2389
-
2390
- Set environment variables:
2391
- - \`CONTEXTSTREAM_HOOK_ENABLED=false\` - Disable PreToolUse blocking
2392
- - \`CONTEXTSTREAM_REMINDER_ENABLED=false\` - Disable UserPromptSubmit reminders
2393
- `.trim();
2394
- }
2395
- var PRETOOLUSE_HOOK_SCRIPT, USER_PROMPT_HOOK_SCRIPT, MEDIA_AWARE_HOOK_SCRIPT, PRECOMPACT_HOOK_SCRIPT, CLAUDE_ENFORCEMENT_CRITICAL_HOOKS, CURSOR_ENFORCEMENT_CRITICAL_HOOKS, CLINE_PRETOOLUSE_HOOK_SCRIPT, CLINE_USER_PROMPT_HOOK_SCRIPT, CLINE_POSTTOOLUSE_HOOK_SCRIPT, CURSOR_PRETOOLUSE_HOOK_SCRIPT, CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT;
2396
- var init_hooks_config = __esm({
2397
- "src/hooks-config.ts"() {
2398
- "use strict";
2399
- PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
2400
- """
2401
- ContextStream PreToolUse Hook for Claude Code
2402
- Blocks Grep/Glob/Search/Explore/Task(Explore|Plan)/EnterPlanMode and redirects to ContextStream.
2403
-
2404
- Only blocks if the current project is indexed in ContextStream.
2405
- If not indexed, allows local tools through with a suggestion to index.
2406
- """
2407
-
2408
- import json
2409
- import sys
2410
- import os
2411
- from pathlib import Path
2412
- from datetime import datetime, timedelta
2413
-
2414
- ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
2415
- INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
2416
- # Consider index stale after 7 days
2417
- STALE_THRESHOLD_DAYS = 7
2418
-
2419
- DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
2420
-
2421
- def is_discovery_glob(pattern):
2422
- pattern_lower = pattern.lower()
2423
- for p in DISCOVERY_PATTERNS:
2424
- if p in pattern_lower:
2425
- return True
2426
- if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
2427
- return True
2428
- if "**" in pattern or "*/" in pattern:
2429
- return True
2430
- return False
2431
-
2432
- def is_discovery_grep(file_path):
2433
- if not file_path or file_path in [".", "./", "*", "**"]:
2434
- return True
2435
- if "*" in file_path or "**" in file_path:
2436
- return True
2437
- return False
2438
-
2439
- def is_project_indexed(cwd: str) -> tuple[bool, bool]:
2440
- """
2441
- Check if the current directory is in an indexed project.
2442
- Returns (is_indexed, is_stale).
2443
- """
2444
- if not INDEX_STATUS_FILE.exists():
2445
- return False, False
2446
-
2447
- try:
2448
- with open(INDEX_STATUS_FILE, "r") as f:
2449
- data = json.load(f)
2450
- except:
2451
- return False, False
2452
-
2453
- projects = data.get("projects", {})
2454
- cwd_path = Path(cwd).resolve()
2455
-
2456
- # Check if cwd is within any indexed project
2457
- for project_path, info in projects.items():
2458
- try:
2459
- indexed_path = Path(project_path).resolve()
2460
- # Check if cwd is the project or a subdirectory
2461
- if cwd_path == indexed_path or indexed_path in cwd_path.parents:
2462
- # Check if stale
2463
- indexed_at = info.get("indexed_at")
2464
- if indexed_at:
2465
- try:
2466
- indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
2467
- if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
2468
- return True, True # Indexed but stale
2469
- except:
2470
- pass
2471
- return True, False # Indexed and fresh
2472
- except:
2473
- continue
2474
-
2475
- return False, False
2476
-
2477
- def main():
2478
- if not ENABLED:
2479
- sys.exit(0)
2480
-
2481
- try:
2482
- data = json.load(sys.stdin)
2483
- except:
2484
- sys.exit(0)
2485
-
2486
- tool = data.get("tool_name", "")
2487
- inp = data.get("tool_input", {})
2488
- cwd = data.get("cwd", os.getcwd())
2489
-
2490
- # Check if project is indexed
2491
- is_indexed, is_stale = is_project_indexed(cwd)
2492
-
2493
- if not is_indexed:
2494
- # Project not indexed - allow local tools but suggest indexing
2495
- # Don't block, just exit successfully
2496
- sys.exit(0)
2497
-
2498
- if is_stale:
2499
- # Index is stale - allow with warning (printed but not blocking)
2500
- # Still allow the tool but remind about re-indexing
2501
- pass # Continue to blocking logic but could add warning
2502
-
2503
- if tool == "Glob":
2504
- pattern = inp.get("pattern", "")
2505
- if is_discovery_glob(pattern):
2506
- print(f"STOP: Use mcp__contextstream__search(mode=\\"auto\\", query=\\"{pattern}\\") instead of Glob.", file=sys.stderr)
2507
- sys.exit(2)
2508
-
2509
- elif tool == "Grep" or tool == "Search":
2510
- # Block ALL Grep/Search operations - use ContextStream search or Read for specific files
2511
- pattern = inp.get("pattern", "")
2512
- path = inp.get("path", "")
2513
- if pattern:
2514
- if path and not is_discovery_grep(path):
2515
- # Specific file - suggest Read instead
2516
- print(f"STOP: Use Read(\\"{path}\\") to view file content, or mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") for codebase search.", file=sys.stderr)
2517
- else:
2518
- print(f"STOP: Use mcp__contextstream__search(mode=\\"auto\\", query=\\"{pattern}\\") instead of {tool}.", file=sys.stderr)
2519
- sys.exit(2)
2520
-
2521
- elif tool == "Explore":
2522
- print("STOP: Use mcp__contextstream__search(mode=\\"auto\\", output_format=\\"paths\\") instead of Explore.", file=sys.stderr)
2523
- sys.exit(2)
2524
-
2525
- elif tool == "Task":
2526
- subagent = inp.get("subagent_type", "") or inp.get("subagentType", "")
2527
- subagent = subagent.lower()
2528
- if "explore" in subagent:
2529
- print("STOP: Use mcp__contextstream__search(mode=\\"auto\\") instead of Task(Explore).", file=sys.stderr)
2530
- sys.exit(2)
2531
- if "plan" in subagent:
2532
- print("STOP: For planning, use mcp__contextstream__search(mode=\\"auto\\", output_format=\\"paths\\") for discovery and mcp__contextstream__session(action=\\"capture_plan\\") for persistence.", file=sys.stderr)
2533
- sys.exit(2)
2534
-
2535
- elif tool == "EnterPlanMode":
2536
- 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)
2537
- sys.exit(2)
2538
-
2539
- sys.exit(0)
2540
-
2541
- if __name__ == "__main__":
2542
- main()
2543
- `;
2544
- USER_PROMPT_HOOK_SCRIPT = `#!/usr/bin/env python3
2545
- """
2546
- ContextStream UserPromptSubmit Hook - Injects reminder on every message.
2547
- """
2548
-
2549
- import json
2550
- import sys
2551
- import os
2552
-
2553
- ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
2554
-
2555
- REMINDER = """[CONTEXTSTREAM RULES]
2556
- 1. BEFORE Glob/Grep/Read/Search: mcp__contextstream__search(mode="auto") FIRST
2557
- 2. Call context_smart at start of EVERY response
2558
- 3. Local tools ONLY if ContextStream returns 0 results
2559
- [END RULES]"""
2560
-
2561
- def main():
2562
- if not ENABLED:
2563
- sys.exit(0)
2564
-
2565
- try:
2566
- json.load(sys.stdin)
2567
- except:
2568
- sys.exit(0)
2569
-
2570
- print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": REMINDER}}))
2571
- sys.exit(0)
2572
-
2573
- if __name__ == "__main__":
2574
- main()
2575
- `;
2576
- MEDIA_AWARE_HOOK_SCRIPT = `#!/usr/bin/env python3
2577
- """
2578
- ContextStream Media-Aware Hook for Claude Code
2579
-
2580
- Detects media-related prompts and injects context about the media tool.
2581
- """
2582
-
2583
- import json
2584
- import sys
2585
- import os
2586
- import re
2587
-
2588
- ENABLED = os.environ.get("CONTEXTSTREAM_MEDIA_HOOK_ENABLED", "true").lower() == "true"
2589
-
2590
- # Media patterns (case-insensitive)
2591
- PATTERNS = [
2592
- r"\\b(video|videos|clip|clips|footage|keyframe)s?\\b",
2593
- r"\\b(remotion|timeline|video\\s*edit)\\b",
2594
- r"\\b(image|images|photo|photos|picture|thumbnail)s?\\b",
2595
- r"\\b(audio|podcast|transcript|transcription|voice)\\b",
2596
- r"\\b(media|asset|assets|creative|b-roll)\\b",
2597
- r"\\b(find|search|show).*(clip|video|image|audio|footage|media)\\b",
2598
- ]
2599
-
2600
- COMPILED = [re.compile(p, re.IGNORECASE) for p in PATTERNS]
2601
-
2602
- MEDIA_CONTEXT = """[MEDIA TOOLS AVAILABLE]
2603
- Your workspace may have indexed media. Use ContextStream media tools:
2604
-
2605
- - **Search**: \`mcp__contextstream__media(action="search", query="description")\`
2606
- - **Get clip**: \`mcp__contextstream__media(action="get_clip", content_id="...", start="1:34", end="2:15", output_format="remotion|ffmpeg|raw")\`
2607
- - **List assets**: \`mcp__contextstream__media(action="list")\`
2608
- - **Index**: \`mcp__contextstream__media(action="index", file_path="...", content_type="video|audio|image|document")\`
2609
-
2610
- For Remotion: use \`output_format="remotion"\` to get frame-based props.
2611
- [END MEDIA TOOLS]"""
2612
-
2613
- def matches(text):
2614
- return any(p.search(text) for p in COMPILED)
2615
-
2616
- def main():
2617
- if not ENABLED:
2618
- sys.exit(0)
2619
-
2620
- try:
2621
- data = json.load(sys.stdin)
2622
- except:
2623
- sys.exit(0)
2624
-
2625
- prompt = data.get("prompt", "")
2626
- if not prompt:
2627
- session = data.get("session", {})
2628
- for msg in reversed(session.get("messages", [])):
2629
- if msg.get("role") == "user":
2630
- content = msg.get("content", "")
2631
- prompt = content if isinstance(content, str) else ""
2632
- if isinstance(content, list):
2633
- for b in content:
2634
- if isinstance(b, dict) and b.get("type") == "text":
2635
- prompt = b.get("text", "")
2636
- break
2637
- break
2638
-
2639
- if not prompt or not matches(prompt):
2640
- sys.exit(0)
2641
-
2642
- print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": MEDIA_CONTEXT}}))
2643
- sys.exit(0)
2644
-
2645
- if __name__ == "__main__":
2646
- main()
2647
- `;
2648
- PRECOMPACT_HOOK_SCRIPT = `#!/usr/bin/env python3
2649
- """
2650
- ContextStream PreCompact Hook for Claude Code
2651
-
2652
- Runs BEFORE conversation context is compacted (manual via /compact or automatic).
2653
- Automatically saves conversation state to ContextStream by parsing the transcript.
2654
-
2655
- Input (via stdin):
2656
- {
2657
- "session_id": "...",
2658
- "transcript_path": "/path/to/transcript.jsonl",
2659
- "permission_mode": "default",
2660
- "hook_event_name": "PreCompact",
2661
- "trigger": "manual" | "auto",
2662
- "custom_instructions": "..."
2663
- }
2664
-
2665
- Output (to stdout):
2666
- {
2667
- "hookSpecificOutput": {
2668
- "hookEventName": "PreCompact",
2669
- "additionalContext": "... status message ..."
2670
- }
2671
- }
2672
- """
2673
-
2674
- import json
2675
- import sys
2676
- import os
2677
- import re
2678
- import urllib.request
2679
- import urllib.error
2680
-
2681
- ENABLED = os.environ.get("CONTEXTSTREAM_PRECOMPACT_ENABLED", "true").lower() == "true"
2682
- AUTO_SAVE = os.environ.get("CONTEXTSTREAM_PRECOMPACT_AUTO_SAVE", "true").lower() == "true"
2683
- API_URL = os.environ.get("CONTEXTSTREAM_API_URL", "https://api.contextstream.io")
2684
- API_KEY = os.environ.get("CONTEXTSTREAM_API_KEY", "")
2685
-
2686
- WORKSPACE_ID = None
2687
-
2688
- def load_config_from_mcp_json(cwd):
2689
- """Load API config from .mcp.json if env vars not set."""
2690
- global API_URL, API_KEY, WORKSPACE_ID
2691
-
2692
- # Try to find .mcp.json and .contextstream/config.json in cwd or parent directories
2693
- search_dir = cwd
2694
- for _ in range(5): # Search up to 5 levels
2695
- # Load API config from .mcp.json
2696
- if not API_KEY:
2697
- mcp_path = os.path.join(search_dir, ".mcp.json")
2698
- if os.path.exists(mcp_path):
2699
- try:
2700
- with open(mcp_path, 'r') as f:
2701
- config = json.load(f)
2702
- servers = config.get("mcpServers", {})
2703
- cs_config = servers.get("contextstream", {})
2704
- env = cs_config.get("env", {})
2705
- if env.get("CONTEXTSTREAM_API_KEY"):
2706
- API_KEY = env["CONTEXTSTREAM_API_KEY"]
2707
- if env.get("CONTEXTSTREAM_API_URL"):
2708
- API_URL = env["CONTEXTSTREAM_API_URL"]
2709
- except:
2710
- pass
2711
-
2712
- # Load workspace_id from .contextstream/config.json
2713
- if not WORKSPACE_ID:
2714
- cs_config_path = os.path.join(search_dir, ".contextstream", "config.json")
2715
- if os.path.exists(cs_config_path):
2716
- try:
2717
- with open(cs_config_path, 'r') as f:
2718
- cs_config = json.load(f)
2719
- if cs_config.get("workspace_id"):
2720
- WORKSPACE_ID = cs_config["workspace_id"]
2721
- except:
2722
- pass
2723
-
2724
- parent = os.path.dirname(search_dir)
2725
- if parent == search_dir:
2726
- break
2727
- search_dir = parent
2728
-
2729
- def parse_transcript(transcript_path):
2730
- """Parse transcript to extract active files, decisions, and context."""
2731
- active_files = set()
2732
- recent_messages = []
2733
- tool_calls = []
2734
-
2735
- try:
2736
- with open(transcript_path, 'r') as f:
2737
- for line in f:
2738
- try:
2739
- entry = json.loads(line.strip())
2740
- msg_type = entry.get("type", "")
2741
-
2742
- # Extract files from tool calls
2743
- if msg_type == "tool_use":
2744
- tool_name = entry.get("name", "")
2745
- tool_input = entry.get("input", {})
2746
- tool_calls.append({"name": tool_name, "input": tool_input})
2747
-
2748
- # Extract file paths from common tools
2749
- if tool_name in ["Read", "Write", "Edit", "NotebookEdit"]:
2750
- file_path = tool_input.get("file_path") or tool_input.get("notebook_path")
2751
- if file_path:
2752
- active_files.add(file_path)
2753
- elif tool_name == "Glob":
2754
- pattern = tool_input.get("pattern", "")
2755
- if pattern:
2756
- active_files.add(f"[glob:{pattern}]")
2757
-
2758
- # Collect recent assistant messages for summary
2759
- if msg_type == "assistant" and entry.get("content"):
2760
- content = entry.get("content", "")
2761
- if isinstance(content, str) and len(content) > 50:
2762
- recent_messages.append(content[:500])
2763
-
2764
- except json.JSONDecodeError:
2765
- continue
2766
- except Exception as e:
2767
- pass
2768
-
2769
- return {
2770
- "active_files": list(active_files)[-20:], # Last 20 files
2771
- "tool_call_count": len(tool_calls),
2772
- "message_count": len(recent_messages),
2773
- "last_tools": [t["name"] for t in tool_calls[-10:]], # Last 10 tool names
2774
- }
2775
-
2776
- def save_snapshot(session_id, transcript_data, trigger):
2777
- """Save snapshot to ContextStream API."""
2778
- if not API_KEY:
2779
- return False, "No API key configured"
2780
-
2781
- snapshot_content = {
2782
- "session_id": session_id,
2783
- "trigger": trigger,
2784
- "captured_at": None, # API will set timestamp
2785
- "active_files": transcript_data.get("active_files", []),
2786
- "tool_call_count": transcript_data.get("tool_call_count", 0),
2787
- "last_tools": transcript_data.get("last_tools", []),
2788
- "auto_captured": True,
2789
- }
2790
-
2791
- payload = {
2792
- "event_type": "session_snapshot",
2793
- "title": f"Auto Pre-compaction Snapshot ({trigger})",
2794
- "content": json.dumps(snapshot_content),
2795
- "importance": "high",
2796
- "tags": ["session_snapshot", "pre_compaction", "auto_captured"],
2797
- "source_type": "hook",
2798
- }
2799
-
2800
- # Add workspace_id if available
2801
- if WORKSPACE_ID:
2802
- payload["workspace_id"] = WORKSPACE_ID
2803
-
2804
- try:
2805
- req = urllib.request.Request(
2806
- f"{API_URL}/api/v1/memory/events",
2807
- data=json.dumps(payload).encode('utf-8'),
2808
- headers={
2809
- "Content-Type": "application/json",
2810
- "X-API-Key": API_KEY,
2811
- },
2812
- method="POST"
2813
- )
2814
- with urllib.request.urlopen(req, timeout=5) as resp:
2815
- return True, "Snapshot saved"
2816
- except urllib.error.URLError as e:
2817
- return False, str(e)
2818
- except Exception as e:
2819
- return False, str(e)
2820
-
2821
- def main():
2822
- if not ENABLED:
2823
- sys.exit(0)
2824
-
2825
- try:
2826
- data = json.load(sys.stdin)
2827
- except:
2828
- sys.exit(0)
2829
-
2830
- # Load config from .mcp.json if env vars not set
2831
- cwd = data.get("cwd", os.getcwd())
2832
- load_config_from_mcp_json(cwd)
2833
-
2834
- session_id = data.get("session_id", "unknown")
2835
- transcript_path = data.get("transcript_path", "")
2836
- trigger = data.get("trigger", "unknown")
2837
- custom_instructions = data.get("custom_instructions", "")
2838
-
2839
- # Parse transcript for context
2840
- transcript_data = {}
2841
- if transcript_path and os.path.exists(transcript_path):
2842
- transcript_data = parse_transcript(transcript_path)
2843
-
2844
- # Auto-save snapshot if enabled
2845
- auto_save_status = ""
2846
- if AUTO_SAVE and API_KEY:
2847
- success, msg = save_snapshot(session_id, transcript_data, trigger)
2848
- if success:
2849
- auto_save_status = f"\\n[ContextStream: Auto-saved snapshot with {len(transcript_data.get('active_files', []))} active files]"
2850
- else:
2851
- auto_save_status = f"\\n[ContextStream: Auto-save failed - {msg}]"
2852
-
2853
- # Build context injection for the AI (backup in case auto-save fails)
2854
- files_list = ", ".join(transcript_data.get("active_files", [])[:5]) or "none detected"
2855
- context = f"""[CONTEXT COMPACTION - {trigger.upper()}]{auto_save_status}
2856
-
2857
- Active files detected: {files_list}
2858
- Tool calls in session: {transcript_data.get('tool_call_count', 0)}
2859
-
2860
- After compaction, call session_init(is_post_compact=true) to restore context.
2861
- {f"User instructions: {custom_instructions}" if custom_instructions else ""}"""
2862
-
2863
- output = {
2864
- "hookSpecificOutput": {
2865
- "hookEventName": "PreCompact",
2866
- "additionalContext": context
2867
- }
2868
- }
2869
-
2870
- print(json.dumps(output))
2871
- sys.exit(0)
2872
-
2873
- if __name__ == "__main__":
2874
- main()
2875
- `;
2876
- CLAUDE_ENFORCEMENT_CRITICAL_HOOKS = [
2877
- "PreToolUse",
2878
- "UserPromptSubmit",
2879
- "SessionStart",
2880
- "PreCompact",
2881
- "PostToolUse"
2882
- ];
2883
- CURSOR_ENFORCEMENT_CRITICAL_HOOKS = [
2884
- "preToolUse",
2885
- "beforeSubmitPrompt",
2886
- "beforeMCPExecution",
2887
- "beforeShellExecution",
2888
- "beforeReadFile",
2889
- "afterFileEdit",
2890
- "sessionStart",
2891
- "preCompact"
2892
- ];
2893
- CLINE_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
2894
- """
2895
- ContextStream PreToolUse Hook for Cline
2896
- Blocks discovery tools and redirects to ContextStream search.
2897
-
2898
- Cline hooks use JSON output format:
2899
- {
2900
- "cancel": true/false,
2901
- "errorMessage": "optional error description",
2902
- "contextModification": "optional text to inject"
2903
- }
2904
- """
2905
-
2906
- import json
2907
- import sys
2908
- import os
2909
- from pathlib import Path
2910
- from datetime import datetime, timedelta
2911
-
2912
- ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
2913
- INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
2914
- STALE_THRESHOLD_DAYS = 7
2915
-
2916
- DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
2917
-
2918
- def is_discovery_glob(pattern):
2919
- pattern_lower = pattern.lower()
2920
- for p in DISCOVERY_PATTERNS:
2921
- if p in pattern_lower:
2922
- return True
2923
- if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
2924
- return True
2925
- if "**" in pattern or "*/" in pattern:
2926
- return True
2927
- return False
2928
-
2929
- def is_discovery_grep(file_path):
2930
- if not file_path or file_path in [".", "./", "*", "**"]:
2931
- return True
2932
- if "*" in file_path or "**" in file_path:
2933
- return True
2934
- return False
2935
-
2936
- def is_project_indexed(workspace_roots):
2937
- """Check if any workspace root is in an indexed project."""
2938
- if not INDEX_STATUS_FILE.exists():
2939
- return False, False
2940
-
2941
- try:
2942
- with open(INDEX_STATUS_FILE, "r") as f:
2943
- data = json.load(f)
2944
- except:
2945
- return False, False
2946
-
2947
- projects = data.get("projects", {})
2948
-
2949
- for workspace in workspace_roots:
2950
- cwd_path = Path(workspace).resolve()
2951
- for project_path, info in projects.items():
2952
- try:
2953
- indexed_path = Path(project_path).resolve()
2954
- if cwd_path == indexed_path or indexed_path in cwd_path.parents:
2955
- indexed_at = info.get("indexed_at")
2956
- if indexed_at:
2957
- try:
2958
- indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
2959
- if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
2960
- return True, True
2961
- except:
2962
- pass
2963
- return True, False
2964
- except:
2965
- continue
2966
- return False, False
2967
-
2968
- def output_allow(context_mod=None):
2969
- result = {"cancel": False}
2970
- if context_mod:
2971
- result["contextModification"] = context_mod
2972
- print(json.dumps(result))
2973
- sys.exit(0)
2974
-
2975
- def output_block(error_msg, context_mod=None):
2976
- result = {"cancel": True, "errorMessage": error_msg}
2977
- if context_mod:
2978
- result["contextModification"] = context_mod
2979
- print(json.dumps(result))
2980
- sys.exit(0)
2981
-
2982
- def main():
2983
- if not ENABLED:
2984
- output_allow()
2985
-
2986
- try:
2987
- data = json.load(sys.stdin)
2988
- except:
2989
- output_allow()
2990
-
2991
- hook_name = data.get("hookName", "")
2992
- if hook_name != "PreToolUse":
2993
- output_allow()
2994
-
2995
- tool = data.get("toolName", "")
2996
- params = data.get("toolParameters", {})
2997
- workspace_roots = data.get("workspaceRoots", [])
2998
-
2999
- # Check if project is indexed
3000
- is_indexed, is_stale = is_project_indexed(workspace_roots)
3001
- if not is_indexed:
3002
- output_allow()
3003
-
3004
- # Check for discovery patterns
3005
- if tool == "list_files" or tool == "search_files":
3006
- pattern = params.get("path", "") or params.get("regex", "")
3007
- if is_discovery_glob(pattern) or is_discovery_grep(pattern):
3008
- output_block(
3009
- f"Use mcp__contextstream__search(mode=\\"auto\\", query=\\"{pattern}\\") instead of {tool}. "
3010
- "ContextStream search is indexed and faster. Only use local tools if ContextStream returns 0 results.",
3011
- "[CONTEXTSTREAM] Use ContextStream search for code discovery."
3012
- )
3013
-
3014
- elif tool == "read_file":
3015
- # Allow read_file by default - blocking discovery at search level is enough
3016
- pass
3017
-
3018
- output_allow()
3019
-
3020
- if __name__ == "__main__":
3021
- main()
3022
- `;
3023
- CLINE_USER_PROMPT_HOOK_SCRIPT = `#!/usr/bin/env python3
3024
- """
3025
- ContextStream UserPromptSubmit Hook for Cline
3026
- Injects reminder about ContextStream rules on every message.
3027
- """
3028
-
3029
- import json
3030
- import sys
3031
- import os
3032
-
3033
- ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
3034
-
3035
- REMINDER = """[CONTEXTSTREAM RULES]
3036
- 1. BEFORE list_files/search_files/read_file: mcp__contextstream__search(mode="auto") FIRST
3037
- 2. Call context_smart at start of EVERY response
3038
- 3. Local tools ONLY if ContextStream returns 0 results
3039
- [END RULES]"""
3040
-
3041
- def main():
3042
- if not ENABLED:
3043
- print(json.dumps({"cancel": False}))
3044
- sys.exit(0)
3045
-
3046
- try:
3047
- json.load(sys.stdin)
3048
- except:
3049
- print(json.dumps({"cancel": False}))
3050
- sys.exit(0)
3051
-
3052
- print(json.dumps({
3053
- "cancel": False,
3054
- "contextModification": REMINDER
3055
- }))
3056
- sys.exit(0)
3057
-
3058
- if __name__ == "__main__":
3059
- main()
3060
- `;
3061
- CLINE_POSTTOOLUSE_HOOK_SCRIPT = `#!/bin/bash
3062
- # ContextStream PostToolUse Hook for Cline/Roo/Kilo Code
3063
- # Indexes files after Edit/Write/NotebookEdit operations for real-time search updates.
3064
- #
3065
- # The hook receives JSON on stdin with tool_name and toolParameters.
3066
- # Only runs for write operations (write_to_file, edit_file).
3067
-
3068
- TOOL_NAME=$(cat | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('toolName', d.get('tool_name', '')))" 2>/dev/null)
3069
-
3070
- case "$TOOL_NAME" in
3071
- write_to_file|edit_file|Write|Edit|NotebookEdit)
3072
- npx @contextstream/mcp-server hook post-write
3073
- ;;
3074
- esac
3075
-
3076
- exit 0
3077
- `;
3078
- CURSOR_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
3079
- """
3080
- ContextStream PreToolUse Hook for Cursor
3081
- Blocks discovery tools and redirects to ContextStream search.
3082
-
3083
- Cursor hooks use JSON output format:
3084
- {
3085
- "decision": "allow" | "deny",
3086
- "reason": "optional error description"
3087
- }
3088
- """
3089
-
3090
- import json
3091
- import sys
3092
- import os
3093
- from pathlib import Path
3094
- from datetime import datetime, timedelta
3095
-
3096
- ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
3097
- INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
3098
- STALE_THRESHOLD_DAYS = 7
3099
-
3100
- DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
3101
-
3102
- def is_discovery_glob(pattern):
3103
- pattern_lower = pattern.lower()
3104
- for p in DISCOVERY_PATTERNS:
3105
- if p in pattern_lower:
3106
- return True
3107
- if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
3108
- return True
3109
- if "**" in pattern or "*/" in pattern:
3110
- return True
3111
- return False
3112
-
3113
- def is_discovery_grep(file_path):
3114
- if not file_path or file_path in [".", "./", "*", "**"]:
3115
- return True
3116
- if "*" in file_path or "**" in file_path:
3117
- return True
3118
- return False
3119
-
3120
- def is_project_indexed(workspace_roots):
3121
- """Check if any workspace root is in an indexed project."""
3122
- if not INDEX_STATUS_FILE.exists():
3123
- return False, False
3124
-
3125
- try:
3126
- with open(INDEX_STATUS_FILE, "r") as f:
3127
- data = json.load(f)
3128
- except:
3129
- return False, False
3130
-
3131
- projects = data.get("projects", {})
3132
-
3133
- for workspace in workspace_roots:
3134
- cwd_path = Path(workspace).resolve()
3135
- for project_path, info in projects.items():
3136
- try:
3137
- indexed_path = Path(project_path).resolve()
3138
- if cwd_path == indexed_path or indexed_path in cwd_path.parents:
3139
- indexed_at = info.get("indexed_at")
3140
- if indexed_at:
3141
- try:
3142
- indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
3143
- if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
3144
- return True, True
3145
- except:
3146
- pass
3147
- return True, False
3148
- except:
3149
- continue
3150
- return False, False
3151
-
3152
- def output_allow():
3153
- print(json.dumps({"decision": "allow"}))
3154
- sys.exit(0)
3155
-
3156
- def output_deny(reason):
3157
- print(json.dumps({"decision": "deny", "reason": reason}))
3158
- sys.exit(0)
3159
-
3160
- def main():
3161
- if not ENABLED:
3162
- output_allow()
3163
-
3164
- try:
3165
- data = json.load(sys.stdin)
3166
- except:
3167
- output_allow()
3168
-
3169
- hook_name = data.get("hook_event_name", "")
3170
- if hook_name != "preToolUse":
3171
- output_allow()
3172
-
3173
- tool = data.get("tool_name", "")
3174
- params = data.get("tool_input", {}) or data.get("parameters", {})
3175
- workspace_roots = data.get("workspace_roots", [])
3176
-
3177
- # Check if project is indexed
3178
- is_indexed, _ = is_project_indexed(workspace_roots)
3179
- if not is_indexed:
3180
- output_allow()
3181
-
3182
- # Check for Cursor tools
3183
- if tool in ["Glob", "glob", "list_files"]:
3184
- pattern = params.get("pattern", "") or params.get("path", "")
3185
- if is_discovery_glob(pattern):
3186
- output_deny(
3187
- f"Use mcp__contextstream__search(mode=\\"auto\\", query=\\"{pattern}\\") instead of {tool}. "
3188
- "ContextStream search is indexed and faster."
3189
- )
3190
-
3191
- elif tool in ["Grep", "grep", "search_files", "ripgrep"]:
3192
- pattern = params.get("pattern", "") or params.get("regex", "")
3193
- file_path = params.get("path", "")
3194
- if is_discovery_grep(file_path):
3195
- output_deny(
3196
- f"Use mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") instead of {tool}. "
3197
- "ContextStream search is indexed and faster."
3198
- )
3199
-
3200
- output_allow()
3201
-
3202
- if __name__ == "__main__":
3203
- main()
3204
- `;
3205
- CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT = `#!/usr/bin/env python3
3206
- """
3207
- ContextStream BeforeSubmitPrompt Hook for Cursor
3208
- Injects reminder about ContextStream rules.
3209
- """
3210
-
3211
- import json
3212
- import sys
3213
- import os
3214
-
3215
- ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
3216
-
3217
- def main():
3218
- if not ENABLED:
3219
- print(json.dumps({"continue": True}))
3220
- sys.exit(0)
3221
-
3222
- try:
3223
- json.load(sys.stdin)
3224
- except:
3225
- print(json.dumps({"continue": True}))
3226
- sys.exit(0)
3227
-
3228
- print(json.dumps({
3229
- "continue": True,
3230
- "user_message": "[CONTEXTSTREAM] Search with mcp__contextstream__search before using Glob/Grep/Read"
3231
- }))
3232
- sys.exit(0)
3233
-
3234
- if __name__ == "__main__":
3235
- main()
3236
- `;
3237
- }
3238
- });
3239
-
3240
- // src/hooks/auto-rules.ts
3241
- import * as fs4 from "node:fs";
3242
- import * as path4 from "node:path";
3243
- import { homedir as homedir3 } from "node:os";
3244
- var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
3245
- var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
3246
- var ENABLED = process.env.CONTEXTSTREAM_AUTO_RULES !== "false";
3247
- var MARKER_FILE = path4.join(homedir3(), ".contextstream", ".auto-rules-ran");
3248
- var COOLDOWN_MS = 4 * 60 * 60 * 1e3;
3249
- function hasRunRecently() {
3250
- try {
3251
- if (!fs4.existsSync(MARKER_FILE)) return false;
3252
- const stat = fs4.statSync(MARKER_FILE);
3253
- const age = Date.now() - stat.mtimeMs;
3254
- return age < COOLDOWN_MS;
3255
- } catch {
3256
- return false;
3257
- }
3258
- }
3259
- function markAsRan() {
3260
- try {
3261
- const dir = path4.dirname(MARKER_FILE);
3262
- if (!fs4.existsSync(dir)) {
3263
- fs4.mkdirSync(dir, { recursive: true });
3264
- }
3265
- fs4.writeFileSync(MARKER_FILE, (/* @__PURE__ */ new Date()).toISOString());
3266
- } catch {
3267
- }
3268
- }
3269
- function extractRulesNotice(input) {
3270
- if (input.tool_result) {
3271
- try {
3272
- const parsed = JSON.parse(input.tool_result);
3273
- if (parsed.rules_notice) return parsed.rules_notice;
3274
- } catch {
3275
- }
3276
- }
3277
- if (input.tool_response?.structuredContent) {
3278
- const sc = input.tool_response.structuredContent;
3279
- if (sc.rules_notice) return sc.rules_notice;
3280
- }
3281
- if (input.response) {
3282
- if (input.response.rules_notice) {
3283
- return input.response.rules_notice;
3284
- }
3285
- }
3286
- return null;
3287
- }
3288
- function extractCwd(input) {
3289
- if (input.cwd) return input.cwd;
3290
- return process.cwd();
3291
- }
3292
- function hasPythonHooks(settingsPath) {
3293
- try {
3294
- if (!fs4.existsSync(settingsPath)) return false;
3295
- const content = fs4.readFileSync(settingsPath, "utf-8");
3296
- const settings = JSON.parse(content);
3297
- const hooks = settings.hooks;
3298
- if (!hooks) return false;
3299
- for (const hookType of Object.keys(hooks)) {
3300
- const matchers = hooks[hookType];
3301
- if (!Array.isArray(matchers)) continue;
3302
- for (const matcher of matchers) {
3303
- const hookList = matcher.hooks;
3304
- if (!Array.isArray(hookList)) continue;
3305
- for (const hook of hookList) {
3306
- const cmd = hook.command || "";
3307
- if (cmd.includes("python3") && cmd.includes("contextstream")) {
3308
- return true;
3309
- }
3310
- }
3311
- }
3312
- }
3313
- return false;
3314
- } catch {
3315
- return false;
3316
- }
3317
- }
3318
- function detectPythonHooks(cwd) {
3319
- const globalSettingsPath = path4.join(homedir3(), ".claude", "settings.json");
3320
- const projectSettingsPath = path4.join(cwd, ".claude", "settings.json");
3321
- return {
3322
- global: hasPythonHooks(globalSettingsPath),
3323
- project: hasPythonHooks(projectSettingsPath)
3324
- };
3325
- }
3326
- async function upgradeHooksForFolder(folderPath) {
3327
- const { installClaudeCodeHooks: installClaudeCodeHooks2 } = await Promise.resolve().then(() => (init_hooks_config(), hooks_config_exports));
3328
- await installClaudeCodeHooks2({
3329
- scope: "both",
3330
- projectPath: folderPath,
3331
- includePreCompact: true,
3332
- includeMediaAware: true,
3333
- includePostWrite: true,
3334
- includeAutoRules: true
3335
- });
3336
- }
3337
- async function runAutoRulesHook() {
3338
- if (!ENABLED) {
3339
- process.exit(0);
3340
- }
3341
- if (hasRunRecently()) {
3342
- process.exit(0);
3343
- }
3344
- let inputData = "";
3345
- for await (const chunk of process.stdin) {
3346
- inputData += chunk;
3347
- }
3348
- if (!inputData.trim()) {
3349
- process.exit(0);
3350
- }
3351
- let input;
3352
- try {
3353
- input = JSON.parse(inputData);
3354
- } catch {
3355
- process.exit(0);
3356
- }
3357
- const toolName = input.tool_name || input.toolName || "";
3358
- const isContextTool = toolName.includes("init") || toolName.includes("context") || toolName.includes("session_init") || toolName.includes("context_smart");
3359
- if (!isContextTool) {
3360
- process.exit(0);
3361
- }
3362
- const cwd = extractCwd(input);
3363
- const pythonHooks = detectPythonHooks(cwd);
3364
- const hasPythonHooksToUpgrade = pythonHooks.global || pythonHooks.project;
3365
- const rulesNotice = extractRulesNotice(input);
3366
- const rulesNeedUpdate = rulesNotice && rulesNotice.status !== "current";
3367
- if (!hasPythonHooksToUpgrade && !rulesNeedUpdate) {
3368
- process.exit(0);
3369
- }
3370
- const folderPath = rulesNotice?.update_args?.folder_path || cwd;
3371
- try {
3372
- await upgradeHooksForFolder(folderPath);
3373
- markAsRan();
3374
- } catch {
3375
- }
3376
- process.exit(0);
3377
- }
3378
- var isDirectRun = process.argv[1]?.includes("auto-rules") || process.argv[2] === "auto-rules";
3379
- if (isDirectRun) {
3380
- runAutoRulesHook().catch(() => process.exit(0));
3381
- }
3382
- export {
3383
- runAutoRulesHook
3384
- };