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