@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.
- package/NOTICE +25 -0
- package/README.md +77 -353
- package/npm/bin/contextstream-hook.mjs +9 -0
- package/npm/bin/contextstream-mcp.mjs +9 -0
- package/npm/bin/mcp-server.mjs +9 -0
- package/npm/launcher.mjs +390 -0
- package/package.json +27 -72
- package/server.json +43 -0
- package/dist/hooks/auto-rules.js +0 -3529
- package/dist/hooks/media-aware.js +0 -103
- package/dist/hooks/notification.js +0 -162
- package/dist/hooks/on-bash.js +0 -190
- package/dist/hooks/on-read.js +0 -321
- package/dist/hooks/on-save-intent.js +0 -132
- package/dist/hooks/on-task.js +0 -139
- package/dist/hooks/on-web.js +0 -155
- package/dist/hooks/permission-request.js +0 -174
- package/dist/hooks/post-compact.js +0 -172
- package/dist/hooks/post-tool-use-failure.js +0 -213
- package/dist/hooks/post-write.js +0 -1023
- package/dist/hooks/pre-compact.js +0 -317
- package/dist/hooks/pre-tool-use.js +0 -553
- package/dist/hooks/runner.js +0 -5826
- package/dist/hooks/session-end.js +0 -285
- package/dist/hooks/session-init.js +0 -1880
- package/dist/hooks/stop.js +0 -171
- package/dist/hooks/subagent-start.js +0 -195
- package/dist/hooks/subagent-stop.js +0 -301
- package/dist/hooks/task-completed.js +0 -260
- package/dist/hooks/teammate-idle.js +0 -200
- package/dist/hooks/user-prompt-submit.js +0 -866
- package/dist/index.js +0 -41985
- package/dist/test-server.js +0 -4363
- package/scripts/postinstall.js +0 -56
- package/scripts/registration-smoke.mts +0 -45
package/dist/hooks/auto-rules.js
DELETED
|
@@ -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
|
-
};
|