@contextstream/mcp-server 0.4.36 → 0.4.38

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/dist/index.js CHANGED
@@ -1,9 +1,496 @@
1
1
  #!/usr/bin/env node
2
+ var __create = Object.create;
2
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 __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
14
+ var __commonJS = (cb, mod) => function __require2() {
15
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ };
3
17
  var __export = (target, all) => {
4
18
  for (var name in all)
5
19
  __defProp(target, name, { get: all[name], enumerable: true });
6
20
  };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
37
+
38
+ // node_modules/ignore/index.js
39
+ var require_ignore = __commonJS({
40
+ "node_modules/ignore/index.js"(exports, module) {
41
+ function makeArray(subject) {
42
+ return Array.isArray(subject) ? subject : [subject];
43
+ }
44
+ var UNDEFINED = void 0;
45
+ var EMPTY = "";
46
+ var SPACE = " ";
47
+ var ESCAPE = "\\";
48
+ var REGEX_TEST_BLANK_LINE = /^\s+$/;
49
+ var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
50
+ var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
51
+ var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
52
+ var REGEX_SPLITALL_CRLF = /\r?\n/g;
53
+ var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
54
+ var REGEX_TEST_TRAILING_SLASH = /\/$/;
55
+ var SLASH = "/";
56
+ var TMP_KEY_IGNORE = "node-ignore";
57
+ if (typeof Symbol !== "undefined") {
58
+ TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
59
+ }
60
+ var KEY_IGNORE = TMP_KEY_IGNORE;
61
+ var define = (object, key, value) => {
62
+ Object.defineProperty(object, key, { value });
63
+ return value;
64
+ };
65
+ var REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
66
+ var RETURN_FALSE = () => false;
67
+ var sanitizeRange = (range) => range.replace(
68
+ REGEX_REGEXP_RANGE,
69
+ (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match : EMPTY
70
+ );
71
+ var cleanRangeBackSlash = (slashes) => {
72
+ const { length } = slashes;
73
+ return slashes.slice(0, length - length % 2);
74
+ };
75
+ var REPLACERS = [
76
+ [
77
+ // Remove BOM
78
+ // TODO:
79
+ // Other similar zero-width characters?
80
+ /^\uFEFF/,
81
+ () => EMPTY
82
+ ],
83
+ // > Trailing spaces are ignored unless they are quoted with backslash ("\")
84
+ [
85
+ // (a\ ) -> (a )
86
+ // (a ) -> (a)
87
+ // (a ) -> (a)
88
+ // (a \ ) -> (a )
89
+ /((?:\\\\)*?)(\\?\s+)$/,
90
+ (_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY)
91
+ ],
92
+ // Replace (\ ) with ' '
93
+ // (\ ) -> ' '
94
+ // (\\ ) -> '\\ '
95
+ // (\\\ ) -> '\\ '
96
+ [
97
+ /(\\+?)\s/g,
98
+ (_, m1) => {
99
+ const { length } = m1;
100
+ return m1.slice(0, length - length % 2) + SPACE;
101
+ }
102
+ ],
103
+ // Escape metacharacters
104
+ // which is written down by users but means special for regular expressions.
105
+ // > There are 12 characters with special meanings:
106
+ // > - the backslash \,
107
+ // > - the caret ^,
108
+ // > - the dollar sign $,
109
+ // > - the period or dot .,
110
+ // > - the vertical bar or pipe symbol |,
111
+ // > - the question mark ?,
112
+ // > - the asterisk or star *,
113
+ // > - the plus sign +,
114
+ // > - the opening parenthesis (,
115
+ // > - the closing parenthesis ),
116
+ // > - and the opening square bracket [,
117
+ // > - the opening curly brace {,
118
+ // > These special characters are often called "metacharacters".
119
+ [
120
+ /[\\$.|*+(){^]/g,
121
+ (match) => `\\${match}`
122
+ ],
123
+ [
124
+ // > a question mark (?) matches a single character
125
+ /(?!\\)\?/g,
126
+ () => "[^/]"
127
+ ],
128
+ // leading slash
129
+ [
130
+ // > A leading slash matches the beginning of the pathname.
131
+ // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
132
+ // A leading slash matches the beginning of the pathname
133
+ /^\//,
134
+ () => "^"
135
+ ],
136
+ // replace special metacharacter slash after the leading slash
137
+ [
138
+ /\//g,
139
+ () => "\\/"
140
+ ],
141
+ [
142
+ // > A leading "**" followed by a slash means match in all directories.
143
+ // > For example, "**/foo" matches file or directory "foo" anywhere,
144
+ // > the same as pattern "foo".
145
+ // > "**/foo/bar" matches file or directory "bar" anywhere that is directly
146
+ // > under directory "foo".
147
+ // Notice that the '*'s have been replaced as '\\*'
148
+ /^\^*\\\*\\\*\\\//,
149
+ // '**/foo' <-> 'foo'
150
+ () => "^(?:.*\\/)?"
151
+ ],
152
+ // starting
153
+ [
154
+ // there will be no leading '/'
155
+ // (which has been replaced by section "leading slash")
156
+ // If starts with '**', adding a '^' to the regular expression also works
157
+ /^(?=[^^])/,
158
+ function startingReplacer() {
159
+ return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^";
160
+ }
161
+ ],
162
+ // two globstars
163
+ [
164
+ // Use lookahead assertions so that we could match more than one `'/**'`
165
+ /\\\/\\\*\\\*(?=\\\/|$)/g,
166
+ // Zero, one or several directories
167
+ // should not use '*', or it will be replaced by the next replacer
168
+ // Check if it is not the last `'/**'`
169
+ (_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
170
+ ],
171
+ // normal intermediate wildcards
172
+ [
173
+ // Never replace escaped '*'
174
+ // ignore rule '\*' will match the path '*'
175
+ // 'abc.*/' -> go
176
+ // 'abc.*' -> skip this rule,
177
+ // coz trailing single wildcard will be handed by [trailing wildcard]
178
+ /(^|[^\\]+)(\\\*)+(?=.+)/g,
179
+ // '*.js' matches '.js'
180
+ // '*.js' doesn't match 'abc'
181
+ (_, p1, p2) => {
182
+ const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
183
+ return p1 + unescaped;
184
+ }
185
+ ],
186
+ [
187
+ // unescape, revert step 3 except for back slash
188
+ // For example, if a user escape a '\\*',
189
+ // after step 3, the result will be '\\\\\\*'
190
+ /\\\\\\(?=[$.|*+(){^])/g,
191
+ () => ESCAPE
192
+ ],
193
+ [
194
+ // '\\\\' -> '\\'
195
+ /\\\\/g,
196
+ () => ESCAPE
197
+ ],
198
+ [
199
+ // > The range notation, e.g. [a-zA-Z],
200
+ // > can be used to match one of the characters in a range.
201
+ // `\` is escaped by step 3
202
+ /(\\)?\[([^\]/]*?)(\\*)($|\])/g,
203
+ (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]"
204
+ ],
205
+ // ending
206
+ [
207
+ // 'js' will not match 'js.'
208
+ // 'ab' will not match 'abc'
209
+ /(?:[^*])$/,
210
+ // WTF!
211
+ // https://git-scm.com/docs/gitignore
212
+ // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
213
+ // which re-fixes #24, #38
214
+ // > If there is a separator at the end of the pattern then the pattern
215
+ // > will only match directories, otherwise the pattern can match both
216
+ // > files and directories.
217
+ // 'js*' will not match 'a.js'
218
+ // 'js/' will not match 'a.js'
219
+ // 'js' will match 'a.js' and 'a.js/'
220
+ (match) => /\/$/.test(match) ? `${match}$` : `${match}(?=$|\\/$)`
221
+ ]
222
+ ];
223
+ var REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/;
224
+ var MODE_IGNORE = "regex";
225
+ var MODE_CHECK_IGNORE = "checkRegex";
226
+ var UNDERSCORE = "_";
227
+ var TRAILING_WILD_CARD_REPLACERS = {
228
+ [MODE_IGNORE](_, p1) {
229
+ const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
230
+ return `${prefix}(?=$|\\/$)`;
231
+ },
232
+ [MODE_CHECK_IGNORE](_, p1) {
233
+ const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
234
+ return `${prefix}(?=$|\\/$)`;
235
+ }
236
+ };
237
+ var makeRegexPrefix = (pattern) => REPLACERS.reduce(
238
+ (prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)),
239
+ pattern
240
+ );
241
+ var isString = (subject) => typeof subject === "string";
242
+ var checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0;
243
+ var splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean);
244
+ var IgnoreRule = class {
245
+ constructor(pattern, mark, body, ignoreCase, negative, prefix) {
246
+ this.pattern = pattern;
247
+ this.mark = mark;
248
+ this.negative = negative;
249
+ define(this, "body", body);
250
+ define(this, "ignoreCase", ignoreCase);
251
+ define(this, "regexPrefix", prefix);
252
+ }
253
+ get regex() {
254
+ const key = UNDERSCORE + MODE_IGNORE;
255
+ if (this[key]) {
256
+ return this[key];
257
+ }
258
+ return this._make(MODE_IGNORE, key);
259
+ }
260
+ get checkRegex() {
261
+ const key = UNDERSCORE + MODE_CHECK_IGNORE;
262
+ if (this[key]) {
263
+ return this[key];
264
+ }
265
+ return this._make(MODE_CHECK_IGNORE, key);
266
+ }
267
+ _make(mode, key) {
268
+ const str = this.regexPrefix.replace(
269
+ REGEX_REPLACE_TRAILING_WILDCARD,
270
+ // It does not need to bind pattern
271
+ TRAILING_WILD_CARD_REPLACERS[mode]
272
+ );
273
+ const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
274
+ return define(this, key, regex);
275
+ }
276
+ };
277
+ var createRule = ({
278
+ pattern,
279
+ mark
280
+ }, ignoreCase) => {
281
+ let negative = false;
282
+ let body = pattern;
283
+ if (body.indexOf("!") === 0) {
284
+ negative = true;
285
+ body = body.substr(1);
286
+ }
287
+ body = body.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
288
+ const regexPrefix = makeRegexPrefix(body);
289
+ return new IgnoreRule(
290
+ pattern,
291
+ mark,
292
+ body,
293
+ ignoreCase,
294
+ negative,
295
+ regexPrefix
296
+ );
297
+ };
298
+ var RuleManager = class {
299
+ constructor(ignoreCase) {
300
+ this._ignoreCase = ignoreCase;
301
+ this._rules = [];
302
+ }
303
+ _add(pattern) {
304
+ if (pattern && pattern[KEY_IGNORE]) {
305
+ this._rules = this._rules.concat(pattern._rules._rules);
306
+ this._added = true;
307
+ return;
308
+ }
309
+ if (isString(pattern)) {
310
+ pattern = {
311
+ pattern
312
+ };
313
+ }
314
+ if (checkPattern(pattern.pattern)) {
315
+ const rule = createRule(pattern, this._ignoreCase);
316
+ this._added = true;
317
+ this._rules.push(rule);
318
+ }
319
+ }
320
+ // @param {Array<string> | string | Ignore} pattern
321
+ add(pattern) {
322
+ this._added = false;
323
+ makeArray(
324
+ isString(pattern) ? splitPattern(pattern) : pattern
325
+ ).forEach(this._add, this);
326
+ return this._added;
327
+ }
328
+ // Test one single path without recursively checking parent directories
329
+ //
330
+ // - checkUnignored `boolean` whether should check if the path is unignored,
331
+ // setting `checkUnignored` to `false` could reduce additional
332
+ // path matching.
333
+ // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
334
+ // @returns {TestResult} true if a file is ignored
335
+ test(path9, checkUnignored, mode) {
336
+ let ignored = false;
337
+ let unignored = false;
338
+ let matchedRule;
339
+ this._rules.forEach((rule) => {
340
+ const { negative } = rule;
341
+ if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
342
+ return;
343
+ }
344
+ const matched = rule[mode].test(path9);
345
+ if (!matched) {
346
+ return;
347
+ }
348
+ ignored = !negative;
349
+ unignored = negative;
350
+ matchedRule = negative ? UNDEFINED : rule;
351
+ });
352
+ const ret = {
353
+ ignored,
354
+ unignored
355
+ };
356
+ if (matchedRule) {
357
+ ret.rule = matchedRule;
358
+ }
359
+ return ret;
360
+ }
361
+ };
362
+ var throwError = (message, Ctor) => {
363
+ throw new Ctor(message);
364
+ };
365
+ var checkPath = (path9, originalPath, doThrow) => {
366
+ if (!isString(path9)) {
367
+ return doThrow(
368
+ `path must be a string, but got \`${originalPath}\``,
369
+ TypeError
370
+ );
371
+ }
372
+ if (!path9) {
373
+ return doThrow(`path must not be empty`, TypeError);
374
+ }
375
+ if (checkPath.isNotRelative(path9)) {
376
+ const r = "`path.relative()`d";
377
+ return doThrow(
378
+ `path should be a ${r} string, but got "${originalPath}"`,
379
+ RangeError
380
+ );
381
+ }
382
+ return true;
383
+ };
384
+ var isNotRelative = (path9) => REGEX_TEST_INVALID_PATH.test(path9);
385
+ checkPath.isNotRelative = isNotRelative;
386
+ checkPath.convert = (p) => p;
387
+ var Ignore2 = class {
388
+ constructor({
389
+ ignorecase = true,
390
+ ignoreCase = ignorecase,
391
+ allowRelativePaths = false
392
+ } = {}) {
393
+ define(this, KEY_IGNORE, true);
394
+ this._rules = new RuleManager(ignoreCase);
395
+ this._strictPathCheck = !allowRelativePaths;
396
+ this._initCache();
397
+ }
398
+ _initCache() {
399
+ this._ignoreCache = /* @__PURE__ */ Object.create(null);
400
+ this._testCache = /* @__PURE__ */ Object.create(null);
401
+ }
402
+ add(pattern) {
403
+ if (this._rules.add(pattern)) {
404
+ this._initCache();
405
+ }
406
+ return this;
407
+ }
408
+ // legacy
409
+ addPattern(pattern) {
410
+ return this.add(pattern);
411
+ }
412
+ // @returns {TestResult}
413
+ _test(originalPath, cache, checkUnignored, slices) {
414
+ const path9 = originalPath && checkPath.convert(originalPath);
415
+ checkPath(
416
+ path9,
417
+ originalPath,
418
+ this._strictPathCheck ? throwError : RETURN_FALSE
419
+ );
420
+ return this._t(path9, cache, checkUnignored, slices);
421
+ }
422
+ checkIgnore(path9) {
423
+ if (!REGEX_TEST_TRAILING_SLASH.test(path9)) {
424
+ return this.test(path9);
425
+ }
426
+ const slices = path9.split(SLASH).filter(Boolean);
427
+ slices.pop();
428
+ if (slices.length) {
429
+ const parent = this._t(
430
+ slices.join(SLASH) + SLASH,
431
+ this._testCache,
432
+ true,
433
+ slices
434
+ );
435
+ if (parent.ignored) {
436
+ return parent;
437
+ }
438
+ }
439
+ return this._rules.test(path9, false, MODE_CHECK_IGNORE);
440
+ }
441
+ _t(path9, cache, checkUnignored, slices) {
442
+ if (path9 in cache) {
443
+ return cache[path9];
444
+ }
445
+ if (!slices) {
446
+ slices = path9.split(SLASH).filter(Boolean);
447
+ }
448
+ slices.pop();
449
+ if (!slices.length) {
450
+ return cache[path9] = this._rules.test(path9, checkUnignored, MODE_IGNORE);
451
+ }
452
+ const parent = this._t(
453
+ slices.join(SLASH) + SLASH,
454
+ cache,
455
+ checkUnignored,
456
+ slices
457
+ );
458
+ return cache[path9] = parent.ignored ? parent : this._rules.test(path9, checkUnignored, MODE_IGNORE);
459
+ }
460
+ ignores(path9) {
461
+ return this._test(path9, this._ignoreCache, false).ignored;
462
+ }
463
+ createFilter() {
464
+ return (path9) => !this.ignores(path9);
465
+ }
466
+ filter(paths) {
467
+ return makeArray(paths).filter(this.createFilter());
468
+ }
469
+ // @returns {TestResult}
470
+ test(path9) {
471
+ return this._test(path9, this._testCache, true);
472
+ }
473
+ };
474
+ var factory = (options) => new Ignore2(options);
475
+ var isPathValid = (path9) => checkPath(path9 && checkPath.convert(path9), path9, RETURN_FALSE);
476
+ var setupWindows = () => {
477
+ const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
478
+ checkPath.convert = makePosix;
479
+ const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
480
+ checkPath.isNotRelative = (path9) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path9) || isNotRelative(path9);
481
+ };
482
+ if (
483
+ // Detect `process` so that it can run in browsers.
484
+ typeof process !== "undefined" && process.platform === "win32"
485
+ ) {
486
+ setupWindows();
487
+ }
488
+ module.exports = factory;
489
+ factory.default = factory;
490
+ module.exports.isPathValid = isPathValid;
491
+ define(module.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
492
+ }
493
+ });
7
494
 
8
495
  // src/index.ts
9
496
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -487,8 +974,8 @@ function getErrorMap() {
487
974
 
488
975
  // node_modules/zod/v3/helpers/parseUtil.js
489
976
  var makeIssue = (params) => {
490
- const { data, path: path8, errorMaps, issueData } = params;
491
- const fullPath = [...path8, ...issueData.path || []];
977
+ const { data, path: path9, errorMaps, issueData } = params;
978
+ const fullPath = [...path9, ...issueData.path || []];
492
979
  const fullIssue = {
493
980
  ...issueData,
494
981
  path: fullPath
@@ -604,11 +1091,11 @@ var errorUtil;
604
1091
 
605
1092
  // node_modules/zod/v3/types.js
606
1093
  var ParseInputLazyPath = class {
607
- constructor(parent, value, path8, key) {
1094
+ constructor(parent, value, path9, key) {
608
1095
  this._cachedPath = [];
609
1096
  this.parent = parent;
610
1097
  this.data = value;
611
- this._path = path8;
1098
+ this._path = path9;
612
1099
  this._key = key;
613
1100
  }
614
1101
  get path() {
@@ -4195,7 +4682,8 @@ var configSchema = external_exports.object({
4195
4682
  defaultProjectId: external_exports.string().uuid().optional(),
4196
4683
  userAgent: external_exports.string().default(`contextstream-mcp/${VERSION}`),
4197
4684
  allowHeaderAuth: external_exports.boolean().optional(),
4198
- contextPackEnabled: external_exports.boolean().default(true)
4685
+ contextPackEnabled: external_exports.boolean().default(true),
4686
+ showTiming: external_exports.boolean().default(false)
4199
4687
  });
4200
4688
  var MISSING_CREDENTIALS_ERROR = "Set CONTEXTSTREAM_API_KEY or CONTEXTSTREAM_JWT for authentication (or CONTEXTSTREAM_ALLOW_HEADER_AUTH=true for header-based auth).";
4201
4689
  function isMissingCredentialsError(err) {
@@ -4209,6 +4697,7 @@ function loadConfig() {
4209
4697
  const contextPackEnabled = parseBooleanEnv(
4210
4698
  process.env.CONTEXTSTREAM_CONTEXT_PACK ?? process.env.CONTEXTSTREAM_CONTEXT_PACK_ENABLED
4211
4699
  );
4700
+ const showTiming = parseBooleanEnv(process.env.CONTEXTSTREAM_SHOW_TIMING);
4212
4701
  const parsed = configSchema.safeParse({
4213
4702
  apiUrl: process.env.CONTEXTSTREAM_API_URL,
4214
4703
  apiKey: process.env.CONTEXTSTREAM_API_KEY,
@@ -4217,7 +4706,8 @@ function loadConfig() {
4217
4706
  defaultProjectId: process.env.CONTEXTSTREAM_PROJECT_ID,
4218
4707
  userAgent: process.env.CONTEXTSTREAM_USER_AGENT,
4219
4708
  allowHeaderAuth,
4220
- contextPackEnabled
4709
+ contextPackEnabled,
4710
+ showTiming
4221
4711
  });
4222
4712
  if (!parsed.success) {
4223
4713
  const missing = parsed.error.errors.map((e) => e.path.join(".")).join(", ");
@@ -4233,7 +4723,7 @@ function loadConfig() {
4233
4723
 
4234
4724
  // src/client.ts
4235
4725
  import { randomUUID } from "node:crypto";
4236
- import * as path3 from "node:path";
4726
+ import * as path4 from "node:path";
4237
4727
 
4238
4728
  // src/auth-context.ts
4239
4729
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -4299,12 +4789,12 @@ var BASE_DELAY = 1e3;
4299
4789
  async function sleep(ms) {
4300
4790
  return new Promise((resolve4) => setTimeout(resolve4, ms));
4301
4791
  }
4302
- async function request(config, path8, options = {}) {
4792
+ async function request(config, path9, options = {}) {
4303
4793
  const { apiUrl, userAgent } = config;
4304
4794
  const authOverride = getAuthOverride();
4305
4795
  const apiKey = authOverride?.apiKey ?? config.apiKey;
4306
4796
  const jwt = authOverride?.jwt ?? config.jwt;
4307
- const apiPath = path8.startsWith("/api/") ? path8 : `/api/v1${path8}`;
4797
+ const apiPath = path9.startsWith("/api/") ? path9 : `/api/v1${path9}`;
4308
4798
  const url = `${apiUrl.replace(/\/$/, "")}${apiPath}`;
4309
4799
  const maxRetries = options.retries ?? MAX_RETRIES;
4310
4800
  const baseDelay = options.retryDelay ?? BASE_DELAY;
@@ -4450,9 +4940,9 @@ function extractErrorCode(payload) {
4450
4940
  if (typeof payload.code === "string" && payload.code.trim()) return payload.code.trim();
4451
4941
  return null;
4452
4942
  }
4453
- function detectIntegrationProvider(path8) {
4454
- if (/\/github(\/|$)/i.test(path8)) return "github";
4455
- if (/\/slack(\/|$)/i.test(path8)) return "slack";
4943
+ function detectIntegrationProvider(path9) {
4944
+ if (/\/github(\/|$)/i.test(path9)) return "github";
4945
+ if (/\/slack(\/|$)/i.test(path9)) return "slack";
4456
4946
  return null;
4457
4947
  }
4458
4948
  function rewriteNotFoundMessage(input) {
@@ -4465,8 +4955,81 @@ function rewriteNotFoundMessage(input) {
4465
4955
  }
4466
4956
 
4467
4957
  // src/files.ts
4958
+ import * as fs2 from "fs";
4959
+ import * as path2 from "path";
4960
+
4961
+ // src/ignore.ts
4962
+ var import_ignore = __toESM(require_ignore(), 1);
4468
4963
  import * as fs from "fs";
4469
4964
  import * as path from "path";
4965
+ var IGNORE_FILENAME = ".contextstream/ignore";
4966
+ var DEFAULT_IGNORE_PATTERNS = [
4967
+ // Version control
4968
+ ".git/",
4969
+ ".svn/",
4970
+ ".hg/",
4971
+ // Package managers / dependencies
4972
+ "node_modules/",
4973
+ "vendor/",
4974
+ ".pnpm/",
4975
+ // Build outputs
4976
+ "target/",
4977
+ "dist/",
4978
+ "build/",
4979
+ "out/",
4980
+ ".next/",
4981
+ ".nuxt/",
4982
+ // Python
4983
+ "__pycache__/",
4984
+ ".pytest_cache/",
4985
+ ".mypy_cache/",
4986
+ "venv/",
4987
+ ".venv/",
4988
+ "env/",
4989
+ ".env/",
4990
+ // IDE
4991
+ ".idea/",
4992
+ ".vscode/",
4993
+ ".vs/",
4994
+ // Coverage
4995
+ "coverage/",
4996
+ ".coverage/",
4997
+ // Lock files
4998
+ "package-lock.json",
4999
+ "yarn.lock",
5000
+ "pnpm-lock.yaml",
5001
+ "Cargo.lock",
5002
+ "poetry.lock",
5003
+ "Gemfile.lock",
5004
+ "composer.lock",
5005
+ // OS files
5006
+ ".DS_Store",
5007
+ "Thumbs.db"
5008
+ ];
5009
+ async function loadIgnorePatterns(projectRoot) {
5010
+ const ig = (0, import_ignore.default)();
5011
+ const patterns = [...DEFAULT_IGNORE_PATTERNS];
5012
+ ig.add(DEFAULT_IGNORE_PATTERNS);
5013
+ const ignoreFilePath = path.join(projectRoot, IGNORE_FILENAME);
5014
+ let hasUserPatterns = false;
5015
+ try {
5016
+ const content = await fs.promises.readFile(ignoreFilePath, "utf-8");
5017
+ const userPatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
5018
+ if (userPatterns.length > 0) {
5019
+ ig.add(userPatterns);
5020
+ patterns.push(...userPatterns);
5021
+ hasUserPatterns = true;
5022
+ }
5023
+ } catch {
5024
+ }
5025
+ return {
5026
+ ignores: (pathname) => ig.ignores(pathname),
5027
+ patterns,
5028
+ hasUserPatterns
5029
+ };
5030
+ }
5031
+
5032
+ // src/files.ts
4470
5033
  var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
4471
5034
  // Rust
4472
5035
  "rs",
@@ -4577,31 +5140,34 @@ var MAX_FILES_PER_BATCH = 200;
4577
5140
  async function* readAllFilesInBatches(rootPath, options = {}) {
4578
5141
  const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
4579
5142
  const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
4580
- const maxFilesPerBatch = options.maxFilesPerBatch ?? MAX_FILES_PER_BATCH;
5143
+ const maxFilesPerBatch = options.maxFilesPerBatch ?? options.batchSize ?? MAX_FILES_PER_BATCH;
4581
5144
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
5145
+ const ig = options.ignoreInstance ?? await loadIgnorePatterns(rootPath);
4582
5146
  let batch = [];
4583
5147
  let currentBatchBytes = 0;
4584
5148
  async function* walkDir(dir, relativePath = "") {
4585
5149
  let entries;
4586
5150
  try {
4587
- entries = await fs.promises.readdir(dir, { withFileTypes: true });
5151
+ entries = await fs2.promises.readdir(dir, { withFileTypes: true });
4588
5152
  } catch {
4589
5153
  return;
4590
5154
  }
4591
5155
  for (const entry of entries) {
4592
- const fullPath = path.join(dir, entry.name);
4593
- const relPath = path.join(relativePath, entry.name);
5156
+ const fullPath = path2.join(dir, entry.name);
5157
+ const relPath = path2.join(relativePath, entry.name);
4594
5158
  if (entry.isDirectory()) {
4595
5159
  if (IGNORE_DIRS.has(entry.name)) continue;
5160
+ if (ig.ignores(relPath + "/")) continue;
4596
5161
  yield* walkDir(fullPath, relPath);
4597
5162
  } else if (entry.isFile()) {
4598
5163
  if (IGNORE_FILES.has(entry.name)) continue;
5164
+ if (ig.ignores(relPath)) continue;
4599
5165
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
4600
5166
  if (!CODE_EXTENSIONS.has(ext)) continue;
4601
5167
  try {
4602
- const stat2 = await fs.promises.stat(fullPath);
5168
+ const stat2 = await fs2.promises.stat(fullPath);
4603
5169
  if (stat2.size > maxFileSize) continue;
4604
- const content = await fs.promises.readFile(fullPath, "utf-8");
5170
+ const content = await fs2.promises.readFile(fullPath, "utf-8");
4605
5171
  yield { path: relPath, content, sizeBytes: stat2.size };
4606
5172
  } catch {
4607
5173
  }
@@ -4637,9 +5203,10 @@ async function* readAllFilesInBatches(rootPath, options = {}) {
4637
5203
  async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}) {
4638
5204
  const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
4639
5205
  const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
4640
- const maxFilesPerBatch = options.maxFilesPerBatch ?? MAX_FILES_PER_BATCH;
5206
+ const maxFilesPerBatch = options.maxFilesPerBatch ?? options.batchSize ?? MAX_FILES_PER_BATCH;
4641
5207
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
4642
5208
  const sinceMs = sinceTimestamp.getTime();
5209
+ const ig = options.ignoreInstance ?? await loadIgnorePatterns(rootPath);
4643
5210
  let batch = [];
4644
5211
  let currentBatchBytes = 0;
4645
5212
  let filesScanned = 0;
@@ -4647,26 +5214,28 @@ async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}
4647
5214
  async function* walkDir(dir, relativePath = "") {
4648
5215
  let entries;
4649
5216
  try {
4650
- entries = await fs.promises.readdir(dir, { withFileTypes: true });
5217
+ entries = await fs2.promises.readdir(dir, { withFileTypes: true });
4651
5218
  } catch {
4652
5219
  return;
4653
5220
  }
4654
5221
  for (const entry of entries) {
4655
- const fullPath = path.join(dir, entry.name);
4656
- const relPath = path.join(relativePath, entry.name);
5222
+ const fullPath = path2.join(dir, entry.name);
5223
+ const relPath = path2.join(relativePath, entry.name);
4657
5224
  if (entry.isDirectory()) {
4658
5225
  if (IGNORE_DIRS.has(entry.name)) continue;
5226
+ if (ig.ignores(relPath + "/")) continue;
4659
5227
  yield* walkDir(fullPath, relPath);
4660
5228
  } else if (entry.isFile()) {
4661
5229
  if (IGNORE_FILES.has(entry.name)) continue;
5230
+ if (ig.ignores(relPath)) continue;
4662
5231
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
4663
5232
  if (!CODE_EXTENSIONS.has(ext)) continue;
4664
5233
  try {
4665
- const stat2 = await fs.promises.stat(fullPath);
5234
+ const stat2 = await fs2.promises.stat(fullPath);
4666
5235
  filesScanned++;
4667
5236
  if (stat2.mtimeMs <= sinceMs) continue;
4668
5237
  if (stat2.size > maxFileSize) continue;
4669
- const content = await fs.promises.readFile(fullPath, "utf-8");
5238
+ const content = await fs2.promises.readFile(fullPath, "utf-8");
4670
5239
  filesChanged++;
4671
5240
  yield { path: relPath, content, sizeBytes: stat2.size };
4672
5241
  } catch {
@@ -4706,16 +5275,17 @@ async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}
4706
5275
  async function countIndexableFiles(rootPath, options = {}) {
4707
5276
  const maxFiles = options.maxFiles ?? 1;
4708
5277
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
5278
+ const ig = options.ignoreInstance ?? await loadIgnorePatterns(rootPath);
4709
5279
  let count = 0;
4710
5280
  let stopped = false;
4711
- async function walkDir(dir) {
5281
+ async function walkDir(dir, relativePath = "") {
4712
5282
  if (count >= maxFiles) {
4713
5283
  stopped = true;
4714
5284
  return;
4715
5285
  }
4716
5286
  let entries;
4717
5287
  try {
4718
- entries = await fs.promises.readdir(dir, { withFileTypes: true });
5288
+ entries = await fs2.promises.readdir(dir, { withFileTypes: true });
4719
5289
  } catch {
4720
5290
  return;
4721
5291
  }
@@ -4724,16 +5294,19 @@ async function countIndexableFiles(rootPath, options = {}) {
4724
5294
  stopped = true;
4725
5295
  return;
4726
5296
  }
4727
- const fullPath = path.join(dir, entry.name);
5297
+ const fullPath = path2.join(dir, entry.name);
5298
+ const relPath = path2.join(relativePath, entry.name);
4728
5299
  if (entry.isDirectory()) {
4729
5300
  if (IGNORE_DIRS.has(entry.name)) continue;
4730
- await walkDir(fullPath);
5301
+ if (ig.ignores(relPath + "/")) continue;
5302
+ await walkDir(fullPath, relPath);
4731
5303
  } else if (entry.isFile()) {
4732
5304
  if (IGNORE_FILES.has(entry.name)) continue;
5305
+ if (ig.ignores(relPath)) continue;
4733
5306
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
4734
5307
  if (!CODE_EXTENSIONS.has(ext)) continue;
4735
5308
  try {
4736
- const stat2 = await fs.promises.stat(fullPath);
5309
+ const stat2 = await fs2.promises.stat(fullPath);
4737
5310
  if (stat2.size > maxFileSize) continue;
4738
5311
  count++;
4739
5312
  if (count >= maxFiles) {
@@ -4750,16 +5323,16 @@ async function countIndexableFiles(rootPath, options = {}) {
4750
5323
  }
4751
5324
 
4752
5325
  // src/workspace-config.ts
4753
- import * as fs2 from "fs";
4754
- import * as path2 from "path";
5326
+ import * as fs3 from "fs";
5327
+ import * as path3 from "path";
4755
5328
  var CONFIG_DIR = ".contextstream";
4756
5329
  var CONFIG_FILE = "config.json";
4757
5330
  var GLOBAL_MAPPINGS_FILE = ".contextstream-mappings.json";
4758
5331
  function readLocalConfig(repoPath) {
4759
- const configPath = path2.join(repoPath, CONFIG_DIR, CONFIG_FILE);
5332
+ const configPath = path3.join(repoPath, CONFIG_DIR, CONFIG_FILE);
4760
5333
  try {
4761
- if (fs2.existsSync(configPath)) {
4762
- const content = fs2.readFileSync(configPath, "utf-8");
5334
+ if (fs3.existsSync(configPath)) {
5335
+ const content = fs3.readFileSync(configPath, "utf-8");
4763
5336
  return JSON.parse(content);
4764
5337
  }
4765
5338
  } catch (e) {
@@ -4768,13 +5341,13 @@ function readLocalConfig(repoPath) {
4768
5341
  return null;
4769
5342
  }
4770
5343
  function writeLocalConfig(repoPath, config) {
4771
- const configDir = path2.join(repoPath, CONFIG_DIR);
4772
- const configPath = path2.join(configDir, CONFIG_FILE);
5344
+ const configDir = path3.join(repoPath, CONFIG_DIR);
5345
+ const configPath = path3.join(configDir, CONFIG_FILE);
4773
5346
  try {
4774
- if (!fs2.existsSync(configDir)) {
4775
- fs2.mkdirSync(configDir, { recursive: true });
5347
+ if (!fs3.existsSync(configDir)) {
5348
+ fs3.mkdirSync(configDir, { recursive: true });
4776
5349
  }
4777
- fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
5350
+ fs3.writeFileSync(configPath, JSON.stringify(config, null, 2));
4778
5351
  return true;
4779
5352
  } catch (e) {
4780
5353
  console.error(`Failed to write config to ${configPath}:`, e);
@@ -4783,10 +5356,10 @@ function writeLocalConfig(repoPath, config) {
4783
5356
  }
4784
5357
  function readGlobalMappings() {
4785
5358
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
4786
- const mappingsPath = path2.join(homeDir, GLOBAL_MAPPINGS_FILE);
5359
+ const mappingsPath = path3.join(homeDir, GLOBAL_MAPPINGS_FILE);
4787
5360
  try {
4788
- if (fs2.existsSync(mappingsPath)) {
4789
- const content = fs2.readFileSync(mappingsPath, "utf-8");
5361
+ if (fs3.existsSync(mappingsPath)) {
5362
+ const content = fs3.readFileSync(mappingsPath, "utf-8");
4790
5363
  return JSON.parse(content);
4791
5364
  }
4792
5365
  } catch (e) {
@@ -4796,9 +5369,9 @@ function readGlobalMappings() {
4796
5369
  }
4797
5370
  function writeGlobalMappings(mappings) {
4798
5371
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
4799
- const mappingsPath = path2.join(homeDir, GLOBAL_MAPPINGS_FILE);
5372
+ const mappingsPath = path3.join(homeDir, GLOBAL_MAPPINGS_FILE);
4800
5373
  try {
4801
- fs2.writeFileSync(mappingsPath, JSON.stringify(mappings, null, 2));
5374
+ fs3.writeFileSync(mappingsPath, JSON.stringify(mappings, null, 2));
4802
5375
  return true;
4803
5376
  } catch (e) {
4804
5377
  console.error(`Failed to write global mappings:`, e);
@@ -4806,20 +5379,20 @@ function writeGlobalMappings(mappings) {
4806
5379
  }
4807
5380
  }
4808
5381
  function addGlobalMapping(mapping) {
4809
- const normalizedPattern = path2.normalize(mapping.pattern);
5382
+ const normalizedPattern = path3.normalize(mapping.pattern);
4810
5383
  const mappings = readGlobalMappings();
4811
- const filtered = mappings.filter((m) => path2.normalize(m.pattern) !== normalizedPattern);
5384
+ const filtered = mappings.filter((m) => path3.normalize(m.pattern) !== normalizedPattern);
4812
5385
  filtered.push({ ...mapping, pattern: normalizedPattern });
4813
5386
  return writeGlobalMappings(filtered);
4814
5387
  }
4815
5388
  function findMatchingMapping(repoPath) {
4816
5389
  const mappings = readGlobalMappings();
4817
- const normalizedRepo = path2.normalize(repoPath);
5390
+ const normalizedRepo = path3.normalize(repoPath);
4818
5391
  for (const mapping of mappings) {
4819
- const normalizedPattern = path2.normalize(mapping.pattern);
4820
- if (normalizedPattern.endsWith(`${path2.sep}*`)) {
5392
+ const normalizedPattern = path3.normalize(mapping.pattern);
5393
+ if (normalizedPattern.endsWith(`${path3.sep}*`)) {
4821
5394
  const parentDir = normalizedPattern.slice(0, -2);
4822
- if (normalizedRepo.startsWith(parentDir + path2.sep)) {
5395
+ if (normalizedRepo.startsWith(parentDir + path3.sep)) {
4823
5396
  return mapping;
4824
5397
  }
4825
5398
  } else if (normalizedRepo === normalizedPattern) {
@@ -5008,6 +5581,47 @@ var INGEST_BENEFITS = [
5008
5581
  "Allow the AI assistant to find relevant code without manual file navigation",
5009
5582
  "Build a searchable knowledge base of your codebase structure"
5010
5583
  ];
5584
+ var PROJECT_MARKERS = [
5585
+ ".git",
5586
+ "package.json",
5587
+ "Cargo.toml",
5588
+ "pyproject.toml",
5589
+ "go.mod",
5590
+ "pom.xml",
5591
+ "build.gradle",
5592
+ "Gemfile",
5593
+ "composer.json",
5594
+ ".contextstream"
5595
+ ];
5596
+ function isMultiProjectFolder(folderPath) {
5597
+ try {
5598
+ const fs8 = __require("fs");
5599
+ const pathModule = __require("path");
5600
+ const rootHasGit = fs8.existsSync(pathModule.join(folderPath, ".git"));
5601
+ const entries = fs8.readdirSync(folderPath, { withFileTypes: true });
5602
+ const subdirs = entries.filter(
5603
+ (e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules"
5604
+ );
5605
+ const projectSubdirs = [];
5606
+ for (const subdir of subdirs) {
5607
+ const subdirPath = pathModule.join(folderPath, subdir.name);
5608
+ for (const marker of PROJECT_MARKERS) {
5609
+ if (fs8.existsSync(pathModule.join(subdirPath, marker))) {
5610
+ projectSubdirs.push(subdir.name);
5611
+ break;
5612
+ }
5613
+ }
5614
+ }
5615
+ const isMultiProject = projectSubdirs.length >= 1 && (!rootHasGit || projectSubdirs.length >= 2);
5616
+ return {
5617
+ isMultiProject,
5618
+ projectCount: projectSubdirs.length,
5619
+ projectNames: projectSubdirs
5620
+ };
5621
+ } catch {
5622
+ return { isMultiProject: false, projectCount: 0, projectNames: [] };
5623
+ }
5624
+ }
5011
5625
  var ContextStreamClient = class {
5012
5626
  constructor(config) {
5013
5627
  this.config = config;
@@ -5922,7 +6536,7 @@ var ContextStreamClient = class {
5922
6536
  context.workspace_source = resolved.source;
5923
6537
  context.workspace_resolved_from = resolved.source === "local_config" ? `${rootPath}/.contextstream/config.json` : "parent_folder_mapping";
5924
6538
  } else {
5925
- const folderName = rootPath ? path3.basename(rootPath).toLowerCase() : "";
6539
+ const folderName = rootPath ? path4.basename(rootPath).toLowerCase() : "";
5926
6540
  try {
5927
6541
  const workspaces = await this.listWorkspaces({ page_size: 50 });
5928
6542
  if (workspaces.items && workspaces.items.length > 0) {
@@ -5978,13 +6592,13 @@ var ContextStreamClient = class {
5978
6592
  name: w.name,
5979
6593
  description: w.description
5980
6594
  }));
5981
- context.message = `New folder detected: "${rootPath ? path3.basename(rootPath) : "this folder"}". Please select which workspace this belongs to, or create a new one.`;
6595
+ context.message = `New folder detected: "${rootPath ? path4.basename(rootPath) : "this folder"}". Please select which workspace this belongs to, or create a new one.`;
5982
6596
  context.ide_roots = ideRoots;
5983
- context.folder_name = rootPath ? path3.basename(rootPath) : void 0;
6597
+ context.folder_name = rootPath ? path4.basename(rootPath) : void 0;
5984
6598
  return context;
5985
6599
  }
5986
6600
  } else {
5987
- const folderDisplayName = rootPath ? path3.basename(rootPath) || "this folder" : "this folder";
6601
+ const folderDisplayName = rootPath ? path4.basename(rootPath) || "this folder" : "this folder";
5988
6602
  context.status = "requires_workspace_name";
5989
6603
  context.workspace_source = "none_found";
5990
6604
  context.ide_roots = ideRoots;
@@ -6012,7 +6626,7 @@ var ContextStreamClient = class {
6012
6626
  }
6013
6627
  }
6014
6628
  if (!workspaceId && !params.allow_no_workspace) {
6015
- const folderDisplayName = rootPath ? path3.basename(rootPath) || "this folder" : "this folder";
6629
+ const folderDisplayName = rootPath ? path4.basename(rootPath) || "this folder" : "this folder";
6016
6630
  context.ide_roots = ideRoots;
6017
6631
  context.folder_name = folderDisplayName;
6018
6632
  if (rootPath) {
@@ -6043,8 +6657,26 @@ var ContextStreamClient = class {
6043
6657
  return context;
6044
6658
  }
6045
6659
  }
6046
- if (!projectId && workspaceId && rootPath && params.auto_index !== false) {
6047
- const projectName = path3.basename(rootPath) || "My Project";
6660
+ let autoDetectedMultiProject = false;
6661
+ if (!params.skip_project_creation && !projectId && rootPath) {
6662
+ const detection = isMultiProjectFolder(rootPath);
6663
+ if (detection.isMultiProject) {
6664
+ autoDetectedMultiProject = true;
6665
+ context.workspace_only_mode = true;
6666
+ context.auto_detected_multi_project = true;
6667
+ context.detected_projects = detection.projectNames;
6668
+ context.project_skipped_reason = `Auto-detected ${detection.projectCount} projects in folder: ${detection.projectNames.slice(0, 5).join(", ")}${detection.projectCount > 5 ? "..." : ""}. Working at workspace level.`;
6669
+ console.error(
6670
+ `[ContextStream] Auto-detected multi-project folder with ${detection.projectCount} projects: ${detection.projectNames.slice(0, 5).join(", ")}`
6671
+ );
6672
+ }
6673
+ }
6674
+ if (params.skip_project_creation) {
6675
+ context.workspace_only_mode = true;
6676
+ context.project_skipped_reason = "skip_project_creation=true - working at workspace level for multi-project folder";
6677
+ } else if (autoDetectedMultiProject) {
6678
+ } else if (!projectId && workspaceId && rootPath && params.auto_index !== false) {
6679
+ const projectName = path4.basename(rootPath) || "My Project";
6048
6680
  try {
6049
6681
  const projects = await this.listProjects({ workspace_id: workspaceId });
6050
6682
  const projectNameLower = projectName.toLowerCase();
@@ -6354,16 +6986,21 @@ var ContextStreamClient = class {
6354
6986
  * Persists the selection to .contextstream/config.json for future sessions.
6355
6987
  */
6356
6988
  async associateWorkspace(params) {
6357
- const { folder_path, workspace_id, workspace_name, create_parent_mapping } = params;
6989
+ const { folder_path, workspace_id, workspace_name, create_parent_mapping, version, configured_editors, context_pack, api_url } = params;
6358
6990
  const saved = writeLocalConfig(folder_path, {
6359
6991
  workspace_id,
6360
6992
  workspace_name,
6361
- associated_at: (/* @__PURE__ */ new Date()).toISOString()
6993
+ associated_at: (/* @__PURE__ */ new Date()).toISOString(),
6994
+ version,
6995
+ configured_editors,
6996
+ context_pack,
6997
+ api_url,
6998
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
6362
6999
  });
6363
7000
  if (create_parent_mapping) {
6364
- const parentDir = path3.dirname(folder_path);
7001
+ const parentDir = path4.dirname(folder_path);
6365
7002
  addGlobalMapping({
6366
- pattern: path3.join(parentDir, "*"),
7003
+ pattern: path4.join(parentDir, "*"),
6367
7004
  workspace_id,
6368
7005
  workspace_name: workspace_name || "Unknown"
6369
7006
  });
@@ -6869,9 +7506,9 @@ var ContextStreamClient = class {
6869
7506
  candidateParts.push("## Relevant Code\n");
6870
7507
  currentChars += 18;
6871
7508
  const codeEntries = code.results.map((c) => {
6872
- const path8 = c.file_path || "file";
7509
+ const path9 = c.file_path || "file";
6873
7510
  const content = c.content?.slice(0, 150) || "";
6874
- return { path: path8, entry: `\u2022 ${path8}: ${content}...
7511
+ return { path: path9, entry: `\u2022 ${path9}: ${content}...
6875
7512
  ` };
6876
7513
  });
6877
7514
  for (const c of codeEntries) {
@@ -7029,7 +7666,10 @@ var ContextStreamClient = class {
7029
7666
  distill: params.distill,
7030
7667
  client_version: VERSION,
7031
7668
  rules_version: VERSION,
7032
- notice_inline: false
7669
+ notice_inline: false,
7670
+ // Session token tracking for context pressure
7671
+ ...params.session_tokens !== void 0 && { session_tokens: params.session_tokens },
7672
+ ...params.context_threshold !== void 0 && { context_threshold: params.context_threshold }
7033
7673
  }
7034
7674
  });
7035
7675
  const data = unwrapApiResponse(apiResult);
@@ -7049,7 +7689,8 @@ var ContextStreamClient = class {
7049
7689
  project_id: withDefaults.project_id,
7050
7690
  ...versionNotice2 ? { version_notice: versionNotice2 } : {},
7051
7691
  ...Array.isArray(data?.errors) ? { errors: data.errors } : {},
7052
- ...this.indexRefreshInProgress ? { index_status: "refreshing" } : {}
7692
+ ...this.indexRefreshInProgress ? { index_status: "refreshing" } : {},
7693
+ ...data?.context_pressure ? { context_pressure: data.context_pressure } : {}
7053
7694
  };
7054
7695
  } catch (err) {
7055
7696
  const message2 = err instanceof Error ? err.message : String(err);
@@ -8192,8 +8833,8 @@ W:${wsHint}
8192
8833
  };
8193
8834
 
8194
8835
  // src/tools.ts
8195
- import * as fs4 from "node:fs";
8196
- import * as path5 from "node:path";
8836
+ import * as fs5 from "node:fs";
8837
+ import * as path6 from "node:path";
8197
8838
  import { homedir as homedir3 } from "node:os";
8198
8839
 
8199
8840
  // src/rules-templates.ts
@@ -8417,6 +9058,39 @@ If context still feels missing, use \`session(action="recall", query="...")\` fo
8417
9058
 
8418
9059
  ---
8419
9060
 
9061
+ ### Context Pressure & Compaction Awareness
9062
+
9063
+ ContextStream tracks context pressure to help you stay ahead of conversation compaction:
9064
+
9065
+ **Automatic tracking:** Token usage is tracked automatically. \`context_smart\` returns \`context_pressure\` when usage is high.
9066
+
9067
+ **When \`context_smart\` returns \`context_pressure\` with high/critical level:**
9068
+ 1. Review the \`suggested_action\` field:
9069
+ - \`prepare_save\`: Start thinking about saving important state
9070
+ - \`save_now\`: Immediately call \`session(action="capture", event_type="session_snapshot")\` to preserve state
9071
+
9072
+ **PreCompact Hook (Optional):** If enabled, Claude Code will inject a reminder to save state before compaction.
9073
+ Enable with: \`generate_rules(install_hooks=true, include_pre_compact=true)\`
9074
+
9075
+ **Before compaction happens (when warned):**
9076
+ \`\`\`
9077
+ session(action="capture", event_type="session_snapshot", title="Pre-compaction snapshot", content="{
9078
+ \\"conversation_summary\\": \\"<summarize what we've been doing>\\",
9079
+ \\"current_goal\\": \\"<the main task>\\",
9080
+ \\"active_files\\": [\\"file1.ts\\", \\"file2.ts\\"],
9081
+ \\"recent_decisions\\": [{title: \\"...\\", rationale: \\"...\\"}],
9082
+ \\"unfinished_work\\": [{task: \\"...\\", status: \\"...\\", next_steps: \\"...\\"}]
9083
+ }")
9084
+ \`\`\`
9085
+
9086
+ **After compaction (when context seems lost):**
9087
+ 1. Call \`session_init(folder_path="...", is_post_compact=true)\` - this auto-restores the most recent snapshot
9088
+ 2. Or call \`session_restore_context()\` directly to get the saved state
9089
+ 3. Review the \`restored_context\` to understand prior work
9090
+ 4. Acknowledge to the user what was restored and continue
9091
+
9092
+ ---
9093
+
8420
9094
  ### Index Status (Auto-Managed)
8421
9095
 
8422
9096
  **Indexing is automatic.** After \`session_init\`, the project is auto-indexed in the background.
@@ -8650,12 +9324,27 @@ ContextStream search is **indexed** and returns semantic matches + context in ON
8650
9324
  - Before risky work: \`session(action="get_lessons", query="<topic>")\`
8651
9325
  - On mistakes: \`session(action="capture_lesson", title="...", trigger="...", impact="...", prevention="...")\`
8652
9326
 
9327
+ ### Context Pressure & Compaction
9328
+
9329
+ - If \`context_smart\` returns high/critical \`context_pressure\`: call \`session_capture_smart(...)\` to save state
9330
+ - After compaction (context lost): call \`session_init(..., is_post_compact=true)\` or \`session_restore_context()\`
9331
+
8653
9332
  ### Plans & Tasks
8654
9333
 
8655
9334
  When user asks for a plan, use ContextStream (not EnterPlanMode):
8656
9335
  1. \`session(action="capture_plan", title="...", steps=[...])\`
8657
9336
  2. \`memory(action="create_task", title="...", plan_id="<id>")\`
8658
9337
 
9338
+ ### Workspace-Only Mode (Multi-Project Folders)
9339
+
9340
+ If working in a parent folder containing multiple projects:
9341
+ \`\`\`
9342
+ session_init(folder_path="...", skip_project_creation=true)
9343
+ \`\`\`
9344
+
9345
+ This enables workspace-level memory and context without project-specific indexing.
9346
+ Use for monorepos or folders with multiple independent projects.
9347
+
8659
9348
  Full docs: https://contextstream.io/docs/mcp/tools
8660
9349
  `.trim();
8661
9350
  var TEMPLATES = {
@@ -8937,8 +9626,8 @@ function getCoreToolsHint() {
8937
9626
  }
8938
9627
 
8939
9628
  // src/hooks-config.ts
8940
- import * as fs3 from "node:fs/promises";
8941
- import * as path4 from "node:path";
9629
+ import * as fs4 from "node:fs/promises";
9630
+ import * as path5 from "node:path";
8942
9631
  import { homedir as homedir2 } from "node:os";
8943
9632
  var PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
8944
9633
  """
@@ -9108,26 +9797,102 @@ def main():
9108
9797
  print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": REMINDER}}))
9109
9798
  sys.exit(0)
9110
9799
 
9800
+ if __name__ == "__main__":
9801
+ main()
9802
+ `;
9803
+ var PRECOMPACT_HOOK_SCRIPT = `#!/usr/bin/env python3
9804
+ """
9805
+ ContextStream PreCompact Hook for Claude Code
9806
+
9807
+ Runs BEFORE conversation context is compacted (manual via /compact or automatic).
9808
+ Injects a reminder for the AI to save conversation state using session_capture_smart.
9809
+
9810
+ Input (via stdin):
9811
+ {
9812
+ "session_id": "...",
9813
+ "transcript_path": "/path/to/transcript.jsonl",
9814
+ "permission_mode": "default",
9815
+ "hook_event_name": "PreCompact",
9816
+ "trigger": "manual" | "auto",
9817
+ "custom_instructions": "..."
9818
+ }
9819
+
9820
+ Output (to stdout):
9821
+ {
9822
+ "hookSpecificOutput": {
9823
+ "hookEventName": "PreCompact",
9824
+ "additionalContext": "... instructions for AI ..."
9825
+ }
9826
+ }
9827
+ """
9828
+
9829
+ import json
9830
+ import sys
9831
+ import os
9832
+
9833
+ ENABLED = os.environ.get("CONTEXTSTREAM_PRECOMPACT_ENABLED", "true").lower() == "true"
9834
+
9835
+ def main():
9836
+ if not ENABLED:
9837
+ sys.exit(0)
9838
+
9839
+ try:
9840
+ data = json.load(sys.stdin)
9841
+ except:
9842
+ sys.exit(0)
9843
+
9844
+ trigger = data.get("trigger", "unknown")
9845
+ custom_instructions = data.get("custom_instructions", "")
9846
+
9847
+ # Build context injection for the AI
9848
+ context = f"""[CONTEXT COMPACTION IMMINENT - {trigger.upper()}]
9849
+ CRITICAL: Before context is compacted, you MUST save conversation state:
9850
+
9851
+ 1. IMMEDIATELY call: mcp__contextstream__session(action="capture", event_type="session_snapshot", title="Pre-compaction snapshot", content="<JSON with: conversation_summary, active_goals, recent_decisions, active_files, unfinished_work>")
9852
+
9853
+ 2. Include in the snapshot:
9854
+ - conversation_summary: Brief summary of what was discussed
9855
+ - active_goals: List of goals/tasks in progress
9856
+ - recent_decisions: Key decisions made in this session
9857
+ - active_files: Files currently being worked on
9858
+ - unfinished_work: Any incomplete tasks
9859
+
9860
+ 3. After compaction, call session_init(is_post_compact=true) to restore context.
9861
+
9862
+ {f"User instructions: {custom_instructions}" if custom_instructions else ""}
9863
+ [END COMPACTION WARNING]"""
9864
+
9865
+ output = {
9866
+ "hookSpecificOutput": {
9867
+ "hookEventName": "PreCompact",
9868
+ "additionalContext": context
9869
+ }
9870
+ }
9871
+
9872
+ print(json.dumps(output))
9873
+ sys.exit(0)
9874
+
9111
9875
  if __name__ == "__main__":
9112
9876
  main()
9113
9877
  `;
9114
9878
  function getClaudeSettingsPath(scope, projectPath) {
9115
9879
  if (scope === "user") {
9116
- return path4.join(homedir2(), ".claude", "settings.json");
9880
+ return path5.join(homedir2(), ".claude", "settings.json");
9117
9881
  }
9118
9882
  if (!projectPath) {
9119
9883
  throw new Error("projectPath required for project scope");
9120
9884
  }
9121
- return path4.join(projectPath, ".claude", "settings.json");
9885
+ return path5.join(projectPath, ".claude", "settings.json");
9122
9886
  }
9123
9887
  function getHooksDir() {
9124
- return path4.join(homedir2(), ".claude", "hooks");
9888
+ return path5.join(homedir2(), ".claude", "hooks");
9125
9889
  }
9126
- function buildHooksConfig() {
9890
+ function buildHooksConfig(options) {
9127
9891
  const hooksDir = getHooksDir();
9128
- const preToolUsePath = path4.join(hooksDir, "contextstream-redirect.py");
9129
- const userPromptPath = path4.join(hooksDir, "contextstream-reminder.py");
9130
- return {
9892
+ const preToolUsePath = path5.join(hooksDir, "contextstream-redirect.py");
9893
+ const userPromptPath = path5.join(hooksDir, "contextstream-reminder.py");
9894
+ const preCompactPath = path5.join(hooksDir, "contextstream-precompact.py");
9895
+ const config = {
9131
9896
  PreToolUse: [
9132
9897
  {
9133
9898
  matcher: "Glob|Grep|Search|Task|EnterPlanMode",
@@ -9153,20 +9918,45 @@ function buildHooksConfig() {
9153
9918
  }
9154
9919
  ]
9155
9920
  };
9921
+ if (options?.includePreCompact) {
9922
+ config.PreCompact = [
9923
+ {
9924
+ // Match both manual (/compact) and automatic compaction
9925
+ matcher: "*",
9926
+ hooks: [
9927
+ {
9928
+ type: "command",
9929
+ command: `python3 "${preCompactPath}"`,
9930
+ timeout: 10
9931
+ }
9932
+ ]
9933
+ }
9934
+ ];
9935
+ }
9936
+ return config;
9156
9937
  }
9157
- async function installHookScripts() {
9938
+ async function installHookScripts(options) {
9158
9939
  const hooksDir = getHooksDir();
9159
- await fs3.mkdir(hooksDir, { recursive: true });
9160
- const preToolUsePath = path4.join(hooksDir, "contextstream-redirect.py");
9161
- const userPromptPath = path4.join(hooksDir, "contextstream-reminder.py");
9162
- await fs3.writeFile(preToolUsePath, PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
9163
- await fs3.writeFile(userPromptPath, USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
9164
- return { preToolUse: preToolUsePath, userPrompt: userPromptPath };
9940
+ await fs4.mkdir(hooksDir, { recursive: true });
9941
+ const preToolUsePath = path5.join(hooksDir, "contextstream-redirect.py");
9942
+ const userPromptPath = path5.join(hooksDir, "contextstream-reminder.py");
9943
+ const preCompactPath = path5.join(hooksDir, "contextstream-precompact.py");
9944
+ await fs4.writeFile(preToolUsePath, PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
9945
+ await fs4.writeFile(userPromptPath, USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
9946
+ const result = {
9947
+ preToolUse: preToolUsePath,
9948
+ userPrompt: userPromptPath
9949
+ };
9950
+ if (options?.includePreCompact) {
9951
+ await fs4.writeFile(preCompactPath, PRECOMPACT_HOOK_SCRIPT, { mode: 493 });
9952
+ result.preCompact = preCompactPath;
9953
+ }
9954
+ return result;
9165
9955
  }
9166
9956
  async function readClaudeSettings(scope, projectPath) {
9167
9957
  const settingsPath = getClaudeSettingsPath(scope, projectPath);
9168
9958
  try {
9169
- const content = await fs3.readFile(settingsPath, "utf-8");
9959
+ const content = await fs4.readFile(settingsPath, "utf-8");
9170
9960
  return JSON.parse(content);
9171
9961
  } catch {
9172
9962
  return {};
@@ -9174,9 +9964,9 @@ async function readClaudeSettings(scope, projectPath) {
9174
9964
  }
9175
9965
  async function writeClaudeSettings(settings, scope, projectPath) {
9176
9966
  const settingsPath = getClaudeSettingsPath(scope, projectPath);
9177
- const dir = path4.dirname(settingsPath);
9178
- await fs3.mkdir(dir, { recursive: true });
9179
- await fs3.writeFile(settingsPath, JSON.stringify(settings, null, 2));
9967
+ const dir = path5.dirname(settingsPath);
9968
+ await fs4.mkdir(dir, { recursive: true });
9969
+ await fs4.writeFile(settingsPath, JSON.stringify(settings, null, 2));
9180
9970
  }
9181
9971
  function mergeHooksIntoSettings(existingSettings, newHooks) {
9182
9972
  const settings = { ...existingSettings };
@@ -9195,16 +9985,22 @@ function mergeHooksIntoSettings(existingSettings, newHooks) {
9195
9985
  async function installClaudeCodeHooks(options) {
9196
9986
  const result = { scripts: [], settings: [] };
9197
9987
  if (!options.dryRun) {
9198
- const scripts = await installHookScripts();
9988
+ const scripts = await installHookScripts({ includePreCompact: options.includePreCompact });
9199
9989
  result.scripts.push(scripts.preToolUse, scripts.userPrompt);
9990
+ if (scripts.preCompact) {
9991
+ result.scripts.push(scripts.preCompact);
9992
+ }
9200
9993
  } else {
9201
9994
  const hooksDir = getHooksDir();
9202
9995
  result.scripts.push(
9203
- path4.join(hooksDir, "contextstream-redirect.py"),
9204
- path4.join(hooksDir, "contextstream-reminder.py")
9996
+ path5.join(hooksDir, "contextstream-redirect.py"),
9997
+ path5.join(hooksDir, "contextstream-reminder.py")
9205
9998
  );
9999
+ if (options.includePreCompact) {
10000
+ result.scripts.push(path5.join(hooksDir, "contextstream-precompact.py"));
10001
+ }
9206
10002
  }
9207
- const hooksConfig = buildHooksConfig();
10003
+ const hooksConfig = buildHooksConfig({ includePreCompact: options.includePreCompact });
9208
10004
  if (options.scope === "user" || options.scope === "both") {
9209
10005
  const settingsPath = getClaudeSettingsPath("user");
9210
10006
  if (!options.dryRun) {
@@ -9226,12 +10022,12 @@ async function installClaudeCodeHooks(options) {
9226
10022
  return result;
9227
10023
  }
9228
10024
  function getIndexStatusPath() {
9229
- return path4.join(homedir2(), ".contextstream", "indexed-projects.json");
10025
+ return path5.join(homedir2(), ".contextstream", "indexed-projects.json");
9230
10026
  }
9231
10027
  async function readIndexStatus() {
9232
10028
  const statusPath = getIndexStatusPath();
9233
10029
  try {
9234
- const content = await fs3.readFile(statusPath, "utf-8");
10030
+ const content = await fs4.readFile(statusPath, "utf-8");
9235
10031
  return JSON.parse(content);
9236
10032
  } catch {
9237
10033
  return { version: 1, projects: {} };
@@ -9239,13 +10035,13 @@ async function readIndexStatus() {
9239
10035
  }
9240
10036
  async function writeIndexStatus(status) {
9241
10037
  const statusPath = getIndexStatusPath();
9242
- const dir = path4.dirname(statusPath);
9243
- await fs3.mkdir(dir, { recursive: true });
9244
- await fs3.writeFile(statusPath, JSON.stringify(status, null, 2));
10038
+ const dir = path5.dirname(statusPath);
10039
+ await fs4.mkdir(dir, { recursive: true });
10040
+ await fs4.writeFile(statusPath, JSON.stringify(status, null, 2));
9245
10041
  }
9246
10042
  async function markProjectIndexed(projectPath, options) {
9247
10043
  const status = await readIndexStatus();
9248
- const resolvedPath = path4.resolve(projectPath);
10044
+ const resolvedPath = path5.resolve(projectPath);
9249
10045
  status.projects[resolvedPath] = {
9250
10046
  indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
9251
10047
  project_id: options?.project_id,
@@ -9431,15 +10227,15 @@ var RULES_PROJECT_FILES = {
9431
10227
  cursor: ".cursorrules",
9432
10228
  windsurf: ".windsurfrules",
9433
10229
  cline: ".clinerules",
9434
- kilo: path5.join(".kilocode", "rules", "contextstream.md"),
9435
- roo: path5.join(".roo", "rules", "contextstream.md"),
10230
+ kilo: path6.join(".kilocode", "rules", "contextstream.md"),
10231
+ roo: path6.join(".roo", "rules", "contextstream.md"),
9436
10232
  aider: ".aider.conf.yml"
9437
10233
  };
9438
10234
  var RULES_GLOBAL_FILES = {
9439
- codex: [path5.join(homedir3(), ".codex", "AGENTS.md")],
9440
- windsurf: [path5.join(homedir3(), ".codeium", "windsurf", "memories", "global_rules.md")],
9441
- kilo: [path5.join(homedir3(), ".kilocode", "rules", "contextstream.md")],
9442
- roo: [path5.join(homedir3(), ".roo", "rules", "contextstream.md")]
10235
+ codex: [path6.join(homedir3(), ".codex", "AGENTS.md")],
10236
+ windsurf: [path6.join(homedir3(), ".codeium", "windsurf", "memories", "global_rules.md")],
10237
+ kilo: [path6.join(homedir3(), ".kilocode", "rules", "contextstream.md")],
10238
+ roo: [path6.join(homedir3(), ".roo", "rules", "contextstream.md")]
9443
10239
  };
9444
10240
  var rulesNoticeCache = /* @__PURE__ */ new Map();
9445
10241
  function compareVersions2(v1, v2) {
@@ -9492,7 +10288,7 @@ function resolveRulesCandidatePaths(folderPath, editorKey) {
9492
10288
  if (!folderPath) return;
9493
10289
  const rel = RULES_PROJECT_FILES[key];
9494
10290
  if (rel) {
9495
- candidates.add(path5.join(folderPath, rel));
10291
+ candidates.add(path6.join(folderPath, rel));
9496
10292
  }
9497
10293
  };
9498
10294
  const addGlobal = (key) => {
@@ -9524,7 +10320,7 @@ function resolveFolderPath(inputPath, sessionManager) {
9524
10320
  const indicators = [".git", "package.json", "Cargo.toml", "pyproject.toml", ".contextstream"];
9525
10321
  const hasIndicator = indicators.some((entry) => {
9526
10322
  try {
9527
- return fs4.existsSync(path5.join(cwd, entry));
10323
+ return fs5.existsSync(path6.join(cwd, entry));
9528
10324
  } catch {
9529
10325
  return false;
9530
10326
  }
@@ -9543,7 +10339,7 @@ function getRulesNotice(folderPath, clientName) {
9543
10339
  return cached.notice;
9544
10340
  }
9545
10341
  const candidates = resolveRulesCandidatePaths(folderPath, editorKey);
9546
- const existing = candidates.filter((filePath) => fs4.existsSync(filePath));
10342
+ const existing = candidates.filter((filePath) => fs5.existsSync(filePath));
9547
10343
  if (existing.length === 0) {
9548
10344
  const updateCommand2 = "generate_rules()";
9549
10345
  const notice2 = {
@@ -9565,7 +10361,7 @@ function getRulesNotice(folderPath, clientName) {
9565
10361
  const versions = [];
9566
10362
  for (const filePath of existing) {
9567
10363
  try {
9568
- const content = fs4.readFileSync(filePath, "utf-8");
10364
+ const content = fs5.readFileSync(filePath, "utf-8");
9569
10365
  const version = extractRulesVersion(content);
9570
10366
  if (!version) {
9571
10367
  filesMissingVersion.push(filePath);
@@ -9758,23 +10554,23 @@ function replaceContextStreamBlock(existing, content) {
9758
10554
  return { content: appended, status: "appended" };
9759
10555
  }
9760
10556
  async function upsertRuleFile(filePath, content) {
9761
- await fs4.promises.mkdir(path5.dirname(filePath), { recursive: true });
10557
+ await fs5.promises.mkdir(path6.dirname(filePath), { recursive: true });
9762
10558
  const wrappedContent = wrapWithMarkers(content);
9763
10559
  let existing = "";
9764
10560
  try {
9765
- existing = await fs4.promises.readFile(filePath, "utf8");
10561
+ existing = await fs5.promises.readFile(filePath, "utf8");
9766
10562
  } catch {
9767
10563
  }
9768
10564
  if (!existing) {
9769
- await fs4.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
10565
+ await fs5.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
9770
10566
  return "created";
9771
10567
  }
9772
10568
  if (!existing.trim()) {
9773
- await fs4.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
10569
+ await fs5.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
9774
10570
  return "updated";
9775
10571
  }
9776
10572
  const replaced = replaceContextStreamBlock(existing, content);
9777
- await fs4.promises.writeFile(filePath, replaced.content, "utf8");
10573
+ await fs5.promises.writeFile(filePath, replaced.content, "utf8");
9778
10574
  return replaced.status;
9779
10575
  }
9780
10576
  async function writeEditorRules(options) {
@@ -9792,8 +10588,8 @@ async function writeEditorRules(options) {
9792
10588
  results.push({ editor, filename: "", status: "unknown editor" });
9793
10589
  continue;
9794
10590
  }
9795
- const filePath = path5.join(options.folderPath, rule.filename);
9796
- if (fs4.existsSync(filePath) && !options.overwriteExisting) {
10591
+ const filePath = path6.join(options.folderPath, rule.filename);
10592
+ if (fs5.existsSync(filePath) && !options.overwriteExisting) {
9797
10593
  results.push({ editor, filename: rule.filename, status: "skipped (exists)" });
9798
10594
  continue;
9799
10595
  }
@@ -9849,7 +10645,7 @@ async function writeGlobalRules(options) {
9849
10645
  continue;
9850
10646
  }
9851
10647
  for (const filePath of globalPaths) {
9852
- if (fs4.existsSync(filePath) && !options.overwriteExisting) {
10648
+ if (fs5.existsSync(filePath) && !options.overwriteExisting) {
9853
10649
  results.push({ editor, filename: filePath, status: "skipped (exists)", scope: "global" });
9854
10650
  continue;
9855
10651
  }
@@ -9942,9 +10738,9 @@ function humanizeKey(raw) {
9942
10738
  const withSpaces = raw.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ");
9943
10739
  return withSpaces.toLowerCase();
9944
10740
  }
9945
- function buildParamDescription(key, path8) {
10741
+ function buildParamDescription(key, path9) {
9946
10742
  const normalized = key in DEFAULT_PARAM_DESCRIPTIONS ? key : key.toLowerCase();
9947
- const parent = path8[path8.length - 1];
10743
+ const parent = path9[path9.length - 1];
9948
10744
  if (parent === "target") {
9949
10745
  if (key === "id") return "Target identifier (module path, function id, etc.).";
9950
10746
  if (key === "type") return "Target type (module, file, function, type, variable).";
@@ -9975,7 +10771,7 @@ function getDescription(schema) {
9975
10771
  if (def?.description && def.description.trim()) return def.description;
9976
10772
  return void 0;
9977
10773
  }
9978
- function applyParamDescriptions(schema, path8 = []) {
10774
+ function applyParamDescriptions(schema, path9 = []) {
9979
10775
  if (!(schema instanceof external_exports.ZodObject)) {
9980
10776
  return schema;
9981
10777
  }
@@ -9986,7 +10782,7 @@ function applyParamDescriptions(schema, path8 = []) {
9986
10782
  let nextField = field;
9987
10783
  const existingDescription = getDescription(field);
9988
10784
  if (field instanceof external_exports.ZodObject) {
9989
- const nested = applyParamDescriptions(field, [...path8, key]);
10785
+ const nested = applyParamDescriptions(field, [...path9, key]);
9990
10786
  if (nested !== field) {
9991
10787
  nextField = nested;
9992
10788
  changed = true;
@@ -9998,7 +10794,7 @@ function applyParamDescriptions(schema, path8 = []) {
9998
10794
  changed = true;
9999
10795
  }
10000
10796
  } else {
10001
- nextField = nextField.describe(buildParamDescription(key, path8));
10797
+ nextField = nextField.describe(buildParamDescription(key, path9));
10002
10798
  changed = true;
10003
10799
  }
10004
10800
  nextShape[key] = nextField;
@@ -10054,13 +10850,17 @@ function resolveAuthOverride(extra) {
10054
10850
  return { workspaceId, projectId };
10055
10851
  }
10056
10852
  var LIGHT_TOOLSET = /* @__PURE__ */ new Set([
10057
- // Core session tools (13)
10853
+ // Core session tools (15)
10058
10854
  "session_init",
10059
10855
  "session_tools",
10060
10856
  "context_smart",
10061
10857
  "context_feedback",
10062
10858
  "session_summary",
10063
10859
  "session_capture",
10860
+ "session_capture_smart",
10861
+ // Pre-compaction state capture
10862
+ "session_restore_context",
10863
+ // Post-compaction context restore
10064
10864
  "session_capture_lesson",
10065
10865
  "session_get_lessons",
10066
10866
  "session_recall",
@@ -10100,13 +10900,17 @@ var LIGHT_TOOLSET = /* @__PURE__ */ new Set([
10100
10900
  "mcp_server_version"
10101
10901
  ]);
10102
10902
  var STANDARD_TOOLSET = /* @__PURE__ */ new Set([
10103
- // Core session tools (14)
10903
+ // Core session tools (16)
10104
10904
  "session_init",
10105
10905
  "session_tools",
10106
10906
  "context_smart",
10107
10907
  "context_feedback",
10108
10908
  "session_summary",
10109
10909
  "session_capture",
10910
+ "session_capture_smart",
10911
+ // Pre-compaction state capture
10912
+ "session_restore_context",
10913
+ // Post-compaction context restore
10110
10914
  "session_capture_lesson",
10111
10915
  "session_get_lessons",
10112
10916
  "session_recall",
@@ -10557,6 +11361,7 @@ function parsePositiveInt(raw, fallback) {
10557
11361
  }
10558
11362
  var OUTPUT_FORMAT = process.env.CONTEXTSTREAM_OUTPUT_FORMAT || "compact";
10559
11363
  var COMPACT_OUTPUT = OUTPUT_FORMAT === "compact";
11364
+ var SHOW_TIMING = process.env.CONTEXTSTREAM_SHOW_TIMING === "true" || process.env.CONTEXTSTREAM_SHOW_TIMING === "1";
10560
11365
  var DEFAULT_SEARCH_LIMIT = parsePositiveInt(process.env.CONTEXTSTREAM_SEARCH_LIMIT, 3);
10561
11366
  var DEFAULT_SEARCH_CONTENT_MAX_CHARS = parsePositiveInt(
10562
11367
  process.env.CONTEXTSTREAM_SEARCH_MAX_CHARS,
@@ -10669,6 +11474,31 @@ function toStructured(data) {
10669
11474
  }
10670
11475
  return void 0;
10671
11476
  }
11477
+ function formatTimingSummary(roundTripMs, resultCount) {
11478
+ if (!SHOW_TIMING) return "";
11479
+ const countStr = resultCount !== void 0 ? `${resultCount} results` : "done";
11480
+ return `\u2713 ${countStr} in ${roundTripMs}ms
11481
+
11482
+ `;
11483
+ }
11484
+ function getResultCount(data) {
11485
+ if (!data || typeof data !== "object") return void 0;
11486
+ const response = data;
11487
+ const dataObj = response.data;
11488
+ if (dataObj?.results && Array.isArray(dataObj.results)) {
11489
+ return dataObj.results.length;
11490
+ }
11491
+ if (typeof dataObj?.total === "number") {
11492
+ return dataObj.total;
11493
+ }
11494
+ if (typeof dataObj?.count === "number") {
11495
+ return dataObj.count;
11496
+ }
11497
+ if (dataObj?.paths && Array.isArray(dataObj.paths)) {
11498
+ return dataObj.paths.length;
11499
+ }
11500
+ return void 0;
11501
+ }
10672
11502
  function readStatNumber(payload, key) {
10673
11503
  if (!payload || typeof payload !== "object") return void 0;
10674
11504
  const direct = payload[key];
@@ -11334,10 +12164,10 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
11334
12164
  );
11335
12165
  }
11336
12166
  async function validateReadableDirectory(inputPath) {
11337
- const resolvedPath = path5.resolve(inputPath);
12167
+ const resolvedPath = path6.resolve(inputPath);
11338
12168
  let stats;
11339
12169
  try {
11340
- stats = await fs4.promises.stat(resolvedPath);
12170
+ stats = await fs5.promises.stat(resolvedPath);
11341
12171
  } catch (error) {
11342
12172
  if (error?.code === "ENOENT") {
11343
12173
  return { ok: false, error: `Error: path does not exist: ${inputPath}` };
@@ -11351,7 +12181,7 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
11351
12181
  return { ok: false, error: `Error: path is not a directory: ${inputPath}` };
11352
12182
  }
11353
12183
  try {
11354
- await fs4.promises.access(resolvedPath, fs4.constants.R_OK | fs4.constants.X_OK);
12184
+ await fs5.promises.access(resolvedPath, fs5.constants.R_OK | fs5.constants.X_OK);
11355
12185
  } catch (error) {
11356
12186
  return {
11357
12187
  ok: false,
@@ -11774,17 +12604,17 @@ Access: Free`,
11774
12604
  let rulesSkipped = [];
11775
12605
  if (input.folder_path && projectData.id) {
11776
12606
  try {
11777
- const configDir = path5.join(input.folder_path, ".contextstream");
11778
- const configPath = path5.join(configDir, "config.json");
11779
- if (!fs4.existsSync(configDir)) {
11780
- fs4.mkdirSync(configDir, { recursive: true });
12607
+ const configDir = path6.join(input.folder_path, ".contextstream");
12608
+ const configPath = path6.join(configDir, "config.json");
12609
+ if (!fs5.existsSync(configDir)) {
12610
+ fs5.mkdirSync(configDir, { recursive: true });
11781
12611
  }
11782
12612
  const config = {
11783
12613
  workspace_id: workspaceId,
11784
12614
  project_id: projectData.id,
11785
12615
  project_name: input.name
11786
12616
  };
11787
- fs4.writeFileSync(configPath, JSON.stringify(config, null, 2));
12617
+ fs5.writeFileSync(configPath, JSON.stringify(config, null, 2));
11788
12618
  if (input.generate_editor_rules) {
11789
12619
  const ruleResults = await writeEditorRules({
11790
12620
  folderPath: input.folder_path,
@@ -12989,10 +13819,17 @@ This does semantic search on the first message. You only need context_smart on s
12989
13819
  auto_index: external_exports.boolean().optional().describe("Automatically create and index project from IDE workspace (default: true)"),
12990
13820
  allow_no_workspace: external_exports.boolean().optional().describe(
12991
13821
  "If true, allow session_init to return connected even if no workspace is resolved (workspace-level tools may not work)."
13822
+ ),
13823
+ skip_project_creation: external_exports.boolean().optional().describe(
13824
+ "If true, skip automatic project creation/matching. Use for parent folders containing multiple projects where you want workspace-level context but no project-specific context."
13825
+ ),
13826
+ is_post_compact: external_exports.boolean().optional().describe(
13827
+ "Set to true when resuming after conversation compaction. This prioritizes session_snapshot restoration and recent decisions."
12992
13828
  )
12993
13829
  })
12994
13830
  },
12995
13831
  async (input) => {
13832
+ const startTime = Date.now();
12996
13833
  let ideRoots = [];
12997
13834
  try {
12998
13835
  const rootsResponse = await server.server.listRoots();
@@ -13008,8 +13845,48 @@ This does semantic search on the first message. You only need context_smart on s
13008
13845
  }
13009
13846
  const result = await client.initSession(input, ideRoots);
13010
13847
  result.tools_hint = getCoreToolsHint();
13848
+ if (input.is_post_compact) {
13849
+ const workspaceIdForRestore = typeof result.workspace_id === "string" ? result.workspace_id : void 0;
13850
+ const projectIdForRestore = typeof result.project_id === "string" ? result.project_id : void 0;
13851
+ if (workspaceIdForRestore) {
13852
+ try {
13853
+ const snapshotSearch = await client.searchEvents({
13854
+ workspace_id: workspaceIdForRestore,
13855
+ project_id: projectIdForRestore,
13856
+ query: "session_snapshot",
13857
+ event_types: ["session_snapshot"],
13858
+ limit: 1
13859
+ });
13860
+ const snapshots = snapshotSearch?.data?.results || snapshotSearch?.results || snapshotSearch?.data || [];
13861
+ if (snapshots && snapshots.length > 0) {
13862
+ const latestSnapshot = snapshots[0];
13863
+ let snapshotData;
13864
+ try {
13865
+ snapshotData = JSON.parse(latestSnapshot.content);
13866
+ } catch {
13867
+ snapshotData = { conversation_summary: latestSnapshot.content };
13868
+ }
13869
+ result.restored_context = {
13870
+ snapshot_id: latestSnapshot.id,
13871
+ captured_at: snapshotData.captured_at || latestSnapshot.created_at,
13872
+ ...snapshotData
13873
+ };
13874
+ result.is_post_compact = true;
13875
+ result.post_compact_hint = "Session restored from pre-compaction snapshot. Review the 'restored_context' to continue where you left off.";
13876
+ } else {
13877
+ result.is_post_compact = true;
13878
+ result.post_compact_hint = "Post-compaction session started, but no snapshots found. Use context_smart to retrieve relevant context.";
13879
+ }
13880
+ } catch (err) {
13881
+ console.error("[ContextStream] Failed to restore post-compact context:", err);
13882
+ result.is_post_compact = true;
13883
+ result.post_compact_hint = "Post-compaction session started. Snapshot restoration failed, use context_smart for context.";
13884
+ }
13885
+ }
13886
+ }
13011
13887
  if (sessionManager) {
13012
13888
  sessionManager.markInitialized(result);
13889
+ sessionManager.resetTokenCount();
13013
13890
  }
13014
13891
  const folderPathForRules = input.folder_path || ideRoots[0] || resolveFolderPath(void 0, sessionManager);
13015
13892
  if (sessionManager && folderPathForRules) {
@@ -13073,7 +13950,7 @@ This does semantic search on the first message. You only need context_smart on s
13073
13950
  formatContent(result)
13074
13951
  ].join("\n");
13075
13952
  } else if (status === "requires_workspace_selection") {
13076
- const folderName = typeof result.folder_name === "string" ? result.folder_name : typeof input.folder_path === "string" ? path5.basename(input.folder_path) || "this folder" : "this folder";
13953
+ const folderName = typeof result.folder_name === "string" ? result.folder_name : typeof input.folder_path === "string" ? path6.basename(input.folder_path) || "this folder" : "this folder";
13077
13954
  const candidates = Array.isArray(result.workspace_candidates) ? result.workspace_candidates : [];
13078
13955
  const lines = [];
13079
13956
  lines.push(
@@ -13156,6 +14033,12 @@ ${noticeLines.filter(Boolean).join("\n")}`;
13156
14033
  text = `${text}
13157
14034
 
13158
14035
  ${SEARCH_RULES_REMINDER}`;
14036
+ }
14037
+ const roundTripMs = Date.now() - startTime;
14038
+ if (SHOW_TIMING) {
14039
+ text = `\u2713 session initialized in ${roundTripMs}ms
14040
+
14041
+ ${text}`;
13159
14042
  }
13160
14043
  return {
13161
14044
  content: [{ type: "text", text }],
@@ -13325,7 +14208,7 @@ Behavior:
13325
14208
  "Error: folder_path is required. Provide folder_path or run from a project directory."
13326
14209
  );
13327
14210
  }
13328
- const folderName = path5.basename(folderPath) || "My Project";
14211
+ const folderName = path6.basename(folderPath) || "My Project";
13329
14212
  let newWorkspace;
13330
14213
  try {
13331
14214
  newWorkspace = await client.createWorkspace({
@@ -13433,8 +14316,11 @@ Use this to persist decisions, insights, preferences, or important information.`
13433
14316
  // Extracted lesson from correction
13434
14317
  "warning",
13435
14318
  // Proactive reminder
13436
- "frustration"
14319
+ "frustration",
13437
14320
  // User expressed frustration
14321
+ // Compaction awareness
14322
+ "session_snapshot"
14323
+ // Pre-compaction state capture
13438
14324
  ]).describe("Type of context being captured"),
13439
14325
  title: external_exports.string().describe("Brief title for the captured context"),
13440
14326
  content: external_exports.string().describe("Full content/details to capture"),
@@ -13489,6 +14375,224 @@ Use this to persist decisions, insights, preferences, or important information.`
13489
14375
  };
13490
14376
  }
13491
14377
  );
14378
+ registerTool(
14379
+ "session_capture_smart",
14380
+ {
14381
+ title: "Smart capture for conversation compaction",
14382
+ description: `Intelligently capture conversation state before compaction or context loss.
14383
+ This creates a session_snapshot that can be restored after compaction.
14384
+
14385
+ Use when:
14386
+ - Context pressure is high/critical (context_smart returns threshold_warning)
14387
+ - Before manual /compact commands
14388
+ - When significant work progress needs preservation
14389
+
14390
+ Captures:
14391
+ - Conversation summary and current goals
14392
+ - Active files being worked on
14393
+ - Recent decisions with rationale
14394
+ - Unfinished work items
14395
+ - User preferences expressed in session
14396
+
14397
+ The snapshot is automatically prioritized during post-compaction session_init.`,
14398
+ inputSchema: external_exports.object({
14399
+ workspace_id: external_exports.string().uuid().optional(),
14400
+ project_id: external_exports.string().uuid().optional(),
14401
+ conversation_summary: external_exports.string().describe("AI's summary of the conversation so far - what was discussed and accomplished"),
14402
+ current_goal: external_exports.string().optional().describe("The primary goal or task being worked on"),
14403
+ active_files: external_exports.array(external_exports.string()).optional().describe("List of files currently being worked on"),
14404
+ recent_decisions: external_exports.array(
14405
+ external_exports.object({
14406
+ title: external_exports.string(),
14407
+ rationale: external_exports.string().optional()
14408
+ })
14409
+ ).optional().describe("Key decisions made in this session with their rationale"),
14410
+ unfinished_work: external_exports.array(
14411
+ external_exports.object({
14412
+ task: external_exports.string(),
14413
+ status: external_exports.string().optional(),
14414
+ next_steps: external_exports.string().optional()
14415
+ })
14416
+ ).optional().describe("Work items that are in progress or pending"),
14417
+ user_preferences: external_exports.array(external_exports.string()).optional().describe("Preferences expressed by user during this session"),
14418
+ priority_items: external_exports.array(external_exports.string()).optional().describe("User-flagged important items to remember"),
14419
+ metadata: external_exports.record(external_exports.unknown()).optional().describe("Additional context to preserve")
14420
+ })
14421
+ },
14422
+ async (input) => {
14423
+ let workspaceId = input.workspace_id;
14424
+ let projectId = input.project_id;
14425
+ if (!workspaceId && sessionManager) {
14426
+ const ctx = sessionManager.getContext();
14427
+ if (ctx) {
14428
+ workspaceId = ctx.workspace_id;
14429
+ projectId = projectId || ctx.project_id;
14430
+ }
14431
+ }
14432
+ if (!workspaceId) {
14433
+ return errorResult(
14434
+ "Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly."
14435
+ );
14436
+ }
14437
+ const snapshotContent = {
14438
+ conversation_summary: input.conversation_summary,
14439
+ captured_at: (/* @__PURE__ */ new Date()).toISOString()
14440
+ };
14441
+ if (input.current_goal) {
14442
+ snapshotContent.current_goal = input.current_goal;
14443
+ }
14444
+ if (input.active_files?.length) {
14445
+ snapshotContent.active_files = input.active_files;
14446
+ }
14447
+ if (input.recent_decisions?.length) {
14448
+ snapshotContent.recent_decisions = input.recent_decisions;
14449
+ }
14450
+ if (input.unfinished_work?.length) {
14451
+ snapshotContent.unfinished_work = input.unfinished_work;
14452
+ }
14453
+ if (input.user_preferences?.length) {
14454
+ snapshotContent.user_preferences = input.user_preferences;
14455
+ }
14456
+ if (input.priority_items?.length) {
14457
+ snapshotContent.priority_items = input.priority_items;
14458
+ }
14459
+ if (input.metadata) {
14460
+ snapshotContent.metadata = input.metadata;
14461
+ }
14462
+ const result = await client.captureContext({
14463
+ workspace_id: workspaceId,
14464
+ project_id: projectId,
14465
+ event_type: "session_snapshot",
14466
+ title: `Session Snapshot: ${input.current_goal || "Conversation State"}`,
14467
+ content: JSON.stringify(snapshotContent, null, 2),
14468
+ importance: "high",
14469
+ tags: ["session_snapshot", "pre_compaction"]
14470
+ });
14471
+ const response = {
14472
+ ...result,
14473
+ snapshot_id: result?.data?.id || result?.id,
14474
+ message: "Session state captured successfully. This snapshot will be prioritized after compaction.",
14475
+ hint: "After compaction, call session_init with is_post_compact=true to restore this context."
14476
+ };
14477
+ return {
14478
+ content: [{ type: "text", text: formatContent(response) }],
14479
+ structuredContent: toStructured(response)
14480
+ };
14481
+ }
14482
+ );
14483
+ registerTool(
14484
+ "session_restore_context",
14485
+ {
14486
+ title: "Restore context after compaction",
14487
+ description: `Restore conversation context after compaction or context loss.
14488
+ Call this after conversation compaction to retrieve saved session state.
14489
+
14490
+ Returns structured context including:
14491
+ - conversation_summary: What was being discussed
14492
+ - current_goal: The primary task being worked on
14493
+ - active_files: Files that were being modified
14494
+ - recent_decisions: Key decisions made in the session
14495
+ - unfinished_work: Tasks that are still in progress
14496
+ - user_preferences: Preferences expressed during the session
14497
+
14498
+ Use this in combination with session_init(is_post_compact=true) for seamless continuation.`,
14499
+ inputSchema: external_exports.object({
14500
+ workspace_id: external_exports.string().uuid().optional(),
14501
+ project_id: external_exports.string().uuid().optional(),
14502
+ snapshot_id: external_exports.string().uuid().optional().describe("Specific snapshot ID to restore (defaults to most recent)"),
14503
+ max_snapshots: external_exports.number().optional().default(1).describe("Number of recent snapshots to consider (default: 1)")
14504
+ })
14505
+ },
14506
+ async (input) => {
14507
+ let workspaceId = input.workspace_id;
14508
+ let projectId = input.project_id;
14509
+ if (!workspaceId && sessionManager) {
14510
+ const ctx = sessionManager.getContext();
14511
+ if (ctx) {
14512
+ workspaceId = ctx.workspace_id;
14513
+ projectId = projectId || ctx.project_id;
14514
+ }
14515
+ }
14516
+ if (!workspaceId) {
14517
+ return errorResult(
14518
+ "Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly."
14519
+ );
14520
+ }
14521
+ try {
14522
+ if (input.snapshot_id) {
14523
+ const eventResult = await client.getEvent(input.snapshot_id);
14524
+ const event = eventResult?.data || eventResult;
14525
+ if (!event || !event.content) {
14526
+ return errorResult(
14527
+ `Snapshot not found: ${input.snapshot_id}. The snapshot may have been deleted or does not exist.`
14528
+ );
14529
+ }
14530
+ let snapshotData2;
14531
+ try {
14532
+ snapshotData2 = JSON.parse(event.content);
14533
+ } catch {
14534
+ snapshotData2 = { conversation_summary: event.content };
14535
+ }
14536
+ const response2 = {
14537
+ restored: true,
14538
+ snapshot_id: event.id,
14539
+ captured_at: snapshotData2.captured_at || event.created_at,
14540
+ ...snapshotData2,
14541
+ hint: "Context restored. Continue the conversation with awareness of the above state."
14542
+ };
14543
+ return {
14544
+ content: [{ type: "text", text: formatContent(response2) }],
14545
+ structuredContent: toStructured(response2)
14546
+ };
14547
+ }
14548
+ const listResult = await client.listMemoryEvents({
14549
+ workspace_id: workspaceId,
14550
+ project_id: projectId,
14551
+ limit: 50
14552
+ // Fetch more to filter
14553
+ });
14554
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
14555
+ const events = allEvents.filter(
14556
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
14557
+ ).slice(0, input.max_snapshots || 1);
14558
+ if (!events || events.length === 0) {
14559
+ return {
14560
+ content: [
14561
+ {
14562
+ type: "text",
14563
+ text: formatContent({
14564
+ restored: false,
14565
+ message: "No session snapshots found. This may be a new session or snapshots have not been captured.",
14566
+ hint: "Use session_capture_smart to save session state before compaction."
14567
+ })
14568
+ }
14569
+ ]
14570
+ };
14571
+ }
14572
+ const latestEvent = events[0];
14573
+ let snapshotData;
14574
+ try {
14575
+ snapshotData = JSON.parse(latestEvent.content);
14576
+ } catch {
14577
+ snapshotData = { conversation_summary: latestEvent.content };
14578
+ }
14579
+ const response = {
14580
+ restored: true,
14581
+ snapshot_id: latestEvent.id,
14582
+ captured_at: snapshotData.captured_at || latestEvent.created_at,
14583
+ ...snapshotData,
14584
+ hint: "Context restored. Continue the conversation with awareness of the above state."
14585
+ };
14586
+ return {
14587
+ content: [{ type: "text", text: formatContent(response) }],
14588
+ structuredContent: toStructured(response)
14589
+ };
14590
+ } catch (error) {
14591
+ const message = error instanceof Error ? error.message : String(error);
14592
+ return errorResult(`Failed to restore context: ${message}`);
14593
+ }
14594
+ }
14595
+ );
13492
14596
  registerTool(
13493
14597
  "session_capture_lesson",
13494
14598
  {
@@ -13847,6 +14951,7 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
13847
14951
  overwrite_existing: external_exports.boolean().optional().describe("Allow overwriting existing rule files (ContextStream block only)"),
13848
14952
  apply_global: external_exports.boolean().optional().describe("Also write global rule files for supported editors"),
13849
14953
  install_hooks: external_exports.boolean().optional().describe("Install Claude Code hooks to enforce ContextStream-first search. Defaults to true for Claude users. Set to false to skip."),
14954
+ include_pre_compact: external_exports.boolean().optional().describe("Include PreCompact hook for automatic state saving before context compaction. Defaults to false."),
13850
14955
  dry_run: external_exports.boolean().optional().describe("If true, return content without writing files")
13851
14956
  })
13852
14957
  },
@@ -13927,8 +15032,14 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
13927
15032
  { file: "~/.claude/hooks/contextstream-reminder.py", status: "dry run - would create" },
13928
15033
  { file: "~/.claude/settings.json", status: "dry run - would update" }
13929
15034
  ];
15035
+ if (input.include_pre_compact) {
15036
+ hooksResults.push({ file: "~/.claude/hooks/contextstream-precompact.py", status: "dry run - would create" });
15037
+ }
13930
15038
  } else {
13931
- const hookResult = await installClaudeCodeHooks({ scope: "user" });
15039
+ const hookResult = await installClaudeCodeHooks({
15040
+ scope: "user",
15041
+ includePreCompact: input.include_pre_compact
15042
+ });
13932
15043
  hooksResults = [
13933
15044
  ...hookResult.scripts.map((f) => ({ file: f, status: "created" })),
13934
15045
  ...hookResult.settings.map((f) => ({ file: f, status: "updated" }))
@@ -14287,10 +15398,13 @@ This saves ~80% tokens compared to including full chat history.`,
14287
15398
  max_tokens: external_exports.number().optional().describe("Maximum tokens for context (default: 800)"),
14288
15399
  format: external_exports.enum(["minified", "readable", "structured"]).optional().describe("Context format (default: minified)"),
14289
15400
  mode: external_exports.enum(["standard", "pack"]).optional().describe("Context pack mode (default: pack when enabled)"),
14290
- distill: external_exports.boolean().optional().describe("Use distillation for context pack (default: true)")
15401
+ distill: external_exports.boolean().optional().describe("Use distillation for context pack (default: true)"),
15402
+ session_tokens: external_exports.number().optional().describe("Cumulative session token count for context pressure calculation"),
15403
+ context_threshold: external_exports.number().optional().describe("Custom context window threshold (defaults to 70k)")
14291
15404
  })
14292
15405
  },
14293
15406
  async (input) => {
15407
+ const startTime = Date.now();
14294
15408
  if (sessionManager) {
14295
15409
  sessionManager.markContextSmartCalled();
14296
15410
  }
@@ -14303,6 +15417,17 @@ This saves ~80% tokens compared to including full chat history.`,
14303
15417
  projectId = projectId || ctx.project_id;
14304
15418
  }
14305
15419
  }
15420
+ let sessionTokens = input.session_tokens;
15421
+ let contextThreshold = input.context_threshold;
15422
+ if (sessionManager) {
15423
+ if (sessionTokens === void 0) {
15424
+ sessionTokens = sessionManager.getSessionTokens();
15425
+ }
15426
+ if (contextThreshold === void 0) {
15427
+ contextThreshold = sessionManager.getContextThreshold();
15428
+ }
15429
+ sessionManager.addTokens(input.user_message);
15430
+ }
14306
15431
  const result = await client.getSmartContext({
14307
15432
  user_message: input.user_message,
14308
15433
  workspace_id: workspaceId,
@@ -14310,11 +15435,18 @@ This saves ~80% tokens compared to including full chat history.`,
14310
15435
  max_tokens: input.max_tokens,
14311
15436
  format: input.format,
14312
15437
  mode: input.mode,
14313
- distill: input.distill
15438
+ distill: input.distill,
15439
+ session_tokens: sessionTokens,
15440
+ context_threshold: contextThreshold
14314
15441
  });
15442
+ if (sessionManager && result.token_estimate) {
15443
+ sessionManager.addTokens(result.token_estimate);
15444
+ }
15445
+ const roundTripMs = Date.now() - startTime;
15446
+ const timingStr = SHOW_TIMING ? ` | ${roundTripMs}ms` : "";
14315
15447
  const footer = `
14316
15448
  ---
14317
- \u{1F3AF} ${result.sources_used} sources | ~${result.token_estimate} tokens | format: ${result.format}`;
15449
+ \u{1F3AF} ${result.sources_used} sources | ~${result.token_estimate} tokens | format: ${result.format}${timingStr}`;
14318
15450
  const folderPathForRules = resolveFolderPath(void 0, sessionManager);
14319
15451
  const rulesNotice = getRulesNotice(folderPathForRules, detectedClientInfo?.name);
14320
15452
  let versionNotice = result.version_notice;
@@ -14341,6 +15473,22 @@ This saves ~80% tokens compared to including full chat history.`,
14341
15473
  const searchRulesLine = SEARCH_RULES_REMINDER_ENABLED ? `
14342
15474
 
14343
15475
  ${SEARCH_RULES_REMINDER}` : "";
15476
+ let contextPressureWarning = "";
15477
+ if (result.context_pressure) {
15478
+ const cp = result.context_pressure;
15479
+ if (cp.level === "critical") {
15480
+ contextPressureWarning = `
15481
+
15482
+ \u{1F6A8} [CONTEXT PRESSURE: CRITICAL] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
15483
+ Action: ${cp.suggested_action === "save_now" ? 'SAVE STATE NOW - Call session(action="capture") to preserve conversation state before compaction.' : cp.suggested_action}
15484
+ The conversation may compact soon. Save important decisions, insights, and progress immediately.`;
15485
+ } else if (cp.level === "high") {
15486
+ contextPressureWarning = `
15487
+
15488
+ \u26A0\uFE0F [CONTEXT PRESSURE: HIGH] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
15489
+ Action: ${cp.suggested_action === "prepare_save" ? "Consider saving important decisions and conversation state soon." : cp.suggested_action}`;
15490
+ }
15491
+ }
14344
15492
  const allWarnings = [
14345
15493
  lessonsWarningLine,
14346
15494
  rulesWarningLine ? `
@@ -14349,6 +15497,7 @@ ${rulesWarningLine}` : "",
14349
15497
  versionWarningLine ? `
14350
15498
 
14351
15499
  ${versionWarningLine}` : "",
15500
+ contextPressureWarning,
14352
15501
  searchRulesLine
14353
15502
  ].filter(Boolean).join("");
14354
15503
  return {
@@ -15379,6 +16528,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15379
16528
  },
15380
16529
  async (input) => {
15381
16530
  const params = normalizeSearchParams(input);
16531
+ const startTime = Date.now();
15382
16532
  let result;
15383
16533
  let toolType;
15384
16534
  switch (input.mode) {
@@ -15409,7 +16559,9 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15409
16559
  default:
15410
16560
  toolType = "search_hybrid";
15411
16561
  }
15412
- const outputText = formatContent(result);
16562
+ const roundTripMs = Date.now() - startTime;
16563
+ const timingSummary = formatTimingSummary(roundTripMs, getResultCount(result));
16564
+ const outputText = timingSummary + formatContent(result);
15413
16565
  trackToolTokenSavings(client, toolType, outputText, {
15414
16566
  workspace_id: params.workspace_id,
15415
16567
  project_id: params.project_id
@@ -15424,7 +16576,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15424
16576
  "session",
15425
16577
  {
15426
16578
  title: "Session",
15427
- description: `Session management operations. Actions: capture (save decision/insight), capture_lesson (save lesson from mistake), get_lessons (retrieve lessons), recall (natural language recall), remember (quick save), user_context (get preferences), summary (workspace summary), compress (compress chat), delta (changes since timestamp), smart_search (context-enriched search), decision_trace (trace decision provenance). Plan actions: capture_plan (save implementation plan), get_plan (retrieve plan with tasks), update_plan (modify plan), list_plans (list all plans).`,
16579
+ description: `Session management operations. Actions: capture (save decision/insight), capture_lesson (save lesson from mistake), get_lessons (retrieve lessons), recall (natural language recall), remember (quick save), user_context (get preferences), summary (workspace summary), compress (compress chat), delta (changes since timestamp), smart_search (context-enriched search), decision_trace (trace decision provenance), restore_context (restore state after compaction). Plan actions: capture_plan (save implementation plan), get_plan (retrieve plan with tasks), update_plan (modify plan), list_plans (list all plans).`,
15428
16580
  inputSchema: external_exports.object({
15429
16581
  action: external_exports.enum([
15430
16582
  "capture",
@@ -15442,7 +16594,9 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15442
16594
  "capture_plan",
15443
16595
  "get_plan",
15444
16596
  "update_plan",
15445
- "list_plans"
16597
+ "list_plans",
16598
+ // Context restore
16599
+ "restore_context"
15446
16600
  ]).describe("Action to perform"),
15447
16601
  workspace_id: external_exports.string().uuid().optional(),
15448
16602
  project_id: external_exports.string().uuid().optional(),
@@ -15464,7 +16618,8 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15464
16618
  "lesson",
15465
16619
  "warning",
15466
16620
  "frustration",
15467
- "conversation"
16621
+ "conversation",
16622
+ "session_snapshot"
15468
16623
  ]).optional().describe("Event type for capture"),
15469
16624
  importance: external_exports.enum(["low", "medium", "high", "critical"]).optional(),
15470
16625
  tags: external_exports.array(external_exports.string()).optional(),
@@ -15514,7 +16669,10 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15514
16669
  status: external_exports.enum(["draft", "active", "completed", "archived", "abandoned"]).optional().describe("Plan status"),
15515
16670
  due_at: external_exports.string().optional().describe("Due date for plan (ISO timestamp)"),
15516
16671
  source_tool: external_exports.string().optional().describe("Tool that generated this plan"),
15517
- include_tasks: external_exports.boolean().optional().describe("Include tasks when getting plan")
16672
+ include_tasks: external_exports.boolean().optional().describe("Include tasks when getting plan"),
16673
+ // Restore context params
16674
+ snapshot_id: external_exports.string().uuid().optional().describe("Specific snapshot ID to restore (defaults to most recent)"),
16675
+ max_snapshots: external_exports.number().optional().default(1).describe("Number of recent snapshots to consider (default: 1)")
15518
16676
  })
15519
16677
  },
15520
16678
  async (input) => {
@@ -15820,6 +16978,87 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15820
16978
  structuredContent: toStructured(result)
15821
16979
  };
15822
16980
  }
16981
+ case "restore_context": {
16982
+ if (!workspaceId) {
16983
+ return errorResult(
16984
+ "restore_context requires workspace_id. Call session_init first."
16985
+ );
16986
+ }
16987
+ if (input.snapshot_id) {
16988
+ const eventResult = await client.getEvent(input.snapshot_id);
16989
+ const event = eventResult?.data || eventResult;
16990
+ if (!event || !event.content) {
16991
+ return errorResult(
16992
+ `Snapshot not found: ${input.snapshot_id}. The snapshot may have been deleted or does not exist.`
16993
+ );
16994
+ }
16995
+ let snapshotData;
16996
+ try {
16997
+ snapshotData = JSON.parse(event.content);
16998
+ } catch {
16999
+ snapshotData = { conversation_summary: event.content };
17000
+ }
17001
+ const response2 = {
17002
+ restored: true,
17003
+ snapshot_id: event.id,
17004
+ captured_at: snapshotData.captured_at || event.created_at,
17005
+ ...snapshotData,
17006
+ hint: "Context restored. Continue the conversation with awareness of the above state."
17007
+ };
17008
+ return {
17009
+ content: [{ type: "text", text: formatContent(response2) }],
17010
+ structuredContent: toStructured(response2)
17011
+ };
17012
+ }
17013
+ const listResult = await client.listMemoryEvents({
17014
+ workspace_id: workspaceId,
17015
+ project_id: projectId,
17016
+ limit: 50
17017
+ // Fetch more to filter
17018
+ });
17019
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
17020
+ const snapshotEvents = allEvents.filter(
17021
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
17022
+ ).slice(0, input.max_snapshots || 1);
17023
+ if (!snapshotEvents || snapshotEvents.length === 0) {
17024
+ return {
17025
+ content: [
17026
+ {
17027
+ type: "text",
17028
+ text: formatContent({
17029
+ restored: false,
17030
+ message: "No session snapshots found. Use session_capture_smart to save state before compaction.",
17031
+ hint: "Start fresh or use session_init to get recent context."
17032
+ })
17033
+ }
17034
+ ]
17035
+ };
17036
+ }
17037
+ const snapshots = snapshotEvents.map((event) => {
17038
+ let snapshotData;
17039
+ try {
17040
+ snapshotData = JSON.parse(event.content || "{}");
17041
+ } catch {
17042
+ snapshotData = { conversation_summary: event.content };
17043
+ }
17044
+ return {
17045
+ snapshot_id: event.id,
17046
+ captured_at: snapshotData.captured_at || event.created_at,
17047
+ ...snapshotData
17048
+ };
17049
+ });
17050
+ const response = {
17051
+ restored: true,
17052
+ snapshots_found: snapshots.length,
17053
+ latest: snapshots[0],
17054
+ all_snapshots: snapshots.length > 1 ? snapshots : void 0,
17055
+ hint: "Context restored. Continue the conversation with awareness of the above state."
17056
+ };
17057
+ return {
17058
+ content: [{ type: "text", text: formatContent(response) }],
17059
+ structuredContent: toStructured(response)
17060
+ };
17061
+ }
15823
17062
  default:
15824
17063
  return errorResult(`Unknown action: ${input.action}`);
15825
17064
  }
@@ -15829,7 +17068,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15829
17068
  "memory",
15830
17069
  {
15831
17070
  title: "Memory",
15832
- description: `Memory operations for events and nodes. Event actions: create_event, get_event, update_event, delete_event, list_events, distill_event. Node actions: create_node, get_node, update_node, delete_node, list_nodes, supersede_node. Query actions: search, decisions, timeline, summary. Task actions: create_task (create task, optionally linked to plan), get_task, update_task (can link/unlink task to plan via plan_id), delete_task, list_tasks, reorder_tasks.`,
17071
+ description: `Memory operations for events and nodes. Event actions: create_event, get_event, update_event, delete_event, list_events, distill_event, import_batch (bulk import array of events). Node actions: create_node, get_node, update_node, delete_node, list_nodes, supersede_node. Query actions: search, decisions, timeline, summary. Task actions: create_task (create task, optionally linked to plan), get_task, update_task (can link/unlink task to plan via plan_id), delete_task, list_tasks, reorder_tasks.`,
15833
17072
  inputSchema: external_exports.object({
15834
17073
  action: external_exports.enum([
15835
17074
  "create_event",
@@ -15848,6 +17087,8 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15848
17087
  "decisions",
15849
17088
  "timeline",
15850
17089
  "summary",
17090
+ // Batch actions
17091
+ "import_batch",
15851
17092
  // Task actions
15852
17093
  "create_task",
15853
17094
  "get_task",
@@ -15908,7 +17149,33 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15908
17149
  order: external_exports.number().optional().describe("Task order within plan"),
15909
17150
  task_ids: external_exports.array(external_exports.string().uuid()).optional().describe("Task IDs for reorder_tasks"),
15910
17151
  blocked_reason: external_exports.string().optional().describe("Reason when task is blocked"),
15911
- tags: external_exports.array(external_exports.string()).optional().describe("Tags for task")
17152
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for task"),
17153
+ // Batch import params
17154
+ events: external_exports.array(
17155
+ external_exports.object({
17156
+ event_type: external_exports.string(),
17157
+ title: external_exports.string(),
17158
+ content: external_exports.string(),
17159
+ metadata: external_exports.record(external_exports.any()).optional(),
17160
+ provenance: external_exports.object({
17161
+ repo: external_exports.string().optional(),
17162
+ branch: external_exports.string().optional(),
17163
+ commit_sha: external_exports.string().optional(),
17164
+ pr_url: external_exports.string().url().optional(),
17165
+ issue_url: external_exports.string().url().optional(),
17166
+ slack_thread_url: external_exports.string().url().optional()
17167
+ }).optional(),
17168
+ code_refs: external_exports.array(
17169
+ external_exports.object({
17170
+ file_path: external_exports.string(),
17171
+ symbol_id: external_exports.string().optional(),
17172
+ symbol_name: external_exports.string().optional()
17173
+ })
17174
+ ).optional(),
17175
+ tags: external_exports.array(external_exports.string()).optional(),
17176
+ occurred_at: external_exports.string().optional().describe("ISO timestamp for when the event occurred")
17177
+ })
17178
+ ).optional().describe("Array of events for import_batch action")
15912
17179
  })
15913
17180
  },
15914
17181
  async (input) => {
@@ -15979,6 +17246,36 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15979
17246
  structuredContent: toStructured(result)
15980
17247
  };
15981
17248
  }
17249
+ case "import_batch": {
17250
+ if (!input.events || !Array.isArray(input.events) || input.events.length === 0) {
17251
+ return errorResult("import_batch requires: events (non-empty array of event objects)");
17252
+ }
17253
+ if (!workspaceId) {
17254
+ return errorResult("import_batch requires workspace_id. Call session_init first.");
17255
+ }
17256
+ const eventsWithContext = input.events.map((event) => ({
17257
+ ...event,
17258
+ workspace_id: workspaceId,
17259
+ project_id: projectId || event.project_id
17260
+ }));
17261
+ const result = await client.bulkIngestEvents({
17262
+ workspace_id: workspaceId,
17263
+ project_id: projectId,
17264
+ events: eventsWithContext
17265
+ });
17266
+ const count = Array.isArray(result) ? result.length : result?.data?.length ?? 0;
17267
+ return {
17268
+ content: [
17269
+ {
17270
+ type: "text",
17271
+ text: `\u2705 Imported ${count} event(s) successfully.
17272
+
17273
+ ${formatContent(result)}`
17274
+ }
17275
+ ],
17276
+ structuredContent: toStructured(result)
17277
+ };
17278
+ }
15982
17279
  case "distill_event": {
15983
17280
  if (!input.event_id) {
15984
17281
  return errorResult("distill_event requires: event_id");
@@ -18327,6 +19624,7 @@ function registerPrompts(server) {
18327
19624
 
18328
19625
  // src/session-manager.ts
18329
19626
  var SessionManager = class {
19627
+ // Conservative default for 100k context window
18330
19628
  constructor(server, client) {
18331
19629
  this.server = server;
18332
19630
  this.client = client;
@@ -18337,6 +19635,9 @@ var SessionManager = class {
18337
19635
  this.folderPath = null;
18338
19636
  this.contextSmartCalled = false;
18339
19637
  this.warningShown = false;
19638
+ // Token tracking for context pressure calculation
19639
+ this.sessionTokens = 0;
19640
+ this.contextThreshold = 7e4;
18340
19641
  }
18341
19642
  /**
18342
19643
  * Check if session has been auto-initialized
@@ -18375,8 +19676,8 @@ var SessionManager = class {
18375
19676
  /**
18376
19677
  * Set the folder path hint (can be passed from tools that know the workspace path)
18377
19678
  */
18378
- setFolderPath(path8) {
18379
- this.folderPath = path8;
19679
+ setFolderPath(path9) {
19680
+ this.folderPath = path9;
18380
19681
  }
18381
19682
  /**
18382
19683
  * Mark that context_smart has been called in this session
@@ -18384,6 +19685,51 @@ var SessionManager = class {
18384
19685
  markContextSmartCalled() {
18385
19686
  this.contextSmartCalled = true;
18386
19687
  }
19688
+ /**
19689
+ * Get current session token count for context pressure calculation.
19690
+ */
19691
+ getSessionTokens() {
19692
+ return this.sessionTokens;
19693
+ }
19694
+ /**
19695
+ * Get the context threshold (max tokens before compaction warning).
19696
+ */
19697
+ getContextThreshold() {
19698
+ return this.contextThreshold;
19699
+ }
19700
+ /**
19701
+ * Set a custom context threshold (useful if client provides model info).
19702
+ */
19703
+ setContextThreshold(threshold) {
19704
+ this.contextThreshold = threshold;
19705
+ }
19706
+ /**
19707
+ * Add tokens to the session count.
19708
+ * Call this after each tool response to track token accumulation.
19709
+ *
19710
+ * @param tokens - Exact token count or text to estimate
19711
+ */
19712
+ addTokens(tokens) {
19713
+ if (typeof tokens === "number") {
19714
+ this.sessionTokens += tokens;
19715
+ } else {
19716
+ this.sessionTokens += Math.ceil(tokens.length / 4);
19717
+ }
19718
+ }
19719
+ /**
19720
+ * Estimate tokens from a tool response.
19721
+ * Uses a simple heuristic: ~4 characters per token.
19722
+ */
19723
+ estimateTokens(content) {
19724
+ const text = typeof content === "string" ? content : JSON.stringify(content);
19725
+ return Math.ceil(text.length / 4);
19726
+ }
19727
+ /**
19728
+ * Reset token count (e.g., after compaction or new session).
19729
+ */
19730
+ resetTokenCount() {
19731
+ this.sessionTokens = 0;
19732
+ }
18387
19733
  /**
18388
19734
  * Check if context_smart has been called and warn if not.
18389
19735
  * Returns true if a warning was shown, false otherwise.
@@ -18452,7 +19798,7 @@ var SessionManager = class {
18452
19798
  }
18453
19799
  if (this.ideRoots.length === 0) {
18454
19800
  const cwd = process.cwd();
18455
- const fs7 = await import("fs");
19801
+ const fs8 = await import("fs");
18456
19802
  const projectIndicators = [
18457
19803
  ".git",
18458
19804
  "package.json",
@@ -18462,7 +19808,7 @@ var SessionManager = class {
18462
19808
  ];
18463
19809
  const hasProjectIndicator = projectIndicators.some((f) => {
18464
19810
  try {
18465
- return fs7.existsSync(`${cwd}/${f}`);
19811
+ return fs8.existsSync(`${cwd}/${f}`);
18466
19812
  } catch {
18467
19813
  return false;
18468
19814
  }
@@ -18935,24 +20281,24 @@ async function runHttpGateway() {
18935
20281
  // src/index.ts
18936
20282
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
18937
20283
  import { homedir as homedir6 } from "os";
18938
- import { join as join9 } from "path";
20284
+ import { join as join10 } from "path";
18939
20285
 
18940
20286
  // src/setup.ts
18941
- import * as fs6 from "node:fs/promises";
18942
- import * as path7 from "node:path";
20287
+ import * as fs7 from "node:fs/promises";
20288
+ import * as path8 from "node:path";
18943
20289
  import { homedir as homedir5 } from "node:os";
18944
20290
  import { stdin, stdout } from "node:process";
18945
20291
  import { createInterface } from "node:readline/promises";
18946
20292
 
18947
20293
  // src/credentials.ts
18948
- import * as fs5 from "node:fs/promises";
18949
- import * as path6 from "node:path";
20294
+ import * as fs6 from "node:fs/promises";
20295
+ import * as path7 from "node:path";
18950
20296
  import { homedir as homedir4 } from "node:os";
18951
20297
  function normalizeApiUrl(input) {
18952
20298
  return String(input ?? "").trim().replace(/\/+$/, "");
18953
20299
  }
18954
20300
  function credentialsFilePath() {
18955
- return path6.join(homedir4(), ".contextstream", "credentials.json");
20301
+ return path7.join(homedir4(), ".contextstream", "credentials.json");
18956
20302
  }
18957
20303
  function isRecord(value) {
18958
20304
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -18960,7 +20306,7 @@ function isRecord(value) {
18960
20306
  async function readSavedCredentials() {
18961
20307
  const filePath = credentialsFilePath();
18962
20308
  try {
18963
- const raw = await fs5.readFile(filePath, "utf8");
20309
+ const raw = await fs6.readFile(filePath, "utf8");
18964
20310
  const parsed = JSON.parse(raw);
18965
20311
  if (!isRecord(parsed)) return null;
18966
20312
  const version = parsed.version;
@@ -18986,7 +20332,7 @@ async function readSavedCredentials() {
18986
20332
  }
18987
20333
  async function writeSavedCredentials(input) {
18988
20334
  const filePath = credentialsFilePath();
18989
- await fs5.mkdir(path6.dirname(filePath), { recursive: true });
20335
+ await fs6.mkdir(path7.dirname(filePath), { recursive: true });
18990
20336
  const now = (/* @__PURE__ */ new Date()).toISOString();
18991
20337
  const existing = await readSavedCredentials();
18992
20338
  const value = {
@@ -18998,9 +20344,9 @@ async function writeSavedCredentials(input) {
18998
20344
  updated_at: now
18999
20345
  };
19000
20346
  const body = JSON.stringify(value, null, 2) + "\n";
19001
- await fs5.writeFile(filePath, body, { encoding: "utf8", mode: 384 });
20347
+ await fs6.writeFile(filePath, body, { encoding: "utf8", mode: 384 });
19002
20348
  try {
19003
- await fs5.chmod(filePath, 384);
20349
+ await fs6.chmod(filePath, 384);
19004
20350
  } catch {
19005
20351
  }
19006
20352
  return { path: filePath, value };
@@ -19045,7 +20391,7 @@ function parseNumberList(input, max) {
19045
20391
  }
19046
20392
  async function fileExists(filePath) {
19047
20393
  try {
19048
- await fs6.stat(filePath);
20394
+ await fs7.stat(filePath);
19049
20395
  return true;
19050
20396
  } catch {
19051
20397
  return false;
@@ -19209,41 +20555,41 @@ function replaceContextStreamBlock2(existing, content) {
19209
20555
  return { content: appended, status: "appended" };
19210
20556
  }
19211
20557
  async function upsertTextFile(filePath, content, _marker) {
19212
- await fs6.mkdir(path7.dirname(filePath), { recursive: true });
20558
+ await fs7.mkdir(path8.dirname(filePath), { recursive: true });
19213
20559
  const exists = await fileExists(filePath);
19214
20560
  const wrappedContent = wrapWithMarkers2(content);
19215
20561
  if (!exists) {
19216
- await fs6.writeFile(filePath, wrappedContent + "\n", "utf8");
20562
+ await fs7.writeFile(filePath, wrappedContent + "\n", "utf8");
19217
20563
  return "created";
19218
20564
  }
19219
- const existing = await fs6.readFile(filePath, "utf8").catch(() => "");
20565
+ const existing = await fs7.readFile(filePath, "utf8").catch(() => "");
19220
20566
  if (!existing.trim()) {
19221
- await fs6.writeFile(filePath, wrappedContent + "\n", "utf8");
20567
+ await fs7.writeFile(filePath, wrappedContent + "\n", "utf8");
19222
20568
  return "updated";
19223
20569
  }
19224
20570
  const replaced = replaceContextStreamBlock2(existing, content);
19225
- await fs6.writeFile(filePath, replaced.content, "utf8");
20571
+ await fs7.writeFile(filePath, replaced.content, "utf8");
19226
20572
  return replaced.status;
19227
20573
  }
19228
20574
  function globalRulesPathForEditor(editor) {
19229
20575
  const home = homedir5();
19230
20576
  switch (editor) {
19231
20577
  case "codex":
19232
- return path7.join(home, ".codex", "AGENTS.md");
20578
+ return path8.join(home, ".codex", "AGENTS.md");
19233
20579
  case "claude":
19234
- return path7.join(home, ".claude", "CLAUDE.md");
20580
+ return path8.join(home, ".claude", "CLAUDE.md");
19235
20581
  case "windsurf":
19236
- return path7.join(home, ".codeium", "windsurf", "memories", "global_rules.md");
20582
+ return path8.join(home, ".codeium", "windsurf", "memories", "global_rules.md");
19237
20583
  case "cline":
19238
- return path7.join(home, "Documents", "Cline", "Rules", "contextstream.md");
20584
+ return path8.join(home, "Documents", "Cline", "Rules", "contextstream.md");
19239
20585
  case "kilo":
19240
- return path7.join(home, ".kilocode", "rules", "contextstream.md");
20586
+ return path8.join(home, ".kilocode", "rules", "contextstream.md");
19241
20587
  case "roo":
19242
- return path7.join(home, ".roo", "rules", "contextstream.md");
20588
+ return path8.join(home, ".roo", "rules", "contextstream.md");
19243
20589
  case "aider":
19244
- return path7.join(home, ".aider.conf.yml");
20590
+ return path8.join(home, ".aider.conf.yml");
19245
20591
  case "antigravity":
19246
- return path7.join(home, ".gemini", "GEMINI.md");
20592
+ return path8.join(home, ".gemini", "GEMINI.md");
19247
20593
  case "cursor":
19248
20594
  return null;
19249
20595
  default:
@@ -19261,40 +20607,40 @@ async function isCodexInstalled() {
19261
20607
  const envHome = process.env.CODEX_HOME;
19262
20608
  const candidates = [
19263
20609
  envHome,
19264
- path7.join(home, ".codex"),
19265
- path7.join(home, ".codex", "config.toml"),
19266
- path7.join(home, ".config", "codex")
20610
+ path8.join(home, ".codex"),
20611
+ path8.join(home, ".codex", "config.toml"),
20612
+ path8.join(home, ".config", "codex")
19267
20613
  ].filter((candidate) => Boolean(candidate));
19268
20614
  return anyPathExists(candidates);
19269
20615
  }
19270
20616
  async function isClaudeInstalled() {
19271
20617
  const home = homedir5();
19272
- const candidates = [path7.join(home, ".claude"), path7.join(home, ".config", "claude")];
20618
+ const candidates = [path8.join(home, ".claude"), path8.join(home, ".config", "claude")];
19273
20619
  const desktopConfig = claudeDesktopConfigPath();
19274
20620
  if (desktopConfig) candidates.push(desktopConfig);
19275
20621
  if (process.platform === "darwin") {
19276
- candidates.push(path7.join(home, "Library", "Application Support", "Claude"));
20622
+ candidates.push(path8.join(home, "Library", "Application Support", "Claude"));
19277
20623
  } else if (process.platform === "win32") {
19278
20624
  const appData = process.env.APPDATA;
19279
- if (appData) candidates.push(path7.join(appData, "Claude"));
20625
+ if (appData) candidates.push(path8.join(appData, "Claude"));
19280
20626
  }
19281
20627
  return anyPathExists(candidates);
19282
20628
  }
19283
20629
  async function isWindsurfInstalled() {
19284
20630
  const home = homedir5();
19285
20631
  const candidates = [
19286
- path7.join(home, ".codeium"),
19287
- path7.join(home, ".codeium", "windsurf"),
19288
- path7.join(home, ".config", "codeium")
20632
+ path8.join(home, ".codeium"),
20633
+ path8.join(home, ".codeium", "windsurf"),
20634
+ path8.join(home, ".config", "codeium")
19289
20635
  ];
19290
20636
  if (process.platform === "darwin") {
19291
- candidates.push(path7.join(home, "Library", "Application Support", "Windsurf"));
19292
- candidates.push(path7.join(home, "Library", "Application Support", "Codeium"));
20637
+ candidates.push(path8.join(home, "Library", "Application Support", "Windsurf"));
20638
+ candidates.push(path8.join(home, "Library", "Application Support", "Codeium"));
19293
20639
  } else if (process.platform === "win32") {
19294
20640
  const appData = process.env.APPDATA;
19295
20641
  if (appData) {
19296
- candidates.push(path7.join(appData, "Windsurf"));
19297
- candidates.push(path7.join(appData, "Codeium"));
20642
+ candidates.push(path8.join(appData, "Windsurf"));
20643
+ candidates.push(path8.join(appData, "Codeium"));
19298
20644
  }
19299
20645
  }
19300
20646
  return anyPathExists(candidates);
@@ -19302,42 +20648,42 @@ async function isWindsurfInstalled() {
19302
20648
  async function isClineInstalled() {
19303
20649
  const home = homedir5();
19304
20650
  const candidates = [
19305
- path7.join(home, "Documents", "Cline"),
19306
- path7.join(home, ".cline"),
19307
- path7.join(home, ".config", "cline")
20651
+ path8.join(home, "Documents", "Cline"),
20652
+ path8.join(home, ".cline"),
20653
+ path8.join(home, ".config", "cline")
19308
20654
  ];
19309
20655
  return anyPathExists(candidates);
19310
20656
  }
19311
20657
  async function isKiloInstalled() {
19312
20658
  const home = homedir5();
19313
- const candidates = [path7.join(home, ".kilocode"), path7.join(home, ".config", "kilocode")];
20659
+ const candidates = [path8.join(home, ".kilocode"), path8.join(home, ".config", "kilocode")];
19314
20660
  return anyPathExists(candidates);
19315
20661
  }
19316
20662
  async function isRooInstalled() {
19317
20663
  const home = homedir5();
19318
- const candidates = [path7.join(home, ".roo"), path7.join(home, ".config", "roo")];
20664
+ const candidates = [path8.join(home, ".roo"), path8.join(home, ".config", "roo")];
19319
20665
  return anyPathExists(candidates);
19320
20666
  }
19321
20667
  async function isAiderInstalled() {
19322
20668
  const home = homedir5();
19323
- const candidates = [path7.join(home, ".aider.conf.yml"), path7.join(home, ".config", "aider")];
20669
+ const candidates = [path8.join(home, ".aider.conf.yml"), path8.join(home, ".config", "aider")];
19324
20670
  return anyPathExists(candidates);
19325
20671
  }
19326
20672
  async function isCursorInstalled() {
19327
20673
  const home = homedir5();
19328
- const candidates = [path7.join(home, ".cursor")];
20674
+ const candidates = [path8.join(home, ".cursor")];
19329
20675
  if (process.platform === "darwin") {
19330
20676
  candidates.push("/Applications/Cursor.app");
19331
- candidates.push(path7.join(home, "Applications", "Cursor.app"));
19332
- candidates.push(path7.join(home, "Library", "Application Support", "Cursor"));
20677
+ candidates.push(path8.join(home, "Applications", "Cursor.app"));
20678
+ candidates.push(path8.join(home, "Library", "Application Support", "Cursor"));
19333
20679
  } else if (process.platform === "win32") {
19334
20680
  const localApp = process.env.LOCALAPPDATA;
19335
20681
  const programFiles = process.env.ProgramFiles;
19336
20682
  const programFilesX86 = process.env["ProgramFiles(x86)"];
19337
- if (localApp) candidates.push(path7.join(localApp, "Programs", "Cursor", "Cursor.exe"));
19338
- if (localApp) candidates.push(path7.join(localApp, "Cursor", "Cursor.exe"));
19339
- if (programFiles) candidates.push(path7.join(programFiles, "Cursor", "Cursor.exe"));
19340
- if (programFilesX86) candidates.push(path7.join(programFilesX86, "Cursor", "Cursor.exe"));
20683
+ if (localApp) candidates.push(path8.join(localApp, "Programs", "Cursor", "Cursor.exe"));
20684
+ if (localApp) candidates.push(path8.join(localApp, "Cursor", "Cursor.exe"));
20685
+ if (programFiles) candidates.push(path8.join(programFiles, "Cursor", "Cursor.exe"));
20686
+ if (programFilesX86) candidates.push(path8.join(programFilesX86, "Cursor", "Cursor.exe"));
19341
20687
  } else {
19342
20688
  candidates.push("/usr/bin/cursor");
19343
20689
  candidates.push("/usr/local/bin/cursor");
@@ -19348,19 +20694,19 @@ async function isCursorInstalled() {
19348
20694
  }
19349
20695
  async function isAntigravityInstalled() {
19350
20696
  const home = homedir5();
19351
- const candidates = [path7.join(home, ".gemini")];
20697
+ const candidates = [path8.join(home, ".gemini")];
19352
20698
  if (process.platform === "darwin") {
19353
20699
  candidates.push("/Applications/Antigravity.app");
19354
- candidates.push(path7.join(home, "Applications", "Antigravity.app"));
19355
- candidates.push(path7.join(home, "Library", "Application Support", "Antigravity"));
20700
+ candidates.push(path8.join(home, "Applications", "Antigravity.app"));
20701
+ candidates.push(path8.join(home, "Library", "Application Support", "Antigravity"));
19356
20702
  } else if (process.platform === "win32") {
19357
20703
  const localApp = process.env.LOCALAPPDATA;
19358
20704
  const programFiles = process.env.ProgramFiles;
19359
20705
  const programFilesX86 = process.env["ProgramFiles(x86)"];
19360
- if (localApp) candidates.push(path7.join(localApp, "Programs", "Antigravity", "Antigravity.exe"));
19361
- if (localApp) candidates.push(path7.join(localApp, "Antigravity", "Antigravity.exe"));
19362
- if (programFiles) candidates.push(path7.join(programFiles, "Antigravity", "Antigravity.exe"));
19363
- if (programFilesX86) candidates.push(path7.join(programFilesX86, "Antigravity", "Antigravity.exe"));
20706
+ if (localApp) candidates.push(path8.join(localApp, "Programs", "Antigravity", "Antigravity.exe"));
20707
+ if (localApp) candidates.push(path8.join(localApp, "Antigravity", "Antigravity.exe"));
20708
+ if (programFiles) candidates.push(path8.join(programFiles, "Antigravity", "Antigravity.exe"));
20709
+ if (programFilesX86) candidates.push(path8.join(programFilesX86, "Antigravity", "Antigravity.exe"));
19364
20710
  } else {
19365
20711
  candidates.push("/usr/bin/antigravity");
19366
20712
  candidates.push("/usr/local/bin/antigravity");
@@ -19403,6 +20749,9 @@ function buildContextStreamMcpServer(params) {
19403
20749
  env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
19404
20750
  }
19405
20751
  env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
20752
+ if (params.showTiming) {
20753
+ env.CONTEXTSTREAM_SHOW_TIMING = "true";
20754
+ }
19406
20755
  if (IS_WINDOWS) {
19407
20756
  return {
19408
20757
  command: "cmd",
@@ -19425,6 +20774,9 @@ function buildContextStreamVsCodeServer(params) {
19425
20774
  env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
19426
20775
  }
19427
20776
  env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
20777
+ if (params.showTiming) {
20778
+ env.CONTEXTSTREAM_SHOW_TIMING = "true";
20779
+ }
19428
20780
  if (IS_WINDOWS) {
19429
20781
  return {
19430
20782
  type: "stdio",
@@ -19459,11 +20811,11 @@ function tryParseJsonLike(raw) {
19459
20811
  }
19460
20812
  }
19461
20813
  async function upsertJsonMcpConfig(filePath, server) {
19462
- await fs6.mkdir(path7.dirname(filePath), { recursive: true });
20814
+ await fs7.mkdir(path8.dirname(filePath), { recursive: true });
19463
20815
  const exists = await fileExists(filePath);
19464
20816
  let root = {};
19465
20817
  if (exists) {
19466
- const raw = await fs6.readFile(filePath, "utf8").catch(() => "");
20818
+ const raw = await fs7.readFile(filePath, "utf8").catch(() => "");
19467
20819
  const parsed = tryParseJsonLike(raw);
19468
20820
  if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
19469
20821
  root = parsed.value;
@@ -19474,16 +20826,16 @@ async function upsertJsonMcpConfig(filePath, server) {
19474
20826
  const before = JSON.stringify(root.mcpServers.contextstream ?? null);
19475
20827
  root.mcpServers.contextstream = server;
19476
20828
  const after = JSON.stringify(root.mcpServers.contextstream ?? null);
19477
- await fs6.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
20829
+ await fs7.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
19478
20830
  if (!exists) return "created";
19479
20831
  return before === after ? "skipped" : "updated";
19480
20832
  }
19481
20833
  async function upsertJsonVsCodeMcpConfig(filePath, server) {
19482
- await fs6.mkdir(path7.dirname(filePath), { recursive: true });
20834
+ await fs7.mkdir(path8.dirname(filePath), { recursive: true });
19483
20835
  const exists = await fileExists(filePath);
19484
20836
  let root = {};
19485
20837
  if (exists) {
19486
- const raw = await fs6.readFile(filePath, "utf8").catch(() => "");
20838
+ const raw = await fs7.readFile(filePath, "utf8").catch(() => "");
19487
20839
  const parsed = tryParseJsonLike(raw);
19488
20840
  if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
19489
20841
  root = parsed.value;
@@ -19494,14 +20846,14 @@ async function upsertJsonVsCodeMcpConfig(filePath, server) {
19494
20846
  const before = JSON.stringify(root.servers.contextstream ?? null);
19495
20847
  root.servers.contextstream = server;
19496
20848
  const after = JSON.stringify(root.servers.contextstream ?? null);
19497
- await fs6.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
20849
+ await fs7.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
19498
20850
  if (!exists) return "created";
19499
20851
  return before === after ? "skipped" : "updated";
19500
20852
  }
19501
20853
  function claudeDesktopConfigPath() {
19502
20854
  const home = homedir5();
19503
20855
  if (process.platform === "darwin") {
19504
- return path7.join(
20856
+ return path8.join(
19505
20857
  home,
19506
20858
  "Library",
19507
20859
  "Application Support",
@@ -19510,21 +20862,23 @@ function claudeDesktopConfigPath() {
19510
20862
  );
19511
20863
  }
19512
20864
  if (process.platform === "win32") {
19513
- const appData = process.env.APPDATA || path7.join(home, "AppData", "Roaming");
19514
- return path7.join(appData, "Claude", "claude_desktop_config.json");
20865
+ const appData = process.env.APPDATA || path8.join(home, "AppData", "Roaming");
20866
+ return path8.join(appData, "Claude", "claude_desktop_config.json");
19515
20867
  }
19516
20868
  return null;
19517
20869
  }
19518
20870
  async function upsertCodexTomlConfig(filePath, params) {
19519
- await fs6.mkdir(path7.dirname(filePath), { recursive: true });
20871
+ await fs7.mkdir(path8.dirname(filePath), { recursive: true });
19520
20872
  const exists = await fileExists(filePath);
19521
- const existing = exists ? await fs6.readFile(filePath, "utf8").catch(() => "") : "";
20873
+ const existing = exists ? await fs7.readFile(filePath, "utf8").catch(() => "") : "";
19522
20874
  const marker = "[mcp_servers.contextstream]";
19523
20875
  const envMarker = "[mcp_servers.contextstream.env]";
19524
20876
  const toolsetLine = params.toolset === "router" ? `CONTEXTSTREAM_PROGRESSIVE_MODE = "true"
19525
20877
  ` : "";
19526
20878
  const contextPackLine = `CONTEXTSTREAM_CONTEXT_PACK = "${params.contextPackEnabled === false ? "false" : "true"}"
19527
20879
  `;
20880
+ const showTimingLine = params.showTiming ? `CONTEXTSTREAM_SHOW_TIMING = "true"
20881
+ ` : "";
19528
20882
  const commandLine = IS_WINDOWS ? `command = "cmd"
19529
20883
  args = ["/c", "npx", "-y", "@contextstream/mcp-server"]
19530
20884
  ` : `command = "npx"
@@ -19538,17 +20892,17 @@ args = ["-y", "@contextstream/mcp-server"]
19538
20892
  [mcp_servers.contextstream.env]
19539
20893
  CONTEXTSTREAM_API_URL = "${params.apiUrl}"
19540
20894
  CONTEXTSTREAM_API_KEY = "${params.apiKey}"
19541
- ` + toolsetLine + contextPackLine;
20895
+ ` + toolsetLine + contextPackLine + showTimingLine;
19542
20896
  if (!exists) {
19543
- await fs6.writeFile(filePath, block.trimStart(), "utf8");
20897
+ await fs7.writeFile(filePath, block.trimStart(), "utf8");
19544
20898
  return "created";
19545
20899
  }
19546
20900
  if (!existing.includes(marker)) {
19547
- await fs6.writeFile(filePath, existing.trimEnd() + block, "utf8");
20901
+ await fs7.writeFile(filePath, existing.trimEnd() + block, "utf8");
19548
20902
  return "updated";
19549
20903
  }
19550
20904
  if (!existing.includes(envMarker)) {
19551
- await fs6.writeFile(
20905
+ await fs7.writeFile(
19552
20906
  filePath,
19553
20907
  existing.trimEnd() + "\n\n" + envMarker + `
19554
20908
  CONTEXTSTREAM_API_URL = "${params.apiUrl}"
@@ -19609,18 +20963,18 @@ CONTEXTSTREAM_API_KEY = "${params.apiKey}"
19609
20963
  }
19610
20964
  const updated = out.join("\n");
19611
20965
  if (updated === existing) return "skipped";
19612
- await fs6.writeFile(filePath, updated, "utf8");
20966
+ await fs7.writeFile(filePath, updated, "utf8");
19613
20967
  return "updated";
19614
20968
  }
19615
20969
  async function discoverProjectsUnderFolder(parentFolder) {
19616
- const entries = await fs6.readdir(parentFolder, { withFileTypes: true });
19617
- const candidates = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => path7.join(parentFolder, e.name));
20970
+ const entries = await fs7.readdir(parentFolder, { withFileTypes: true });
20971
+ const candidates = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => path8.join(parentFolder, e.name));
19618
20972
  const projects = [];
19619
20973
  for (const dir of candidates) {
19620
- const hasGit = await fileExists(path7.join(dir, ".git"));
19621
- const hasPkg = await fileExists(path7.join(dir, "package.json"));
19622
- const hasCargo = await fileExists(path7.join(dir, "Cargo.toml"));
19623
- const hasPyProject = await fileExists(path7.join(dir, "pyproject.toml"));
20974
+ const hasGit = await fileExists(path8.join(dir, ".git"));
20975
+ const hasPkg = await fileExists(path8.join(dir, "package.json"));
20976
+ const hasCargo = await fileExists(path8.join(dir, "Cargo.toml"));
20977
+ const hasPyProject = await fileExists(path8.join(dir, "pyproject.toml"));
19624
20978
  if (hasGit || hasPkg || hasCargo || hasPyProject) projects.push(dir);
19625
20979
  }
19626
20980
  return projects;
@@ -19895,6 +21249,11 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
19895
21249
  console.log(" Uses more operations/credits; can be disabled in settings or via env.");
19896
21250
  const contextPackChoice = normalizeInput(await rl.question("Enable Context Pack? [Y/n]: "));
19897
21251
  const contextPackEnabled = !(contextPackChoice.toLowerCase() === "n" || contextPackChoice.toLowerCase() === "no");
21252
+ console.log("\nResponse Timing:");
21253
+ console.log(" Show response time for tool calls (e.g., '\u2713 3 results in 142ms').");
21254
+ console.log(" Useful for debugging performance; disabled by default.");
21255
+ const showTimingChoice = normalizeInput(await rl.question("Show response timing? [y/N]: "));
21256
+ const showTiming = showTimingChoice.toLowerCase() === "y" || showTimingChoice.toLowerCase() === "yes";
19898
21257
  const editors = [
19899
21258
  "codex",
19900
21259
  "claude",
@@ -19969,18 +21328,20 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
19969
21328
  )
19970
21329
  ) || mcpChoiceDefault;
19971
21330
  const mcpScope = mcpChoice === "2" && hasCodex && !hasProjectMcpEditors ? "skip" : mcpChoice === "4" ? "skip" : mcpChoice === "1" ? "global" : mcpChoice === "2" ? "project" : "both";
19972
- const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, toolset, contextPackEnabled });
21331
+ const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, toolset, contextPackEnabled, showTiming });
19973
21332
  const mcpServerClaude = buildContextStreamMcpServer({
19974
21333
  apiUrl,
19975
21334
  apiKey,
19976
21335
  toolset,
19977
- contextPackEnabled
21336
+ contextPackEnabled,
21337
+ showTiming
19978
21338
  });
19979
21339
  const vsCodeServer = buildContextStreamVsCodeServer({
19980
21340
  apiUrl,
19981
21341
  apiKey,
19982
21342
  toolset,
19983
- contextPackEnabled
21343
+ contextPackEnabled,
21344
+ showTiming
19984
21345
  });
19985
21346
  const needsGlobalMcpConfig = mcpScope === "global" || mcpScope === "both" || mcpScope === "project" && hasCodex;
19986
21347
  if (needsGlobalMcpConfig) {
@@ -19989,7 +21350,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
19989
21350
  if (mcpScope === "project" && editor !== "codex") continue;
19990
21351
  try {
19991
21352
  if (editor === "codex") {
19992
- const filePath = path7.join(homedir5(), ".codex", "config.toml");
21353
+ const filePath = path8.join(homedir5(), ".codex", "config.toml");
19993
21354
  if (dryRun) {
19994
21355
  writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
19995
21356
  console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
@@ -19999,14 +21360,15 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
19999
21360
  apiUrl,
20000
21361
  apiKey,
20001
21362
  toolset,
20002
- contextPackEnabled
21363
+ contextPackEnabled,
21364
+ showTiming
20003
21365
  });
20004
21366
  writeActions.push({ kind: "mcp-config", target: filePath, status });
20005
21367
  console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
20006
21368
  continue;
20007
21369
  }
20008
21370
  if (editor === "windsurf") {
20009
- const filePath = path7.join(homedir5(), ".codeium", "windsurf", "mcp_config.json");
21371
+ const filePath = path8.join(homedir5(), ".codeium", "windsurf", "mcp_config.json");
20010
21372
  if (dryRun) {
20011
21373
  writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
20012
21374
  console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
@@ -20048,7 +21410,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
20048
21410
  continue;
20049
21411
  }
20050
21412
  if (editor === "cursor") {
20051
- const filePath = path7.join(homedir5(), ".cursor", "mcp.json");
21413
+ const filePath = path8.join(homedir5(), ".cursor", "mcp.json");
20052
21414
  if (dryRun) {
20053
21415
  writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
20054
21416
  console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
@@ -20106,9 +21468,9 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
20106
21468
  if (dryRun) {
20107
21469
  console.log("- Would install hooks to ~/.claude/hooks/");
20108
21470
  console.log("- Would update ~/.claude/settings.json");
20109
- writeActions.push({ kind: "mcp-config", target: path7.join(homedir5(), ".claude", "hooks", "contextstream-redirect.py"), status: "dry-run" });
20110
- writeActions.push({ kind: "mcp-config", target: path7.join(homedir5(), ".claude", "hooks", "contextstream-reminder.py"), status: "dry-run" });
20111
- writeActions.push({ kind: "mcp-config", target: path7.join(homedir5(), ".claude", "settings.json"), status: "dry-run" });
21471
+ writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "hooks", "contextstream-redirect.py"), status: "dry-run" });
21472
+ writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "hooks", "contextstream-reminder.py"), status: "dry-run" });
21473
+ writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "settings.json"), status: "dry-run" });
20112
21474
  } else {
20113
21475
  const result = await installClaudeCodeHooks({ scope: "user" });
20114
21476
  result.scripts.forEach((script) => {
@@ -20130,6 +21492,102 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
20130
21492
  console.log(" Note: Without hooks, Claude may still use default tools instead of ContextStream.");
20131
21493
  }
20132
21494
  }
21495
+ console.log("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
21496
+ console.log("\u2502 Code Privacy & Indexing \u2502");
21497
+ console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
21498
+ console.log("");
21499
+ console.log(" Your code is protected:");
21500
+ console.log(" \u2713 Encrypted in transit (TLS 1.3) and at rest (AES-256)");
21501
+ console.log(" \u2713 Isolated per workspace \u2014 no cross-tenant access");
21502
+ console.log(" \u2713 You can delete your data anytime");
21503
+ console.log("");
21504
+ console.log(" As an additional measure, you can exclude specific files from indexing");
21505
+ console.log(" using .contextstream/ignore (gitignore syntax). Already excluded:");
21506
+ console.log(" \u2022 node_modules, vendor, .git, build outputs, lock files");
21507
+ console.log("");
21508
+ console.log(" Optional: Add patterns for extra-sensitive files like:");
21509
+ console.log(" \u2022 customer-data/ \u2014 client-specific data");
21510
+ console.log(" \u2022 **/*.pem, **/*.key \u2014 private keys & certificates");
21511
+ console.log(" \u2022 src/legacy/ \u2014 code you don't need indexed");
21512
+ console.log("");
21513
+ const createIgnoreFile = normalizeInput(
21514
+ await rl.question("Create a sample .contextstream/ignore file? [y/N]: ")
21515
+ ).toLowerCase();
21516
+ if (createIgnoreFile === "y" || createIgnoreFile === "yes") {
21517
+ const ignoreContent = `# .contextstream/ignore - Additional exclusions from ContextStream indexing
21518
+ # Uses gitignore syntax: https://git-scm.com/docs/gitignore
21519
+ #
21520
+ # Note: Your code is already protected with encryption (TLS 1.3 + AES-256)
21521
+ # and workspace isolation. This file is for extra-sensitive paths you prefer
21522
+ # to keep completely off the index.
21523
+
21524
+ # Customer/sensitive data
21525
+ **/customer-data/
21526
+ **/secrets/
21527
+ **/*.pem
21528
+ **/*.key
21529
+
21530
+ # Large generated files
21531
+ **/generated/
21532
+ **/*.min.js
21533
+ **/*.min.css
21534
+
21535
+ # Test fixtures with sensitive data
21536
+ **/fixtures/production/
21537
+ **/test-data/real/
21538
+
21539
+ # Vendor code you don't want indexed
21540
+ **/third-party/
21541
+ **/external-libs/
21542
+
21543
+ # Specific paths in your project (uncomment as needed)
21544
+ # src/legacy/
21545
+ # docs/internal/
21546
+ `;
21547
+ console.log("\nWhere to create .contextstream/ignore?");
21548
+ console.log(` 1) Current folder (${process.cwd()})`);
21549
+ console.log(" 2) Home folder (applies globally)");
21550
+ console.log(" 3) I'll add it to specific projects later");
21551
+ const ignoreLocation = normalizeInput(await rl.question("Choose [1/2/3] (default 1): ")) || "1";
21552
+ if (ignoreLocation === "1" || ignoreLocation === "2") {
21553
+ const baseDir = ignoreLocation === "1" ? process.cwd() : homedir5();
21554
+ const ignoreDir = path8.join(baseDir, ".contextstream");
21555
+ const ignorePath = path8.join(ignoreDir, "ignore");
21556
+ if (dryRun) {
21557
+ writeActions.push({ kind: "rules", target: ignorePath, status: "dry-run" });
21558
+ console.log(`- Would create ${ignorePath}`);
21559
+ } else {
21560
+ try {
21561
+ await fs7.mkdir(ignoreDir, { recursive: true });
21562
+ const exists = await fileExists(ignorePath);
21563
+ if (exists) {
21564
+ const overwrite = normalizeInput(
21565
+ await rl.question(`${ignorePath} already exists. Overwrite? [y/N]: `)
21566
+ ).toLowerCase();
21567
+ if (overwrite !== "y" && overwrite !== "yes") {
21568
+ console.log("- Skipped (file exists)");
21569
+ } else {
21570
+ await fs7.writeFile(ignorePath, ignoreContent, "utf-8");
21571
+ writeActions.push({ kind: "rules", target: ignorePath, status: "updated" });
21572
+ console.log(`- Updated ${ignorePath}`);
21573
+ }
21574
+ } else {
21575
+ await fs7.writeFile(ignorePath, ignoreContent, "utf-8");
21576
+ writeActions.push({ kind: "rules", target: ignorePath, status: "created" });
21577
+ console.log(`- Created ${ignorePath}`);
21578
+ }
21579
+ } catch (err) {
21580
+ const message = err instanceof Error ? err.message : String(err);
21581
+ console.log(`- Failed to create ignore file: ${message}`);
21582
+ }
21583
+ }
21584
+ } else {
21585
+ console.log("- Skipped. Add .contextstream/ignore to your projects as needed.");
21586
+ }
21587
+ } else {
21588
+ console.log("- Using default exclusions only.");
21589
+ console.log(" Tip: Create .contextstream/ignore in any project to customize.");
21590
+ }
20133
21591
  if (scope === "global" || scope === "both") {
20134
21592
  console.log("\nInstalling global rules...");
20135
21593
  for (const editor of configuredEditors) {
@@ -20170,7 +21628,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
20170
21628
  await rl.question(`Add current folder as a project? [Y/n] (${process.cwd()}): `)
20171
21629
  );
20172
21630
  if (addCwd.toLowerCase() !== "n" && addCwd.toLowerCase() !== "no") {
20173
- projectPaths.add(path7.resolve(process.cwd()));
21631
+ projectPaths.add(path8.resolve(process.cwd()));
20174
21632
  }
20175
21633
  while (true) {
20176
21634
  console.log("\n 1) Add another project path");
@@ -20180,13 +21638,13 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
20180
21638
  if (choice === "3") break;
20181
21639
  if (choice === "1") {
20182
21640
  const p = normalizeInput(await rl.question("Project folder path: "));
20183
- if (p) projectPaths.add(path7.resolve(p));
21641
+ if (p) projectPaths.add(path8.resolve(p));
20184
21642
  continue;
20185
21643
  }
20186
21644
  if (choice === "2") {
20187
21645
  const parent = normalizeInput(await rl.question("Parent folder path: "));
20188
21646
  if (!parent) continue;
20189
- const parentAbs = path7.resolve(parent);
21647
+ const parentAbs = path8.resolve(parent);
20190
21648
  const projects2 = await discoverProjectsUnderFolder(parentAbs);
20191
21649
  if (projects2.length === 0) {
20192
21650
  console.log(
@@ -20218,11 +21676,16 @@ Applying to ${projects.length} project(s)...`);
20218
21676
  folder_path: projectPath,
20219
21677
  workspace_id: workspaceId,
20220
21678
  workspace_name: workspaceName,
20221
- create_parent_mapping: createParentMapping
21679
+ create_parent_mapping: createParentMapping,
21680
+ // Include version and config info for desktop app compatibility
21681
+ version: VERSION,
21682
+ configured_editors: configuredEditors,
21683
+ context_pack: contextPackEnabled,
21684
+ api_url: apiUrl
20222
21685
  });
20223
21686
  writeActions.push({
20224
21687
  kind: "workspace-config",
20225
- target: path7.join(projectPath, ".contextstream", "config.json"),
21688
+ target: path8.join(projectPath, ".contextstream", "config.json"),
20226
21689
  status: "created"
20227
21690
  });
20228
21691
  console.log(`- Linked workspace in ${projectPath}`);
@@ -20233,7 +21696,7 @@ Applying to ${projects.length} project(s)...`);
20233
21696
  } else if (workspaceId && workspaceId !== "dry-run" && workspaceName && dryRun) {
20234
21697
  writeActions.push({
20235
21698
  kind: "workspace-config",
20236
- target: path7.join(projectPath, ".contextstream", "config.json"),
21699
+ target: path8.join(projectPath, ".contextstream", "config.json"),
20237
21700
  status: "dry-run"
20238
21701
  });
20239
21702
  }
@@ -20241,8 +21704,8 @@ Applying to ${projects.length} project(s)...`);
20241
21704
  for (const editor of configuredEditors) {
20242
21705
  try {
20243
21706
  if (editor === "cursor") {
20244
- const cursorPath = path7.join(projectPath, ".cursor", "mcp.json");
20245
- const vscodePath = path7.join(projectPath, ".vscode", "mcp.json");
21707
+ const cursorPath = path8.join(projectPath, ".cursor", "mcp.json");
21708
+ const vscodePath = path8.join(projectPath, ".vscode", "mcp.json");
20246
21709
  if (dryRun) {
20247
21710
  writeActions.push({ kind: "mcp-config", target: cursorPath, status: "dry-run" });
20248
21711
  writeActions.push({ kind: "mcp-config", target: vscodePath, status: "dry-run" });
@@ -20255,7 +21718,7 @@ Applying to ${projects.length} project(s)...`);
20255
21718
  continue;
20256
21719
  }
20257
21720
  if (editor === "claude") {
20258
- const mcpPath = path7.join(projectPath, ".mcp.json");
21721
+ const mcpPath = path8.join(projectPath, ".mcp.json");
20259
21722
  if (dryRun) {
20260
21723
  writeActions.push({ kind: "mcp-config", target: mcpPath, status: "dry-run" });
20261
21724
  } else {
@@ -20265,7 +21728,7 @@ Applying to ${projects.length} project(s)...`);
20265
21728
  continue;
20266
21729
  }
20267
21730
  if (editor === "kilo") {
20268
- const kiloPath = path7.join(projectPath, ".kilocode", "mcp.json");
21731
+ const kiloPath = path8.join(projectPath, ".kilocode", "mcp.json");
20269
21732
  if (dryRun) {
20270
21733
  writeActions.push({ kind: "mcp-config", target: kiloPath, status: "dry-run" });
20271
21734
  } else {
@@ -20275,7 +21738,7 @@ Applying to ${projects.length} project(s)...`);
20275
21738
  continue;
20276
21739
  }
20277
21740
  if (editor === "roo") {
20278
- const rooPath = path7.join(projectPath, ".roo", "mcp.json");
21741
+ const rooPath = path8.join(projectPath, ".roo", "mcp.json");
20279
21742
  if (dryRun) {
20280
21743
  writeActions.push({ kind: "mcp-config", target: rooPath, status: "dry-run" });
20281
21744
  } else {
@@ -20298,11 +21761,11 @@ Applying to ${projects.length} project(s)...`);
20298
21761
  const rule = generateRuleContent(editor, {
20299
21762
  workspaceName,
20300
21763
  workspaceId: workspaceId && workspaceId !== "dry-run" ? workspaceId : void 0,
20301
- projectName: path7.basename(projectPath),
21764
+ projectName: path8.basename(projectPath),
20302
21765
  mode
20303
21766
  });
20304
21767
  if (!rule) continue;
20305
- const filePath = path7.join(projectPath, rule.filename);
21768
+ const filePath = path8.join(projectPath, rule.filename);
20306
21769
  if (dryRun) {
20307
21770
  writeActions.push({ kind: "rules", target: filePath, status: "dry-run" });
20308
21771
  continue;
@@ -20335,6 +21798,7 @@ Applying to ${projects.length} project(s)...`);
20335
21798
  console.log(`Toolset: ${toolset} (${toolsetDesc})`);
20336
21799
  console.log(`Token reduction: ~75% compared to previous versions.`);
20337
21800
  console.log(`Context Pack: ${contextPackEnabled ? "enabled" : "disabled"}`);
21801
+ console.log(`Response Timing: ${showTiming ? "enabled" : "disabled"}`);
20338
21802
  }
20339
21803
  console.log("\nNext steps:");
20340
21804
  console.log("- Restart your editor/CLI after changing MCP config or rules.");
@@ -20350,6 +21814,9 @@ Applying to ${projects.length} project(s)...`);
20350
21814
  console.log(
20351
21815
  "- Toggle Context Pack with CONTEXTSTREAM_CONTEXT_PACK=true|false (and in dashboard settings)."
20352
21816
  );
21817
+ console.log(
21818
+ "- Toggle Response Timing with CONTEXTSTREAM_SHOW_TIMING=true|false."
21819
+ );
20353
21820
  console.log("");
20354
21821
  console.log("You're set up! Now try these prompts in your AI tool:");
20355
21822
  console.log(' 1) "session summary"');
@@ -20365,8 +21832,8 @@ Applying to ${projects.length} project(s)...`);
20365
21832
  // src/index.ts
20366
21833
  var ENABLE_PROMPTS2 = (process.env.CONTEXTSTREAM_ENABLE_PROMPTS || "true").toLowerCase() !== "false";
20367
21834
  function showFirstRunMessage() {
20368
- const configDir = join9(homedir6(), ".contextstream");
20369
- const starShownFile = join9(configDir, ".star-shown");
21835
+ const configDir = join10(homedir6(), ".contextstream");
21836
+ const starShownFile = join10(configDir, ".star-shown");
20370
21837
  if (existsSync4(starShownFile)) {
20371
21838
  return;
20372
21839
  }