@contextstream/mcp-server 0.4.81 → 0.4.82

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
@@ -11,11 +11,20 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
11
11
  if (typeof require !== "undefined") return require.apply(this, arguments);
12
12
  throw Error('Dynamic require of "' + x + '" is not supported');
13
13
  });
14
- var __esm = (fn, res) => function __init() {
15
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
14
+ var __esm = (fn, res, err) => function __init() {
15
+ if (err) throw err[0];
16
+ try {
17
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
18
+ } catch (e) {
19
+ throw err = [e], e;
20
+ }
16
21
  };
17
22
  var __commonJS = (cb, mod) => function __require2() {
18
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
23
+ try {
24
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
25
+ } catch (e) {
26
+ throw mod = 0, e;
27
+ }
19
28
  };
20
29
  var __export = (target, all) => {
21
30
  for (var name in all)
@@ -4441,7 +4450,7 @@ function detectUpdateMethod() {
4441
4450
  return "curl";
4442
4451
  }
4443
4452
  async function runUpdate(method) {
4444
- return new Promise((resolve16, reject) => {
4453
+ return new Promise((resolve17, reject) => {
4445
4454
  let command;
4446
4455
  let args;
4447
4456
  let shell;
@@ -4472,13 +4481,13 @@ async function runUpdate(method) {
4472
4481
  });
4473
4482
  proc.on("close", (code) => {
4474
4483
  if (code === 0) {
4475
- resolve16();
4484
+ resolve17();
4476
4485
  } else {
4477
4486
  reject(new Error(`Update process exited with code ${code}`));
4478
4487
  }
4479
4488
  });
4480
4489
  proc.unref();
4481
- setTimeout(() => resolve16(), 1e3);
4490
+ setTimeout(() => resolve17(), 1e3);
4482
4491
  });
4483
4492
  }
4484
4493
  function writeUpdateMarker(previousVersion, newVersion) {
@@ -4665,7 +4674,7 @@ function setActiveModelId(modelId) {
4665
4674
  activeModelId = trimmed ? trimmed : null;
4666
4675
  }
4667
4676
  async function sleep(ms) {
4668
- return new Promise((resolve16) => setTimeout(resolve16, ms));
4677
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
4669
4678
  }
4670
4679
  async function request(config, path23, options = {}) {
4671
4680
  const { apiUrl, userAgent } = config;
@@ -5330,39 +5339,119 @@ var require_ignore = __commonJS({
5330
5339
  // src/ignore.ts
5331
5340
  import * as fs from "fs";
5332
5341
  import * as path from "path";
5342
+ function isInvariantIgnoredPath(pathname) {
5343
+ const normalized = pathname.replace(/\\/g, "/").replace(/^\.\//, "");
5344
+ const segments = normalized.split("/").filter(Boolean);
5345
+ if (segments.some((segment) => INVARIANT_IGNORED_DIRECTORIES.has(segment.toLowerCase()))) {
5346
+ return true;
5347
+ }
5348
+ const basename5 = (segments.at(-1) || "").toLowerCase();
5349
+ if (INVARIANT_SECRET_FILENAMES.has(basename5)) return true;
5350
+ if (INVARIANT_SECRET_SUFFIXES.some((suffix) => basename5.endsWith(suffix))) return true;
5351
+ return (basename5 === ".env" || basename5.startsWith(".env.")) && !ENV_TEMPLATE_ALLOWLIST.has(basename5);
5352
+ }
5333
5353
  async function loadIgnorePatterns(projectRoot) {
5334
5354
  const ig = (0, import_ignore.default)();
5335
5355
  const patterns = [...DEFAULT_IGNORE_PATTERNS];
5336
5356
  ig.add(DEFAULT_IGNORE_PATTERNS);
5337
- const ignoreFilePath = path.join(projectRoot, IGNORE_FILENAME);
5338
5357
  let hasUserPatterns = false;
5339
- try {
5340
- const content = await fs.promises.readFile(ignoreFilePath, "utf-8");
5341
- const userPatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
5342
- if (userPatterns.length > 0) {
5343
- ig.add(userPatterns);
5344
- patterns.push(...userPatterns);
5345
- hasUserPatterns = true;
5358
+ for (const filename of IGNORE_FILENAMES) {
5359
+ try {
5360
+ const content = await fs.promises.readFile(path.join(projectRoot, filename), "utf-8");
5361
+ const userPatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
5362
+ if (userPatterns.length > 0) {
5363
+ ig.add(userPatterns);
5364
+ patterns.push(...userPatterns);
5365
+ hasUserPatterns = true;
5366
+ }
5367
+ } catch {
5368
+ }
5369
+ }
5370
+ return {
5371
+ ignores: (pathname) => isInvariantIgnoredPath(pathname) || ig.ignores(pathname),
5372
+ patterns,
5373
+ hasUserPatterns
5374
+ };
5375
+ }
5376
+ function loadIgnorePatternsSync(projectRoot) {
5377
+ const ig = (0, import_ignore.default)();
5378
+ const patterns = [...DEFAULT_IGNORE_PATTERNS];
5379
+ ig.add(DEFAULT_IGNORE_PATTERNS);
5380
+ let hasUserPatterns = false;
5381
+ for (const filename of IGNORE_FILENAMES) {
5382
+ try {
5383
+ const content = fs.readFileSync(path.join(projectRoot, filename), "utf-8");
5384
+ const userPatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
5385
+ if (userPatterns.length > 0) {
5386
+ ig.add(userPatterns);
5387
+ patterns.push(...userPatterns);
5388
+ hasUserPatterns = true;
5389
+ }
5390
+ } catch {
5346
5391
  }
5347
- } catch {
5348
5392
  }
5349
5393
  return {
5350
- ignores: (pathname) => ig.ignores(pathname),
5394
+ ignores: (pathname) => isInvariantIgnoredPath(pathname) || ig.ignores(pathname),
5351
5395
  patterns,
5352
5396
  hasUserPatterns
5353
5397
  };
5354
5398
  }
5355
- var import_ignore, IGNORE_FILENAME, DEFAULT_IGNORE_PATTERNS;
5399
+ var import_ignore, IGNORE_FILENAMES, INVARIANT_IGNORED_DIRECTORIES, INVARIANT_SECRET_FILENAMES, INVARIANT_SECRET_SUFFIXES, ENV_TEMPLATE_ALLOWLIST, DEFAULT_IGNORE_PATTERNS;
5356
5400
  var init_ignore = __esm({
5357
5401
  "src/ignore.ts"() {
5358
5402
  "use strict";
5359
5403
  import_ignore = __toESM(require_ignore(), 1);
5360
- IGNORE_FILENAME = ".contextstream/ignore";
5404
+ IGNORE_FILENAMES = [".gitignore", ".contextignore", ".contextstream/ignore"];
5405
+ INVARIANT_IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
5406
+ ".contextstream",
5407
+ ".claude",
5408
+ ".codex",
5409
+ ".cursor",
5410
+ ".windsurf",
5411
+ ".roo",
5412
+ ".kilocode",
5413
+ ".ssh",
5414
+ ".aws",
5415
+ ".gnupg",
5416
+ ".kube",
5417
+ ".docker"
5418
+ ]);
5419
+ INVARIANT_SECRET_FILENAMES = /* @__PURE__ */ new Set([
5420
+ ".npmrc",
5421
+ ".pypirc",
5422
+ ".netrc",
5423
+ ".pgpass",
5424
+ ".git-credentials",
5425
+ "id_rsa",
5426
+ "id_ed25519",
5427
+ "id_ecdsa",
5428
+ "known_hosts",
5429
+ "credentials.json",
5430
+ "serviceaccountkey.json",
5431
+ "auth.json"
5432
+ ]);
5433
+ INVARIANT_SECRET_SUFFIXES = [".pem", ".key", ".p12", ".pfx", ".jks"];
5434
+ ENV_TEMPLATE_ALLOWLIST = /* @__PURE__ */ new Set([
5435
+ ".env.example",
5436
+ ".env.sample",
5437
+ ".env.template",
5438
+ ".env.dist",
5439
+ ".env.defaults"
5440
+ ]);
5361
5441
  DEFAULT_IGNORE_PATTERNS = [
5362
5442
  // Version control
5363
5443
  ".git/",
5364
5444
  ".svn/",
5365
5445
  ".hg/",
5446
+ // ContextStream and AI editor/agent state. These directories can contain
5447
+ // credentials, generated rules, caches, and nested worktrees.
5448
+ ".contextstream/",
5449
+ ".claude/",
5450
+ ".codex/",
5451
+ ".cursor/",
5452
+ ".windsurf/",
5453
+ ".roo/",
5454
+ ".kilocode/",
5366
5455
  // Package managers / dependencies
5367
5456
  "node_modules/",
5368
5457
  "vendor/",
@@ -5393,6 +5482,12 @@ var init_ignore = __esm({
5393
5482
  ".idea/",
5394
5483
  ".vscode/",
5395
5484
  ".vs/",
5485
+ // Credential stores
5486
+ ".ssh/",
5487
+ ".aws/",
5488
+ ".gnupg/",
5489
+ ".kube/",
5490
+ ".docker/",
5396
5491
  // Coverage
5397
5492
  "coverage/",
5398
5493
  ".coverage/",
@@ -5406,6 +5501,22 @@ var init_ignore = __esm({
5406
5501
  "composer.lock",
5407
5502
  "*.min.js",
5408
5503
  "*.min.css",
5504
+ "*.pem",
5505
+ "*.key",
5506
+ "*.p12",
5507
+ "*.pfx",
5508
+ "*.jks",
5509
+ ".npmrc",
5510
+ ".pypirc",
5511
+ ".netrc",
5512
+ ".pgpass",
5513
+ ".git-credentials",
5514
+ "id_rsa",
5515
+ "id_ed25519",
5516
+ "id_ecdsa",
5517
+ "credentials.json",
5518
+ "serviceAccountKey.json",
5519
+ "auth.json",
5409
5520
  // OS files
5410
5521
  ".DS_Store",
5411
5522
  "Thumbs.db"
@@ -5433,6 +5544,15 @@ import { exec } from "child_process";
5433
5544
  import { promisify } from "util";
5434
5545
  import * as os from "os";
5435
5546
  import * as crypto from "crypto";
5547
+ function shouldSkipSensitiveFile(name) {
5548
+ const lower = name.toLowerCase();
5549
+ if (SECRET_FILENAMES.has(lower)) return true;
5550
+ if (SECRET_FILE_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return true;
5551
+ if ((lower === ".env" || lower.startsWith(".env.")) && !ENV_TEMPLATE_ALLOWLIST2.has(lower)) {
5552
+ return true;
5553
+ }
5554
+ return false;
5555
+ }
5436
5556
  function getMachineId() {
5437
5557
  const hostname2 = os.hostname();
5438
5558
  const hash = crypto.createHash("sha256").update(hostname2).digest("hex");
@@ -5539,6 +5659,7 @@ function clearGitContextCache() {
5539
5659
  gitContextCache.clear();
5540
5660
  }
5541
5661
  async function readFilesFromDirectory(rootPath, options = {}) {
5662
+ if (isInvariantIgnoredPath(path2.resolve(rootPath))) return [];
5542
5663
  const maxFiles = options.maxFiles ?? MAX_FILES_PER_BATCH;
5543
5664
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
5544
5665
  const files = [];
@@ -5563,6 +5684,7 @@ async function readFilesFromDirectory(rootPath, options = {}) {
5563
5684
  await walkDir(fullPath, relPath);
5564
5685
  } else if (entry.isFile()) {
5565
5686
  if (IGNORE_FILES.has(entry.name)) continue;
5687
+ if (shouldSkipSensitiveFile(entry.name)) continue;
5566
5688
  if (ig.ignores(relPath)) continue;
5567
5689
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
5568
5690
  if (!CODE_EXTENSIONS.has(ext)) continue;
@@ -5598,6 +5720,7 @@ async function readFilesFromDirectory(rootPath, options = {}) {
5598
5720
  return files;
5599
5721
  }
5600
5722
  async function* readAllFilesInBatches(rootPath, options = {}) {
5723
+ if (isInvariantIgnoredPath(path2.resolve(rootPath))) return;
5601
5724
  const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
5602
5725
  const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
5603
5726
  const maxFilesPerBatch = options.maxFilesPerBatch ?? options.batchSize ?? MAX_FILES_PER_BATCH;
@@ -5623,6 +5746,7 @@ async function* readAllFilesInBatches(rootPath, options = {}) {
5623
5746
  yield* walkDir(fullPath, relPath);
5624
5747
  } else if (entry.isFile()) {
5625
5748
  if (IGNORE_FILES.has(entry.name)) continue;
5749
+ if (shouldSkipSensitiveFile(entry.name)) continue;
5626
5750
  if (ig.ignores(relPath)) continue;
5627
5751
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
5628
5752
  if (!CODE_EXTENSIONS.has(ext)) continue;
@@ -5683,6 +5807,7 @@ async function* readAllFilesInBatches(rootPath, options = {}) {
5683
5807
  }
5684
5808
  }
5685
5809
  async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}) {
5810
+ if (isInvariantIgnoredPath(path2.resolve(rootPath))) return;
5686
5811
  const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
5687
5812
  const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
5688
5813
  const maxFilesPerBatch = options.maxFilesPerBatch ?? options.batchSize ?? MAX_FILES_PER_BATCH;
@@ -5711,6 +5836,7 @@ async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}
5711
5836
  yield* walkDir(fullPath, relPath);
5712
5837
  } else if (entry.isFile()) {
5713
5838
  if (IGNORE_FILES.has(entry.name)) continue;
5839
+ if (shouldSkipSensitiveFile(entry.name)) continue;
5714
5840
  if (ig.ignores(relPath)) continue;
5715
5841
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
5716
5842
  if (!CODE_EXTENSIONS.has(ext)) continue;
@@ -5777,6 +5903,7 @@ async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}
5777
5903
  );
5778
5904
  }
5779
5905
  async function countIndexableFiles(rootPath, options = {}) {
5906
+ if (isInvariantIgnoredPath(path2.resolve(rootPath))) return { count: 0, stopped: false };
5780
5907
  const maxFiles = options.maxFiles ?? 1;
5781
5908
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
5782
5909
  const ig = options.ignoreInstance ?? await loadIgnorePatterns(rootPath);
@@ -5807,6 +5934,7 @@ async function countIndexableFiles(rootPath, options = {}) {
5807
5934
  await walkDir(fullPath, relPath);
5808
5935
  } else if (entry.isFile()) {
5809
5936
  if (IGNORE_FILES.has(entry.name)) continue;
5937
+ if (shouldSkipSensitiveFile(entry.name)) continue;
5810
5938
  if (ig.ignores(relPath)) continue;
5811
5939
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
5812
5940
  if (!CODE_EXTENSIONS.has(ext)) continue;
@@ -5891,7 +6019,7 @@ function deleteHashManifest(projectId) {
5891
6019
  } catch {
5892
6020
  }
5893
6021
  }
5894
- var execAsync, CODE_EXTENSIONS, IGNORE_DIRS, IGNORE_FILES, MAX_FILE_SIZE, MAX_BATCH_BYTES, LARGE_FILE_THRESHOLD, MAX_FILES_PER_BATCH, gitContextCache;
6022
+ var execAsync, CODE_EXTENSIONS, IGNORE_DIRS, IGNORE_FILES, SECRET_FILENAMES, SECRET_FILE_SUFFIXES, ENV_TEMPLATE_ALLOWLIST2, MAX_FILE_SIZE, MAX_BATCH_BYTES, LARGE_FILE_THRESHOLD, MAX_FILES_PER_BATCH, gitContextCache;
5895
6023
  var init_files = __esm({
5896
6024
  "src/files.ts"() {
5897
6025
  "use strict";
@@ -5967,6 +6095,13 @@ var init_files = __esm({
5967
6095
  IGNORE_DIRS = /* @__PURE__ */ new Set([
5968
6096
  "node_modules",
5969
6097
  ".git",
6098
+ ".contextstream",
6099
+ ".claude",
6100
+ ".codex",
6101
+ ".cursor",
6102
+ ".windsurf",
6103
+ ".roo",
6104
+ ".kilocode",
5970
6105
  ".svn",
5971
6106
  ".hg",
5972
6107
  "target",
@@ -5994,7 +6129,18 @@ var init_files = __esm({
5994
6129
  "obj",
5995
6130
  ".idea",
5996
6131
  ".vscode",
5997
- ".vs"
6132
+ ".vs",
6133
+ ".npm",
6134
+ ".yarn",
6135
+ ".nyc_output",
6136
+ // Credential directories are invariant exclusions. Their contents often
6137
+ // have ordinary-looking names (config, credentials, known_hosts), so a
6138
+ // filename-only filter is not sufficient.
6139
+ ".ssh",
6140
+ ".aws",
6141
+ ".gnupg",
6142
+ ".kube",
6143
+ ".docker"
5998
6144
  ]);
5999
6145
  IGNORE_FILES = /* @__PURE__ */ new Set([
6000
6146
  ".DS_Store",
@@ -6009,6 +6155,28 @@ var init_files = __esm({
6009
6155
  "Gemfile.lock",
6010
6156
  "composer.lock"
6011
6157
  ]);
6158
+ SECRET_FILENAMES = /* @__PURE__ */ new Set([
6159
+ ".npmrc",
6160
+ ".pypirc",
6161
+ ".netrc",
6162
+ ".pgpass",
6163
+ ".git-credentials",
6164
+ "id_rsa",
6165
+ "id_ed25519",
6166
+ "id_ecdsa",
6167
+ "known_hosts",
6168
+ "credentials.json",
6169
+ "serviceaccountkey.json",
6170
+ "auth.json"
6171
+ ]);
6172
+ SECRET_FILE_SUFFIXES = [".pem", ".key", ".p12", ".pfx", ".jks"];
6173
+ ENV_TEMPLATE_ALLOWLIST2 = /* @__PURE__ */ new Set([
6174
+ ".env.example",
6175
+ ".env.sample",
6176
+ ".env.template",
6177
+ ".env.dist",
6178
+ ".env.defaults"
6179
+ ]);
6012
6180
  MAX_FILE_SIZE = 5 * 1024 * 1024;
6013
6181
  MAX_BATCH_BYTES = 1024 * 1024;
6014
6182
  LARGE_FILE_THRESHOLD = 2 * 1024 * 1024;
@@ -8528,10 +8696,14 @@ function extractIndexTimestamp(result) {
8528
8696
  function apiResultReportsIndexed(result) {
8529
8697
  const candidates = candidateObjects(result);
8530
8698
  const indexed = readBoolean(candidates, "indexed");
8531
- if (indexed !== void 0) {
8532
- return indexed;
8699
+ if (indexed === true) {
8700
+ return true;
8533
8701
  }
8534
8702
  const indexedFiles = readNumber(candidates, ["indexed_files", "indexed_file_count"]) ?? 0;
8703
+ const projectIndexState = readString(candidates, "project_index_state")?.toLowerCase();
8704
+ if (indexed === false && projectIndexState !== "ready" && projectIndexState !== "completed") {
8705
+ return false;
8706
+ }
8535
8707
  if (indexedFiles > 0) {
8536
8708
  return true;
8537
8709
  }
@@ -8544,6 +8716,28 @@ function apiResultReportsIndexed(result) {
8544
8716
  }
8545
8717
  return false;
8546
8718
  }
8719
+ function apiIndexResultIsSkipped(result) {
8720
+ const status = readString(candidateObjects(result), "status")?.toLowerCase();
8721
+ return status === "skipped";
8722
+ }
8723
+ function extractIndexedFileCount(result) {
8724
+ const count = readNumber(candidateObjects(result), ["indexed_files", "indexed_file_count"]);
8725
+ return count !== void 0 && count > 0 ? count : void 0;
8726
+ }
8727
+ function classifyRemoteIndexState(result) {
8728
+ if (apiResultReportsIndexed(result)) return "server_index_ready";
8729
+ if (apiResultIsIndexing(result)) return "server_indexing";
8730
+ return "requires_sync_bridge";
8731
+ }
8732
+ function apiAllowsPathIngest(apiUrl, explicitOptIn = false) {
8733
+ if (explicitOptIn) return true;
8734
+ try {
8735
+ const hostname2 = new URL(apiUrl).hostname.toLowerCase();
8736
+ return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1" || hostname2 === "[::1]";
8737
+ } catch {
8738
+ return false;
8739
+ }
8740
+ }
8547
8741
  function apiResultIsIndexing(result) {
8548
8742
  const candidates = candidateObjects(result);
8549
8743
  const projectIndexState = readString(candidates, "project_index_state")?.toLowerCase();
@@ -10334,6 +10528,11 @@ var init_client = __esm({
10334
10528
  };
10335
10529
  }
10336
10530
  async ingestFromPath(projectId, pathStr, opts) {
10531
+ if (!this.canDelegatePathIngest()) {
10532
+ throw new Error(
10533
+ "Hosted APIs cannot read workstation paths. Index from a process with local checkout access or use the ContextStream sync bridge/editor hooks."
10534
+ );
10535
+ }
10337
10536
  return request(this.config, `/projects/${projectId}/files/ingest-from-path`, {
10338
10537
  body: {
10339
10538
  path: pathStr,
@@ -10343,6 +10542,12 @@ var init_client = __esm({
10343
10542
  }
10344
10543
  });
10345
10544
  }
10545
+ canDelegatePathIngest() {
10546
+ return apiAllowsPathIngest(
10547
+ this.config.apiUrl,
10548
+ process.env.CONTEXTSTREAM_ALLOW_API_PATH_INGEST === "1"
10549
+ );
10550
+ }
10346
10551
  /**
10347
10552
  * High-level local ingest: reads files from disk, applies SHA-256 content
10348
10553
  * hash filtering, batches, and sends to the API with cooldown/status handling.
@@ -10365,6 +10570,9 @@ var init_client = __esm({
10365
10570
  let apiSkipped = 0;
10366
10571
  let abortedEarly = false;
10367
10572
  let lastStatus = "completed";
10573
+ let failedBatches = 0;
10574
+ let failedFiles = 0;
10575
+ let lastError;
10368
10576
  const fileSource = sinceTimestamp ? readChangedFilesInBatches(rootPath, sinceTimestamp, {
10369
10577
  batchSize: INGEST_MAX_BATCH_FILES,
10370
10578
  maxFilesPerBatch: INGEST_MAX_BATCH_FILES,
@@ -10377,19 +10585,21 @@ var init_client = __esm({
10377
10585
  for await (const batch of fileSource) {
10378
10586
  if (maxFiles !== void 0 && totalFiles >= maxFiles) break;
10379
10587
  const filteredBatch = [];
10588
+ const changedHashes = /* @__PURE__ */ new Map();
10380
10589
  for (const file of batch) {
10381
10590
  if (maxFiles !== void 0 && totalFiles >= maxFiles) break;
10382
10591
  totalFiles++;
10383
10592
  const hash = sha256Hex(file.content);
10384
- newHashes.set(file.path, hash);
10385
10593
  if (!force) {
10386
10594
  const oldHash = oldHashes.get(file.path);
10387
10595
  if (oldHash === hash) {
10596
+ newHashes.set(file.path, hash);
10388
10597
  filesSkippedByHash++;
10389
10598
  continue;
10390
10599
  }
10391
10600
  }
10392
10601
  filteredBatch.push(file);
10602
+ changedHashes.set(file.path, hash);
10393
10603
  }
10394
10604
  if (filteredBatch.length === 0) continue;
10395
10605
  try {
@@ -10406,6 +10616,11 @@ var init_client = __esm({
10406
10616
  abortedEarly = true;
10407
10617
  break;
10408
10618
  }
10619
+ if (data.status !== "partial") {
10620
+ for (const [filePath, hash] of changedHashes) newHashes.set(filePath, hash);
10621
+ }
10622
+ } else {
10623
+ for (const [filePath, hash] of changedHashes) newHashes.set(filePath, hash);
10409
10624
  }
10410
10625
  } catch (e) {
10411
10626
  console.error(`[ContextStream] Batch ingest error (attempt 1):`, e);
@@ -10420,14 +10635,22 @@ var init_client = __esm({
10420
10635
  abortedEarly = true;
10421
10636
  break;
10422
10637
  }
10638
+ if (data.status !== "partial") {
10639
+ for (const [filePath, hash] of changedHashes) newHashes.set(filePath, hash);
10640
+ }
10641
+ } else {
10642
+ for (const [filePath, hash] of changedHashes) newHashes.set(filePath, hash);
10423
10643
  }
10424
10644
  } catch (retryErr) {
10425
10645
  console.error(`[ContextStream] Batch ingest retry failed:`, retryErr);
10646
+ failedBatches++;
10647
+ failedFiles += filteredBatch.length;
10648
+ lastError = retryErr instanceof Error ? retryErr.message : String(retryErr);
10426
10649
  }
10427
10650
  }
10428
10651
  }
10429
10652
  writeHashManifest(projectId, newHashes);
10430
- const status = abortedEarly ? lastStatus === "daily_limit_exceeded" ? "daily_limit" : "cooldown" : "success";
10653
+ const status = abortedEarly ? lastStatus === "daily_limit_exceeded" ? "daily_limit" : "cooldown" : failedBatches > 0 && filesIndexed === 0 && totalFiles - filesSkippedByHash > 0 ? "error" : failedBatches > 0 || lastStatus === "partial" ? "partial" : "success";
10431
10654
  return {
10432
10655
  totalFiles,
10433
10656
  filesChanged: totalFiles - filesSkippedByHash,
@@ -10435,7 +10658,10 @@ var init_client = __esm({
10435
10658
  filesSkipped: filesSkippedByHash,
10436
10659
  apiSkipped,
10437
10660
  status,
10438
- abortedEarly
10661
+ abortedEarly,
10662
+ failedBatches,
10663
+ failedFiles,
10664
+ lastError
10439
10665
  };
10440
10666
  }
10441
10667
  /**
@@ -11019,6 +11245,9 @@ var init_client = __esm({
11019
11245
  rootPath: rootPathCopy,
11020
11246
  maxFiles: AUTO_INDEX_FILE_CAP
11021
11247
  });
11248
+ if (result.status === "error") {
11249
+ throw new Error(result.lastError || "all changed-file ingest batches failed");
11250
+ }
11022
11251
  console.error(
11023
11252
  `[ContextStream] Background indexing completed for ${rootPathCopy}: ${result.filesIndexed} indexed, ${result.filesSkipped} unchanged`
11024
11253
  );
@@ -11194,6 +11423,9 @@ var init_client = __esm({
11194
11423
  maxFiles: AUTO_INDEX_FILE_CAP,
11195
11424
  sinceTimestamp: sinceDate
11196
11425
  });
11426
+ if (result.status === "error") {
11427
+ throw new Error(result.lastError || "all changed-file ingest batches failed");
11428
+ }
11197
11429
  console.error(
11198
11430
  `[ContextStream] ${useIncremental ? "Incremental" : "Full"} index refresh completed for ${rootPathCopy}: ${result.filesIndexed} indexed, ${result.filesSkipped} unchanged`
11199
11431
  );
@@ -12039,6 +12271,9 @@ var init_client = __esm({
12039
12271
  rootPath: rootPathCopy,
12040
12272
  maxFiles: AUTO_INDEX_FILE_CAP
12041
12273
  });
12274
+ if (result.status === "error") {
12275
+ throw new Error(result.lastError || "all changed-file ingest batches failed");
12276
+ }
12042
12277
  console.error(
12043
12278
  `[ContextStream] Auto-indexing completed for ${rootPathCopy}: ${result.filesIndexed} indexed, ${result.filesSkipped} unchanged`
12044
12279
  );
@@ -13383,6 +13618,25 @@ ${context}`;
13383
13618
  uuidSchema.parse(params.task_id);
13384
13619
  return request(this.config, `/tasks/${params.task_id}`, { method: "GET" });
13385
13620
  }
13621
+ /** List completed Daily Recaps for a workspace, newest first. */
13622
+ async listDailyRecaps(params) {
13623
+ uuidSchema.parse(params.workspace_id);
13624
+ const query = new URLSearchParams({ workspace_id: params.workspace_id });
13625
+ query.set("limit", String(Math.min(100, Math.max(1, params.limit ?? 30))));
13626
+ return request(this.config, `/recap/history?${query.toString()}`, { method: "GET" });
13627
+ }
13628
+ /** Queue an asynchronous Daily Recap generation for a workspace. */
13629
+ async triggerDailyRecap(params) {
13630
+ uuidSchema.parse(params.workspace_id);
13631
+ return request(this.config, "/recap/generate", {
13632
+ method: "POST",
13633
+ body: { workspace_id: params.workspace_id }
13634
+ });
13635
+ }
13636
+ /** Get the latest public MCP release metadata. */
13637
+ async getMcpVersion() {
13638
+ return request(this.config, "/mcp/version", { method: "GET" });
13639
+ }
13386
13640
  /**
13387
13641
  * Update an existing task
13388
13642
  */
@@ -14227,7 +14481,7 @@ function generateAllRuleFiles(options) {
14227
14481
  }))
14228
14482
  ).filter((r) => r !== null);
14229
14483
  }
14230
- var DEFAULT_CLAUDE_MCP_SERVER_NAME, RULES_VERSION, CONTEXTSTREAM_TOOL_NAMES, CONTEXTSTREAM_RULES_BOOTSTRAP, CONTEXTSTREAM_RULES_DYNAMIC, CONTEXTSTREAM_RULES_FULL, CONTEXTSTREAM_RULES_MINIMAL, NO_HOOKS_SUPPLEMENT, ANTIGRAVITY_SUPPLEMENT, NO_HOOKS_EDITORS, TEMPLATES;
14484
+ var DEFAULT_CLAUDE_MCP_SERVER_NAME, RULES_VERSION, CONTEXTSTREAM_TOOL_NAMES, FAST_DIRECT_READ_LANE, CANONICAL_HANDOFF_RULES, CONTEXTSTREAM_RULES_BOOTSTRAP, CONTEXTSTREAM_RULES_DYNAMIC, CONTEXTSTREAM_RULES_FULL, CONTEXTSTREAM_RULES_MINIMAL, NO_HOOKS_SUPPLEMENT, ANTIGRAVITY_SUPPLEMENT, NO_HOOKS_EDITORS, TEMPLATES;
14231
14485
  var init_rules_templates = __esm({
14232
14486
  "src/rules-templates.ts"() {
14233
14487
  "use strict";
@@ -14274,6 +14528,28 @@ var init_rules_templates = __esm({
14274
14528
  "help"
14275
14529
  // Actions: tools, auth, version, editor_rules, enable_bundle
14276
14530
  ];
14531
+ FAST_DIRECT_READ_LANE = `
14532
+ ## Fast Direct-Read Lane
14533
+
14534
+ After the required first-session \`init(...)\` + \`context(...)\`, these exact read-only calls may run without another grounding preamble only when no state-changing tool has run since the last grounding:
14535
+ - \`workspace(action="list"|"get")\`
14536
+ - \`memory(action="list_docs"|"list_events"|"list_todos"|"list_tasks"|"list_transcripts"|"list_nodes"|"list_diagrams")\`
14537
+ - \`help(action="version"|"tools"|"auth")\`
14538
+ - \`project(action="list"|"get"|"index_status")\`
14539
+ - \`reminder(action="list"|"active")\`
14540
+
14541
+ Do not use this lane for recall, decisions, searches, reading a specific record, media queries, or any create/update/delete/index operation; ground those with \`context(...)\` first.
14542
+ `.trim();
14543
+ CANONICAL_HANDOFF_RULES = `
14544
+ ## Canonical Agent Handoffs
14545
+
14546
+ When the user asks to create/prepare a handoff, hand work over, or continue in another agent/session, create the durable record immediately with:
14547
+ \`entity(kind="handoff", action="create", body={title, summary, scope, next_steps})\`
14548
+
14549
+ - Add \`to_user_id\` only when the recipient is known; never invent it.
14550
+ - If a portable bundle, capsule, or share link is requested, also create a session capsule and return both artifacts.
14551
+ - \`HANDOFF.md\`, a scratch prompt, a generic doc/event, or prose alone is not the canonical handoff. If a local file is explicitly requested, create the entity first and treat the file as an additional artifact.
14552
+ `.trim();
14277
14553
  CONTEXTSTREAM_RULES_BOOTSTRAP = `
14278
14554
  <contextstream_rules>
14279
14555
  | Message | Required |
@@ -14297,6 +14573,10 @@ var init_rules_templates = __esm({
14297
14573
  - "find media" / "search photos/videos/audio/docs" / "what's in this PDF/video/audio?" \u2192 \`media(action="search", query="...", content_types=["document"])\`
14298
14574
  - "index media" / "upload asset" / "read this photo/video/audio/PDF" \u2192 \`media(action="index", file_path="...", content_type="image")\` or \`media(action="index", external_url="...", content_type="document")\`
14299
14575
 
14576
+ ${FAST_DIRECT_READ_LANE}
14577
+
14578
+ ${CANONICAL_HANDOFF_RULES}
14579
+
14300
14580
  ## Skills, Docs & Lessons First
14301
14581
 
14302
14582
  Before guessing, improvising, or struggling through a workflow you do not fully know, check whether ContextStream already has guidance for it.
@@ -14326,12 +14606,12 @@ Before guessing, improvising, or struggling through a workflow you do not fully
14326
14606
 
14327
14607
  ## Past Sessions Are Queryable \u2014 USE THEM
14328
14608
 
14329
- Transcripts of every prior session are captured + indexed. Before asking the user what you did last time, or re-deriving context you built together previously, check the transcript + snapshot layer.
14609
+ When \`context()\` returns fresh, relevant \`[GROUNDING]\`, read those ranked prior-work hits first. If they are sufficient, stop there\u2014do not immediately call \`session(action="recall")\` for the same request. Escalate to recall only when grounding is absent, thin, stale, off-topic, or the user explicitly requests broader history.
14330
14610
 
14331
14611
  Triggers: user says "last time"/"previous"/"yesterday"/"pick up where we left off", the task is clearly a continuation, or you're about to ask a clarifying question whose answer is likely in a prior session.
14332
14612
 
14333
14613
  Exact calls:
14334
- - Ranked past-session context: \`session(action="recall", query="<what you're continuing>")\`
14614
+ - First explicit escalation after insufficient grounding: \`session(action="recall", query="<what you're continuing>")\`
14335
14615
  - Chronological list of recent sessions: \`memory(action="list_transcripts", limit=10)\`
14336
14616
  - Full-text search across ALL past transcripts: \`memory(action="search_transcripts", query="<keyword>")\`
14337
14617
  - Save a snapshot of the current session so the NEXT session can pick up: \`session(action="capture", event_type="session_snapshot", title="...", content="...")\`
@@ -14357,6 +14637,10 @@ v${RULES_VERSION}
14357
14637
 
14358
14638
  Use \`context()\` by default to get task-specific rules, lessons from past mistakes, and relevant decisions.
14359
14639
 
14640
+ ${FAST_DIRECT_READ_LANE}
14641
+
14642
+ ${CANONICAL_HANDOFF_RULES}
14643
+
14360
14644
  ---
14361
14645
 
14362
14646
  ## Why Default Context-First
@@ -14890,6 +15174,10 @@ See full documentation: https://contextstream.io/docs/mcp/tools
14890
15174
  **HOOKS: \`<system-reminder>\` tags contain instructions \u2014 FOLLOW THEM**
14891
15175
  </contextstream_protocol>
14892
15176
 
15177
+ ${FAST_DIRECT_READ_LANE}
15178
+
15179
+ ${CANONICAL_HANDOFF_RULES}
15180
+
14893
15181
  Rules Version: ${RULES_VERSION}
14894
15182
 
14895
15183
  ---
@@ -15212,7 +15500,7 @@ curl -fsSL https://contextstream.io/scripts/setup.sh | bash
15212
15500
  irm https://contextstream.io/scripts/setup.ps1 | iex
15213
15501
  \`\`\`
15214
15502
 
15215
- **npm (requires Node.js 18+):**
15503
+ **npm (requires Node.js 20+):**
15216
15504
  \`\`\`bash
15217
15505
  npm install -g @contextstream/mcp-server@latest
15218
15506
  \`\`\`
@@ -15344,14 +15632,14 @@ Full docs: https://contextstream.io/docs/mcp/tools
15344
15632
  });
15345
15633
 
15346
15634
  // src/credentials.ts
15347
- import * as fs8 from "node:fs/promises";
15348
- import * as path9 from "node:path";
15349
- import { homedir as homedir7 } from "node:os";
15635
+ import * as fs7 from "node:fs/promises";
15636
+ import * as path8 from "node:path";
15637
+ import { homedir as homedir6 } from "node:os";
15350
15638
  function normalizeApiUrl(input) {
15351
15639
  return String(input ?? "").trim().replace(/\/+$/, "");
15352
15640
  }
15353
15641
  function credentialsFilePath() {
15354
- return path9.join(homedir7(), ".contextstream", "credentials.json");
15642
+ return path8.join(homedir6(), ".contextstream", "credentials.json");
15355
15643
  }
15356
15644
  function isRecord(value) {
15357
15645
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -15359,7 +15647,7 @@ function isRecord(value) {
15359
15647
  async function readSavedCredentials() {
15360
15648
  const filePath = credentialsFilePath();
15361
15649
  try {
15362
- const raw = await fs8.readFile(filePath, "utf8");
15650
+ const raw = await fs7.readFile(filePath, "utf8");
15363
15651
  const parsed = JSON.parse(raw);
15364
15652
  if (!isRecord(parsed)) return null;
15365
15653
  const version = parsed.version;
@@ -15385,7 +15673,7 @@ async function readSavedCredentials() {
15385
15673
  }
15386
15674
  async function writeSavedCredentials(input) {
15387
15675
  const filePath = credentialsFilePath();
15388
- await fs8.mkdir(path9.dirname(filePath), { recursive: true });
15676
+ await fs7.mkdir(path8.dirname(filePath), { recursive: true });
15389
15677
  const now = (/* @__PURE__ */ new Date()).toISOString();
15390
15678
  const existing = await readSavedCredentials();
15391
15679
  const value = {
@@ -15397,9 +15685,9 @@ async function writeSavedCredentials(input) {
15397
15685
  updated_at: now
15398
15686
  };
15399
15687
  const body = JSON.stringify(value, null, 2) + "\n";
15400
- await fs8.writeFile(filePath, body, { encoding: "utf8", mode: 384 });
15688
+ await fs7.writeFile(filePath, body, { encoding: "utf8", mode: 384 });
15401
15689
  try {
15402
- await fs8.chmod(filePath, 384);
15690
+ await fs7.chmod(filePath, 384);
15403
15691
  } catch {
15404
15692
  }
15405
15693
  return { path: filePath, value };
@@ -15416,7 +15704,7 @@ __export(doctor_exports, {
15416
15704
  runDoctor: () => runDoctor
15417
15705
  });
15418
15706
  import * as fsSync2 from "node:fs";
15419
- import * as path10 from "node:path";
15707
+ import * as path9 from "node:path";
15420
15708
  async function runDoctor() {
15421
15709
  const cwd = process.cwd();
15422
15710
  const lines = [`ContextStream MCP doctor v${VERSION}`, ""];
@@ -15465,7 +15753,7 @@ async function runDoctor() {
15465
15753
  }
15466
15754
  lines.push("", "Local index");
15467
15755
  const status = await readIndexStatus();
15468
- const entry = status.projects?.[path10.resolve(cwd)];
15756
+ const entry = status.projects?.[path9.resolve(cwd)];
15469
15757
  if (!entry) {
15470
15758
  warn(
15471
15759
  "No local index record for this folder.",
@@ -15507,7 +15795,7 @@ async function runDoctor() {
15507
15795
  lines.push("", "Editor rules");
15508
15796
  let ruleFilesFound = 0;
15509
15797
  for (const [editor, template] of Object.entries(TEMPLATES)) {
15510
- const filePath = path10.join(cwd, template.filename);
15798
+ const filePath = path9.join(cwd, template.filename);
15511
15799
  if (!fsSync2.existsSync(filePath)) continue;
15512
15800
  ruleFilesFound += 1;
15513
15801
  const content = fsSync2.readFileSync(filePath, "utf-8");
@@ -15529,8 +15817,8 @@ async function runDoctor() {
15529
15817
  }
15530
15818
  lines.push("", "Hooks");
15531
15819
  const hookTargets = [
15532
- { name: "Claude Code (project)", file: path10.join(cwd, ".claude", "settings.json") },
15533
- { name: "Cursor (project)", file: path10.join(cwd, ".cursor", "hooks.json") }
15820
+ { name: "Claude Code (project)", file: path9.join(cwd, ".claude", "settings.json") },
15821
+ { name: "Cursor (project)", file: path9.join(cwd, ".cursor", "hooks.json") }
15534
15822
  ];
15535
15823
  let hookConfigsFound = 0;
15536
15824
  for (const target of hookTargets) {
@@ -15573,12 +15861,13 @@ var init_doctor = __esm({
15573
15861
  var setup_exports = {};
15574
15862
  __export(setup_exports, {
15575
15863
  __setupTestUtils: () => __setupTestUtils,
15864
+ isBroadProjectPath: () => isBroadProjectPath,
15576
15865
  runIndexCommand: () => runIndexCommand,
15577
15866
  runSetupWizard: () => runSetupWizard
15578
15867
  });
15579
- import * as fs9 from "node:fs/promises";
15580
- import * as path11 from "node:path";
15581
- import { homedir as homedir8 } from "node:os";
15868
+ import * as fs8 from "node:fs/promises";
15869
+ import * as path10 from "node:path";
15870
+ import { homedir as homedir7 } from "node:os";
15582
15871
  import { stdin, stdout } from "node:process";
15583
15872
  import { createInterface } from "node:readline/promises";
15584
15873
  function supportsProjectMcpConfig(editor) {
@@ -15608,40 +15897,40 @@ function parseNumberList(input, max) {
15608
15897
  }
15609
15898
  async function fileExists(filePath) {
15610
15899
  try {
15611
- await fs9.stat(filePath);
15900
+ await fs8.stat(filePath);
15612
15901
  return true;
15613
15902
  } catch {
15614
15903
  return false;
15615
15904
  }
15616
15905
  }
15617
- function wrapWithMarkers2(content) {
15618
- return `${CONTEXTSTREAM_START_MARKER2}
15906
+ function wrapWithMarkers(content) {
15907
+ return `${CONTEXTSTREAM_START_MARKER}
15619
15908
  ${content.trim()}
15620
- ${CONTEXTSTREAM_END_MARKER2}`;
15909
+ ${CONTEXTSTREAM_END_MARKER}`;
15621
15910
  }
15622
- function isLegacyContextStreamRules2(content) {
15911
+ function isLegacyContextStreamRules(content) {
15623
15912
  const lower = content.toLowerCase();
15624
15913
  if (!lower.includes("contextstream")) return false;
15625
- if (!LEGACY_CONTEXTSTREAM_HINTS2.some((hint) => lower.includes(hint))) return false;
15914
+ if (!LEGACY_CONTEXTSTREAM_HINTS.some((hint) => lower.includes(hint))) return false;
15626
15915
  const headingRegex = /^#{1,6}\s+(.+)$/gm;
15627
15916
  let hasHeading = false;
15628
15917
  let match;
15629
15918
  while ((match = headingRegex.exec(content)) !== null) {
15630
15919
  hasHeading = true;
15631
15920
  const heading = match[1].trim().toLowerCase();
15632
- const allowed = LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS2.some(
15921
+ const allowed = LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS.some(
15633
15922
  (prefix) => heading.startsWith(prefix)
15634
15923
  );
15635
15924
  if (!allowed) return false;
15636
15925
  }
15637
15926
  return hasHeading;
15638
15927
  }
15639
- function isContextStreamPreamble2(line) {
15928
+ function isContextStreamPreamble(line) {
15640
15929
  const trimmed = line.trim();
15641
15930
  if (!trimmed) return false;
15642
- return CONTEXTSTREAM_PREAMBLE_PATTERNS2.some((pattern) => pattern.test(trimmed));
15931
+ return CONTEXTSTREAM_PREAMBLE_PATTERNS.some((pattern) => pattern.test(trimmed));
15643
15932
  }
15644
- function findContextStreamHeading2(lines) {
15933
+ function findContextStreamHeading(lines) {
15645
15934
  const headingRegex = /^(#{1,6})\s+(.+)$/;
15646
15935
  for (let i = 0; i < lines.length; i += 1) {
15647
15936
  const match = headingRegex.exec(lines[i]);
@@ -15652,7 +15941,7 @@ function findContextStreamHeading2(lines) {
15652
15941
  }
15653
15942
  return null;
15654
15943
  }
15655
- function findSectionEnd2(lines, startLine, level) {
15944
+ function findSectionEnd(lines, startLine, level) {
15656
15945
  const headingRegex = /^(#{1,6})\s+(.+)$/;
15657
15946
  for (let i = startLine + 1; i < lines.length; i += 1) {
15658
15947
  const match = headingRegex.exec(lines[i]);
@@ -15661,16 +15950,16 @@ function findSectionEnd2(lines, startLine, level) {
15661
15950
  }
15662
15951
  return lines.length;
15663
15952
  }
15664
- function extractContextStreamBlock2(content) {
15953
+ function extractContextStreamBlock(content) {
15665
15954
  const lines = content.split(/\r?\n/);
15666
- const heading = findContextStreamHeading2(lines);
15955
+ const heading = findContextStreamHeading(lines);
15667
15956
  if (!heading) return content.trim();
15668
- const endLine = findSectionEnd2(lines, heading.index, heading.level);
15957
+ const endLine = findSectionEnd(lines, heading.index, heading.level);
15669
15958
  return lines.slice(heading.index, endLine).join("\n").trim();
15670
15959
  }
15671
- function findLegacyContextStreamSection2(content) {
15960
+ function findLegacyContextStreamSection(content) {
15672
15961
  const lines = content.split(/\r?\n/);
15673
- const heading = findContextStreamHeading2(lines);
15962
+ const heading = findContextStreamHeading(lines);
15674
15963
  if (!heading) return null;
15675
15964
  let startLine = heading.index;
15676
15965
  for (let i = heading.index - 1; i >= 0; i -= 1) {
@@ -15679,39 +15968,39 @@ function findLegacyContextStreamSection2(content) {
15679
15968
  startLine = i;
15680
15969
  continue;
15681
15970
  }
15682
- if (isContextStreamPreamble2(line)) {
15971
+ if (isContextStreamPreamble(line)) {
15683
15972
  startLine = i;
15684
15973
  continue;
15685
15974
  }
15686
15975
  break;
15687
15976
  }
15688
- const endLine = findSectionEnd2(lines, heading.index, heading.level);
15977
+ const endLine = findSectionEnd(lines, heading.index, heading.level);
15689
15978
  return { startLine, endLine, contextLine: heading.index };
15690
15979
  }
15691
- function blockHasPreamble2(block) {
15980
+ function blockHasPreamble(block) {
15692
15981
  const lines = block.split(/\r?\n/);
15693
15982
  for (const line of lines) {
15694
15983
  const trimmed = line.trim();
15695
15984
  if (!trimmed) continue;
15696
- if (isContextStreamPreamble2(trimmed)) return true;
15985
+ if (isContextStreamPreamble(trimmed)) return true;
15697
15986
  if (/^#{1,6}\s+/.test(trimmed)) return false;
15698
15987
  }
15699
15988
  return false;
15700
15989
  }
15701
- function replaceContextStreamBlock2(existing, content) {
15702
- const fullWrapped = wrapWithMarkers2(content);
15703
- const blockWrapped = wrapWithMarkers2(extractContextStreamBlock2(content));
15704
- const startIdx = existing.indexOf(CONTEXTSTREAM_START_MARKER2);
15705
- const endIdx = existing.indexOf(CONTEXTSTREAM_END_MARKER2);
15990
+ function replaceContextStreamBlock(existing, content) {
15991
+ const fullWrapped = wrapWithMarkers(content);
15992
+ const blockWrapped = wrapWithMarkers(extractContextStreamBlock(content));
15993
+ const startIdx = existing.indexOf(CONTEXTSTREAM_START_MARKER);
15994
+ const endIdx = existing.indexOf(CONTEXTSTREAM_END_MARKER);
15706
15995
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
15707
- const existingBlock = existing.slice(startIdx + CONTEXTSTREAM_START_MARKER2.length, endIdx);
15708
- const replacement = blockHasPreamble2(existingBlock) ? fullWrapped : blockWrapped;
15996
+ const existingBlock = existing.slice(startIdx + CONTEXTSTREAM_START_MARKER.length, endIdx);
15997
+ const replacement = blockHasPreamble(existingBlock) ? fullWrapped : blockWrapped;
15709
15998
  const before = existing.substring(0, startIdx).trimEnd();
15710
- const after = existing.substring(endIdx + CONTEXTSTREAM_END_MARKER2.length).trimStart();
15999
+ const after = existing.substring(endIdx + CONTEXTSTREAM_END_MARKER.length).trimStart();
15711
16000
  const merged = [before, replacement, after].filter((part) => part.length > 0).join("\n\n");
15712
16001
  return { content: merged.trim() + "\n", status: "updated" };
15713
16002
  }
15714
- const legacy = findLegacyContextStreamSection2(existing);
16003
+ const legacy = findLegacyContextStreamSection(existing);
15715
16004
  if (legacy) {
15716
16005
  const lines = existing.split(/\r?\n/);
15717
16006
  const before = lines.slice(0, legacy.startLine).join("\n").trimEnd();
@@ -15720,54 +16009,54 @@ function replaceContextStreamBlock2(existing, content) {
15720
16009
  const merged = [before, replacement, after].filter((part) => part.length > 0).join("\n\n");
15721
16010
  return { content: merged.trim() + "\n", status: "updated" };
15722
16011
  }
15723
- if (isLegacyContextStreamRules2(existing)) {
16012
+ if (isLegacyContextStreamRules(existing)) {
15724
16013
  return { content: fullWrapped + "\n", status: "updated" };
15725
16014
  }
15726
16015
  const appended = existing.trimEnd() + "\n\n" + blockWrapped + "\n";
15727
16016
  return { content: appended, status: "appended" };
15728
16017
  }
15729
16018
  async function upsertTextFile(filePath, content, _marker) {
15730
- await fs9.mkdir(path11.dirname(filePath), { recursive: true });
16019
+ await fs8.mkdir(path10.dirname(filePath), { recursive: true });
15731
16020
  const exists = await fileExists(filePath);
15732
- const wrappedContent = wrapWithMarkers2(content);
16021
+ const wrappedContent = wrapWithMarkers(content);
15733
16022
  if (!exists) {
15734
- await fs9.writeFile(filePath, wrappedContent + "\n", "utf8");
16023
+ await fs8.writeFile(filePath, wrappedContent + "\n", "utf8");
15735
16024
  return "created";
15736
16025
  }
15737
- const existing = await fs9.readFile(filePath, "utf8").catch(() => "");
16026
+ const existing = await fs8.readFile(filePath, "utf8").catch(() => "");
15738
16027
  if (!existing.trim()) {
15739
- await fs9.writeFile(filePath, wrappedContent + "\n", "utf8");
16028
+ await fs8.writeFile(filePath, wrappedContent + "\n", "utf8");
15740
16029
  return "updated";
15741
16030
  }
15742
- const replaced = replaceContextStreamBlock2(existing, content);
15743
- await fs9.writeFile(filePath, replaced.content, "utf8");
16031
+ const replaced = replaceContextStreamBlock(existing, content);
16032
+ await fs8.writeFile(filePath, replaced.content, "utf8");
15744
16033
  return replaced.status;
15745
16034
  }
15746
16035
  function globalRulesPathForEditor(editor) {
15747
- const home = homedir8();
16036
+ const home = homedir7();
15748
16037
  switch (editor) {
15749
16038
  case "codex":
15750
- return path11.join(home, ".codex", "AGENTS.md");
16039
+ return path10.join(home, ".codex", "AGENTS.md");
15751
16040
  case "copilot":
15752
16041
  return null;
15753
16042
  case "opencode":
15754
- return path11.join(home, ".opencode", "AGENTS.md");
16043
+ return path10.join(home, ".opencode", "AGENTS.md");
15755
16044
  case "claude":
15756
- return path11.join(home, ".claude", "CLAUDE.md");
16045
+ return path10.join(home, ".claude", "CLAUDE.md");
15757
16046
  case "cline":
15758
- return path11.join(home, "Documents", "Cline", "Rules", "contextstream.md");
16047
+ return path10.join(home, "Documents", "Cline", "Rules", "contextstream.md");
15759
16048
  case "kilo":
15760
- return path11.join(kiloConfigDir(), "rules", "contextstream.md");
16049
+ return path10.join(kiloConfigDir(), "rules", "contextstream.md");
15761
16050
  case "roo":
15762
- return path11.join(home, ".roo", "rules", "contextstream.md");
16051
+ return path10.join(home, ".roo", "rules", "contextstream.md");
15763
16052
  case "aider":
15764
- return path11.join(home, ".aider.conf.yml");
16053
+ return path10.join(home, ".aider.conf.yml");
15765
16054
  case "antigravity":
15766
- return path11.join(home, ".gemini", "GEMINI.md");
16055
+ return path10.join(home, ".gemini", "GEMINI.md");
15767
16056
  case "cursor":
15768
16057
  return null;
15769
16058
  case "windsurf":
15770
- return path11.join(home, ".codeium", "windsurf", "memories", "global_rules.md");
16059
+ return path10.join(home, ".codeium", "windsurf", "memories", "global_rules.md");
15771
16060
  default:
15772
16061
  return null;
15773
16062
  }
@@ -15779,104 +16068,104 @@ async function anyPathExists(paths) {
15779
16068
  return false;
15780
16069
  }
15781
16070
  async function isCodexInstalled() {
15782
- const home = homedir8();
16071
+ const home = homedir7();
15783
16072
  const envHome = process.env.CODEX_HOME;
15784
16073
  const candidates = [
15785
16074
  envHome,
15786
- path11.join(home, ".codex"),
15787
- path11.join(home, ".codex", "config.toml"),
15788
- path11.join(home, ".config", "codex")
16075
+ path10.join(home, ".codex"),
16076
+ path10.join(home, ".codex", "config.toml"),
16077
+ path10.join(home, ".config", "codex")
15789
16078
  ].filter((candidate) => Boolean(candidate));
15790
16079
  return anyPathExists(candidates);
15791
16080
  }
15792
16081
  function openCodeConfigPath() {
15793
- const home = homedir8();
16082
+ const home = homedir7();
15794
16083
  const xdgConfigHome = process.env.XDG_CONFIG_HOME;
15795
16084
  if (process.platform === "win32") {
15796
- const appData = process.env.APPDATA || path11.join(home, "AppData", "Roaming");
15797
- return path11.join(appData, "opencode", "opencode.json");
16085
+ const appData = process.env.APPDATA || path10.join(home, "AppData", "Roaming");
16086
+ return path10.join(appData, "opencode", "opencode.json");
15798
16087
  }
15799
- const configRoot = xdgConfigHome || path11.join(home, ".config");
15800
- return path11.join(configRoot, "opencode", "opencode.json");
16088
+ const configRoot = xdgConfigHome || path10.join(home, ".config");
16089
+ return path10.join(configRoot, "opencode", "opencode.json");
15801
16090
  }
15802
16091
  async function isOpenCodeInstalled() {
15803
16092
  const configPath = openCodeConfigPath();
15804
- const configDir = path11.dirname(configPath);
15805
- const home = homedir8();
16093
+ const configDir = path10.dirname(configPath);
16094
+ const home = homedir7();
15806
16095
  const candidates = [
15807
16096
  configDir,
15808
16097
  configPath,
15809
- path11.join(home, ".bun", "bin", "opencode"),
15810
- path11.join(home, ".local", "bin", "opencode")
16098
+ path10.join(home, ".bun", "bin", "opencode"),
16099
+ path10.join(home, ".local", "bin", "opencode")
15811
16100
  ];
15812
16101
  return anyPathExists(candidates);
15813
16102
  }
15814
16103
  async function isClaudeInstalled() {
15815
- const home = homedir8();
15816
- const candidates = [path11.join(home, ".claude"), path11.join(home, ".config", "claude")];
16104
+ const home = homedir7();
16105
+ const candidates = [path10.join(home, ".claude"), path10.join(home, ".config", "claude")];
15817
16106
  const desktopConfig = claudeDesktopConfigPath();
15818
16107
  if (desktopConfig) candidates.push(desktopConfig);
15819
16108
  if (process.platform === "darwin") {
15820
- candidates.push(path11.join(home, "Library", "Application Support", "Claude"));
16109
+ candidates.push(path10.join(home, "Library", "Application Support", "Claude"));
15821
16110
  } else if (process.platform === "win32") {
15822
16111
  const appData = process.env.APPDATA;
15823
- if (appData) candidates.push(path11.join(appData, "Claude"));
16112
+ if (appData) candidates.push(path10.join(appData, "Claude"));
15824
16113
  }
15825
16114
  return anyPathExists(candidates);
15826
16115
  }
15827
16116
  async function isClineInstalled() {
15828
- const home = homedir8();
16117
+ const home = homedir7();
15829
16118
  const candidates = [
15830
- path11.join(home, "Documents", "Cline"),
15831
- path11.join(home, ".cline"),
15832
- path11.join(home, ".config", "cline")
16119
+ path10.join(home, "Documents", "Cline"),
16120
+ path10.join(home, ".cline"),
16121
+ path10.join(home, ".config", "cline")
15833
16122
  ];
15834
16123
  return anyPathExists(candidates);
15835
16124
  }
15836
16125
  async function isCopilotInstalled() {
15837
- const home = homedir8();
16126
+ const home = homedir7();
15838
16127
  const candidates = [
15839
- path11.join(home, ".copilot"),
15840
- path11.join(home, ".config", "github-copilot"),
15841
- path11.join(home, ".vscode", "extensions", "github.copilot-chat"),
15842
- path11.join(home, ".vscode", "extensions", "github.copilot")
16128
+ path10.join(home, ".copilot"),
16129
+ path10.join(home, ".config", "github-copilot"),
16130
+ path10.join(home, ".vscode", "extensions", "github.copilot-chat"),
16131
+ path10.join(home, ".vscode", "extensions", "github.copilot")
15843
16132
  ];
15844
16133
  return anyPathExists(candidates);
15845
16134
  }
15846
16135
  async function isKiloInstalled() {
15847
- const home = homedir8();
16136
+ const home = homedir7();
15848
16137
  const candidates = [
15849
16138
  kiloConfigDir(),
15850
- path11.join(home, ".kilocode"),
15851
- path11.join(home, ".config", "kilocode")
16139
+ path10.join(home, ".kilocode"),
16140
+ path10.join(home, ".config", "kilocode")
15852
16141
  ];
15853
16142
  return anyPathExists(candidates);
15854
16143
  }
15855
16144
  async function isRooInstalled() {
15856
- const home = homedir8();
15857
- const candidates = [path11.join(home, ".roo"), path11.join(home, ".config", "roo")];
16145
+ const home = homedir7();
16146
+ const candidates = [path10.join(home, ".roo"), path10.join(home, ".config", "roo")];
15858
16147
  return anyPathExists(candidates);
15859
16148
  }
15860
16149
  async function isAiderInstalled() {
15861
- const home = homedir8();
15862
- const candidates = [path11.join(home, ".aider.conf.yml"), path11.join(home, ".config", "aider")];
16150
+ const home = homedir7();
16151
+ const candidates = [path10.join(home, ".aider.conf.yml"), path10.join(home, ".config", "aider")];
15863
16152
  return anyPathExists(candidates);
15864
16153
  }
15865
16154
  async function isCursorInstalled() {
15866
- const home = homedir8();
15867
- const candidates = [path11.join(home, ".cursor")];
16155
+ const home = homedir7();
16156
+ const candidates = [path10.join(home, ".cursor")];
15868
16157
  if (process.platform === "darwin") {
15869
16158
  candidates.push("/Applications/Cursor.app");
15870
- candidates.push(path11.join(home, "Applications", "Cursor.app"));
15871
- candidates.push(path11.join(home, "Library", "Application Support", "Cursor"));
16159
+ candidates.push(path10.join(home, "Applications", "Cursor.app"));
16160
+ candidates.push(path10.join(home, "Library", "Application Support", "Cursor"));
15872
16161
  } else if (process.platform === "win32") {
15873
16162
  const localApp = process.env.LOCALAPPDATA;
15874
16163
  const programFiles = process.env.ProgramFiles;
15875
16164
  const programFilesX86 = process.env["ProgramFiles(x86)"];
15876
- if (localApp) candidates.push(path11.join(localApp, "Programs", "Cursor", "Cursor.exe"));
15877
- if (localApp) candidates.push(path11.join(localApp, "Cursor", "Cursor.exe"));
15878
- if (programFiles) candidates.push(path11.join(programFiles, "Cursor", "Cursor.exe"));
15879
- if (programFilesX86) candidates.push(path11.join(programFilesX86, "Cursor", "Cursor.exe"));
16165
+ if (localApp) candidates.push(path10.join(localApp, "Programs", "Cursor", "Cursor.exe"));
16166
+ if (localApp) candidates.push(path10.join(localApp, "Cursor", "Cursor.exe"));
16167
+ if (programFiles) candidates.push(path10.join(programFiles, "Cursor", "Cursor.exe"));
16168
+ if (programFilesX86) candidates.push(path10.join(programFilesX86, "Cursor", "Cursor.exe"));
15880
16169
  } else {
15881
16170
  candidates.push("/usr/bin/cursor");
15882
16171
  candidates.push("/usr/local/bin/cursor");
@@ -15886,20 +16175,20 @@ async function isCursorInstalled() {
15886
16175
  return anyPathExists(candidates);
15887
16176
  }
15888
16177
  async function isWindsurfInstalled() {
15889
- const home = homedir8();
15890
- const candidates = [path11.join(home, ".codeium", "windsurf")];
16178
+ const home = homedir7();
16179
+ const candidates = [path10.join(home, ".codeium", "windsurf")];
15891
16180
  if (process.platform === "darwin") {
15892
16181
  candidates.push("/Applications/Windsurf.app");
15893
- candidates.push(path11.join(home, "Applications", "Windsurf.app"));
15894
- candidates.push(path11.join(home, "Library", "Application Support", "Windsurf"));
16182
+ candidates.push(path10.join(home, "Applications", "Windsurf.app"));
16183
+ candidates.push(path10.join(home, "Library", "Application Support", "Windsurf"));
15895
16184
  } else if (process.platform === "win32") {
15896
16185
  const localApp = process.env.LOCALAPPDATA;
15897
16186
  const programFiles = process.env.ProgramFiles;
15898
16187
  const programFilesX86 = process.env["ProgramFiles(x86)"];
15899
- if (localApp) candidates.push(path11.join(localApp, "Programs", "Windsurf", "Windsurf.exe"));
15900
- if (localApp) candidates.push(path11.join(localApp, "Windsurf", "Windsurf.exe"));
15901
- if (programFiles) candidates.push(path11.join(programFiles, "Windsurf", "Windsurf.exe"));
15902
- if (programFilesX86) candidates.push(path11.join(programFilesX86, "Windsurf", "Windsurf.exe"));
16188
+ if (localApp) candidates.push(path10.join(localApp, "Programs", "Windsurf", "Windsurf.exe"));
16189
+ if (localApp) candidates.push(path10.join(localApp, "Windsurf", "Windsurf.exe"));
16190
+ if (programFiles) candidates.push(path10.join(programFiles, "Windsurf", "Windsurf.exe"));
16191
+ if (programFilesX86) candidates.push(path10.join(programFilesX86, "Windsurf", "Windsurf.exe"));
15903
16192
  } else {
15904
16193
  candidates.push("/usr/bin/windsurf");
15905
16194
  candidates.push("/usr/local/bin/windsurf");
@@ -15909,22 +16198,22 @@ async function isWindsurfInstalled() {
15909
16198
  return anyPathExists(candidates);
15910
16199
  }
15911
16200
  async function isAntigravityInstalled() {
15912
- const home = homedir8();
15913
- const candidates = [path11.join(home, ".gemini")];
16201
+ const home = homedir7();
16202
+ const candidates = [path10.join(home, ".gemini")];
15914
16203
  if (process.platform === "darwin") {
15915
16204
  candidates.push("/Applications/Antigravity.app");
15916
- candidates.push(path11.join(home, "Applications", "Antigravity.app"));
15917
- candidates.push(path11.join(home, "Library", "Application Support", "Antigravity"));
16205
+ candidates.push(path10.join(home, "Applications", "Antigravity.app"));
16206
+ candidates.push(path10.join(home, "Library", "Application Support", "Antigravity"));
15918
16207
  } else if (process.platform === "win32") {
15919
16208
  const localApp = process.env.LOCALAPPDATA;
15920
16209
  const programFiles = process.env.ProgramFiles;
15921
16210
  const programFilesX86 = process.env["ProgramFiles(x86)"];
15922
16211
  if (localApp)
15923
- candidates.push(path11.join(localApp, "Programs", "Antigravity", "Antigravity.exe"));
15924
- if (localApp) candidates.push(path11.join(localApp, "Antigravity", "Antigravity.exe"));
15925
- if (programFiles) candidates.push(path11.join(programFiles, "Antigravity", "Antigravity.exe"));
16212
+ candidates.push(path10.join(localApp, "Programs", "Antigravity", "Antigravity.exe"));
16213
+ if (localApp) candidates.push(path10.join(localApp, "Antigravity", "Antigravity.exe"));
16214
+ if (programFiles) candidates.push(path10.join(programFiles, "Antigravity", "Antigravity.exe"));
15926
16215
  if (programFilesX86)
15927
- candidates.push(path11.join(programFilesX86, "Antigravity", "Antigravity.exe"));
16216
+ candidates.push(path10.join(programFilesX86, "Antigravity", "Antigravity.exe"));
15928
16217
  } else {
15929
16218
  candidates.push("/usr/bin/antigravity");
15930
16219
  candidates.push("/usr/local/bin/antigravity");
@@ -16103,12 +16392,12 @@ function buildContextStreamOpenCodeRemoteServer() {
16103
16392
  };
16104
16393
  }
16105
16394
  function kiloConfigDir() {
16106
- const home = homedir8();
16395
+ const home = homedir7();
16107
16396
  if (process.platform === "win32") {
16108
- const appData = process.env.APPDATA || path11.join(home, "AppData", "Roaming");
16109
- return path11.join(appData, "kilo");
16397
+ const appData = process.env.APPDATA || path10.join(home, "AppData", "Roaming");
16398
+ return path10.join(appData, "kilo");
16110
16399
  }
16111
- return path11.join(home, ".config", "kilo");
16400
+ return path10.join(home, ".config", "kilo");
16112
16401
  }
16113
16402
  function buildContextStreamKiloServer(params) {
16114
16403
  const env = buildSetupEnv(params);
@@ -16121,11 +16410,11 @@ function buildContextStreamKiloServer(params) {
16121
16410
  };
16122
16411
  }
16123
16412
  async function upsertKiloMcpConfig(filePath, server) {
16124
- await fs9.mkdir(path11.dirname(filePath), { recursive: true });
16413
+ await fs8.mkdir(path10.dirname(filePath), { recursive: true });
16125
16414
  const exists = await fileExists(filePath);
16126
16415
  let root = {};
16127
16416
  if (exists) {
16128
- const raw = await fs9.readFile(filePath, "utf8").catch(() => "");
16417
+ const raw = await fs8.readFile(filePath, "utf8").catch(() => "");
16129
16418
  const parsed = tryParseJsonLike(raw);
16130
16419
  if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
16131
16420
  root = parsed.value;
@@ -16138,7 +16427,7 @@ async function upsertKiloMcpConfig(filePath, server) {
16138
16427
  const before = JSON.stringify(root.mcp.contextstream ?? null);
16139
16428
  root.mcp.contextstream = server;
16140
16429
  const after = JSON.stringify(root.mcp.contextstream ?? null);
16141
- await fs9.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
16430
+ await fs8.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
16142
16431
  if (!exists) return "created";
16143
16432
  return before === after ? "skipped" : "updated";
16144
16433
  }
@@ -16161,11 +16450,11 @@ function tryParseJsonLike(raw) {
16161
16450
  }
16162
16451
  }
16163
16452
  async function upsertJsonMcpConfig(filePath, server) {
16164
- await fs9.mkdir(path11.dirname(filePath), { recursive: true });
16453
+ await fs8.mkdir(path10.dirname(filePath), { recursive: true });
16165
16454
  const exists = await fileExists(filePath);
16166
16455
  let root = {};
16167
16456
  if (exists) {
16168
- const raw = await fs9.readFile(filePath, "utf8").catch(() => "");
16457
+ const raw = await fs8.readFile(filePath, "utf8").catch(() => "");
16169
16458
  const parsed = tryParseJsonLike(raw);
16170
16459
  if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
16171
16460
  root = parsed.value;
@@ -16176,16 +16465,16 @@ async function upsertJsonMcpConfig(filePath, server) {
16176
16465
  const before = JSON.stringify(root.mcpServers.contextstream ?? null);
16177
16466
  root.mcpServers.contextstream = server;
16178
16467
  const after = JSON.stringify(root.mcpServers.contextstream ?? null);
16179
- await fs9.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
16468
+ await fs8.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
16180
16469
  if (!exists) return "created";
16181
16470
  return before === after ? "skipped" : "updated";
16182
16471
  }
16183
16472
  async function upsertJsonVsCodeMcpConfig(filePath, server) {
16184
- await fs9.mkdir(path11.dirname(filePath), { recursive: true });
16473
+ await fs8.mkdir(path10.dirname(filePath), { recursive: true });
16185
16474
  const exists = await fileExists(filePath);
16186
16475
  let root = {};
16187
16476
  if (exists) {
16188
- const raw = await fs9.readFile(filePath, "utf8").catch(() => "");
16477
+ const raw = await fs8.readFile(filePath, "utf8").catch(() => "");
16189
16478
  const parsed = tryParseJsonLike(raw);
16190
16479
  if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
16191
16480
  root = parsed.value;
@@ -16207,16 +16496,16 @@ async function upsertJsonVsCodeMcpConfig(filePath, server) {
16207
16496
  root.servers.contextstream = server;
16208
16497
  }
16209
16498
  const after = JSON.stringify(root.servers.contextstream ?? null);
16210
- await fs9.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
16499
+ await fs8.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
16211
16500
  if (!exists) return "created";
16212
16501
  return before === after ? "skipped" : "updated";
16213
16502
  }
16214
16503
  async function upsertOpenCodeMcpConfig(filePath, server) {
16215
- await fs9.mkdir(path11.dirname(filePath), { recursive: true });
16504
+ await fs8.mkdir(path10.dirname(filePath), { recursive: true });
16216
16505
  const exists = await fileExists(filePath);
16217
16506
  let root = {};
16218
16507
  if (exists) {
16219
- const raw = await fs9.readFile(filePath, "utf8").catch(() => "");
16508
+ const raw = await fs8.readFile(filePath, "utf8").catch(() => "");
16220
16509
  const parsed = tryParseJsonLike(raw);
16221
16510
  if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
16222
16511
  root = parsed.value;
@@ -16233,14 +16522,14 @@ async function upsertOpenCodeMcpConfig(filePath, server) {
16233
16522
  schema: root.$schema ?? null,
16234
16523
  contextstream: root.mcp.contextstream ?? null
16235
16524
  });
16236
- await fs9.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
16525
+ await fs8.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
16237
16526
  if (!exists) return "created";
16238
16527
  return before === after ? "skipped" : "updated";
16239
16528
  }
16240
16529
  function claudeDesktopConfigPath() {
16241
- const home = homedir8();
16530
+ const home = homedir7();
16242
16531
  if (process.platform === "darwin") {
16243
- return path11.join(
16532
+ return path10.join(
16244
16533
  home,
16245
16534
  "Library",
16246
16535
  "Application Support",
@@ -16249,18 +16538,18 @@ function claudeDesktopConfigPath() {
16249
16538
  );
16250
16539
  }
16251
16540
  if (process.platform === "win32") {
16252
- const appData = process.env.APPDATA || path11.join(home, "AppData", "Roaming");
16253
- return path11.join(appData, "Claude", "claude_desktop_config.json");
16541
+ const appData = process.env.APPDATA || path10.join(home, "AppData", "Roaming");
16542
+ return path10.join(appData, "Claude", "claude_desktop_config.json");
16254
16543
  }
16255
16544
  if (process.platform === "linux") {
16256
- return path11.join(home, ".config", "Claude", "claude_desktop_config.json");
16545
+ return path10.join(home, ".config", "Claude", "claude_desktop_config.json");
16257
16546
  }
16258
16547
  return null;
16259
16548
  }
16260
16549
  async function upsertCodexTomlConfig(filePath, params) {
16261
- await fs9.mkdir(path11.dirname(filePath), { recursive: true });
16550
+ await fs8.mkdir(path10.dirname(filePath), { recursive: true });
16262
16551
  const exists = await fileExists(filePath);
16263
- const existing = exists ? await fs9.readFile(filePath, "utf8").catch(() => "") : "";
16552
+ const existing = exists ? await fs8.readFile(filePath, "utf8").catch(() => "") : "";
16264
16553
  const env = buildSetupEnv(params);
16265
16554
  const envLines = formatTomlEnvLines(env);
16266
16555
  const marker = "[mcp_servers.contextstream]";
@@ -16278,15 +16567,15 @@ args = ["--prefer-online", "-y", "@contextstream/mcp-server@latest"]
16278
16567
  [mcp_servers.contextstream.env]
16279
16568
  ` + envLines + "\n";
16280
16569
  if (!exists) {
16281
- await fs9.writeFile(filePath, block.trimStart(), "utf8");
16570
+ await fs8.writeFile(filePath, block.trimStart(), "utf8");
16282
16571
  return "created";
16283
16572
  }
16284
16573
  if (!existing.includes(marker)) {
16285
- await fs9.writeFile(filePath, existing.trimEnd() + block, "utf8");
16574
+ await fs8.writeFile(filePath, existing.trimEnd() + block, "utf8");
16286
16575
  return "updated";
16287
16576
  }
16288
16577
  if (!existing.includes(envMarker)) {
16289
- await fs9.writeFile(
16578
+ await fs8.writeFile(
16290
16579
  filePath,
16291
16580
  existing.trimEnd() + "\n\n" + envMarker + "\n" + envLines + "\n",
16292
16581
  "utf8"
@@ -16336,18 +16625,18 @@ args = ["--prefer-online", "-y", "@contextstream/mcp-server@latest"]
16336
16625
  }
16337
16626
  const updated = out.join("\n");
16338
16627
  if (updated === existing) return "skipped";
16339
- await fs9.writeFile(filePath, updated, "utf8");
16628
+ await fs8.writeFile(filePath, updated, "utf8");
16340
16629
  return "updated";
16341
16630
  }
16342
16631
  async function discoverProjectsUnderFolder(parentFolder) {
16343
- const entries = await fs9.readdir(parentFolder, { withFileTypes: true });
16344
- const candidates = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => path11.join(parentFolder, e.name));
16632
+ const entries = await fs8.readdir(parentFolder, { withFileTypes: true });
16633
+ const candidates = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => path10.join(parentFolder, e.name));
16345
16634
  const projects = [];
16346
16635
  for (const dir of candidates) {
16347
- const hasGit = await fileExists(path11.join(dir, ".git"));
16348
- const hasPkg = await fileExists(path11.join(dir, "package.json"));
16349
- const hasCargo = await fileExists(path11.join(dir, "Cargo.toml"));
16350
- const hasPyProject = await fileExists(path11.join(dir, "pyproject.toml"));
16636
+ const hasGit = await fileExists(path10.join(dir, ".git"));
16637
+ const hasPkg = await fileExists(path10.join(dir, "package.json"));
16638
+ const hasCargo = await fileExists(path10.join(dir, "Cargo.toml"));
16639
+ const hasPyProject = await fileExists(path10.join(dir, "pyproject.toml"));
16351
16640
  if (hasGit || hasPkg || hasCargo || hasPyProject) projects.push(dir);
16352
16641
  }
16353
16642
  return projects;
@@ -16381,7 +16670,7 @@ async function selectProjectForCurrentDirectory(client, cwd, workspaceId, dryRun
16381
16670
  if (!workspaceId || workspaceId === "dry-run") {
16382
16671
  return void 0;
16383
16672
  }
16384
- const folderName = path11.basename(cwd) || "project";
16673
+ const folderName = path10.basename(cwd) || "project";
16385
16674
  let projects = [];
16386
16675
  try {
16387
16676
  const result = await client.listProjects({
@@ -16457,15 +16746,34 @@ async function selectProjectForCurrentDirectory(client, cwd, workspaceId, dryRun
16457
16746
  return void 0;
16458
16747
  }
16459
16748
  async function runIndexCommand(targetPath) {
16460
- const resolvedPath = path11.resolve(targetPath);
16749
+ const resolvedPath = path10.resolve(targetPath);
16750
+ if (isInvariantIgnoredPath(resolvedPath)) {
16751
+ throw new Error(invariantProjectPathMessage(resolvedPath));
16752
+ }
16753
+ if (isBroadProjectPath(resolvedPath) && !broadIngestAllowed()) {
16754
+ throw new Error(broadProjectPathMessage(resolvedPath));
16755
+ }
16461
16756
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
16462
16757
  const config = loadConfig2();
16463
16758
  const client = new ContextStreamClient(config);
16464
16759
  const localConfig = readLocalConfig(resolvedPath);
16465
16760
  await indexProjectWithProgress(client, resolvedPath, localConfig?.workspace_id);
16466
16761
  }
16762
+ function isBroadProjectPath(candidatePath, homePath = homedir7()) {
16763
+ const resolved = path10.resolve(candidatePath);
16764
+ return resolved === path10.parse(resolved).root || resolved === path10.resolve(homePath);
16765
+ }
16766
+ function broadIngestAllowed() {
16767
+ return process.env.CONTEXTSTREAM_ALLOW_BROAD_INGEST === "1";
16768
+ }
16769
+ function broadProjectPathMessage(candidatePath) {
16770
+ return `Refusing to use ${candidatePath} as a project root because it is the filesystem root or home directory. Run from a repository folder or pass a narrower project path. Set CONTEXTSTREAM_ALLOW_BROAD_INGEST=1 only for an intentional broad ingest.`;
16771
+ }
16772
+ function invariantProjectPathMessage(candidatePath) {
16773
+ return `Refusing to use ${candidatePath} as a project root because it is inside agent state or a credential store. Choose the actual repository checkout instead.`;
16774
+ }
16467
16775
  async function indexProjectWithProgress(client, projectPath, workspaceId) {
16468
- const projectName = path11.basename(projectPath);
16776
+ const projectName = path10.basename(projectPath);
16469
16777
  console.log(`
16470
16778
  ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
16471
16779
  console.log(`${colors.dim}${projectPath}${colors.reset}
@@ -16571,6 +16879,7 @@ ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
16571
16879
  return 0;
16572
16880
  };
16573
16881
  let failedBatches = 0;
16882
+ let failedFiles = 0;
16574
16883
  try {
16575
16884
  for await (const batch of readAllFilesInBatches(projectPath, {
16576
16885
  batchSize: 20,
@@ -16584,7 +16893,7 @@ ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
16584
16893
  batchCount++;
16585
16894
  } catch {
16586
16895
  failedBatches++;
16587
- filesIndexed += batch.length;
16896
+ failedFiles += batch.length;
16588
16897
  }
16589
16898
  if (filesIndexed > totalFiles) {
16590
16899
  totalFiles = filesIndexed + 100;
@@ -16596,7 +16905,7 @@ ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
16596
16905
  process.stdout.write("\r" + " ".repeat(120) + "\r");
16597
16906
  if (failedBatches > 0) {
16598
16907
  console.log(
16599
- `${colors.yellow}\u2713${colors.reset} Index update complete: ${colors.bright}${filesIndexed.toLocaleString()}${colors.reset} files indexed (${finalSize}) in ${elapsed}s (${failedBatches} batches had errors)`
16908
+ `${colors.yellow}!${colors.reset} Index update incomplete: ${colors.bright}${filesIndexed.toLocaleString()}${colors.reset} files indexed, ${failedFiles.toLocaleString()} failed (${finalSize}) in ${elapsed}s (${failedBatches} batches had errors; retry the command)`
16600
16909
  );
16601
16910
  } else {
16602
16911
  console.log(
@@ -16761,10 +17070,10 @@ Code: ${device.user_code}`);
16761
17070
  if (poll && poll.status === "pending") {
16762
17071
  const intervalSeconds = typeof poll.interval === "number" ? poll.interval : 5;
16763
17072
  const waitMs = Math.max(1, intervalSeconds) * 1e3;
16764
- await new Promise((resolve16) => setTimeout(resolve16, waitMs));
17073
+ await new Promise((resolve17) => setTimeout(resolve17, waitMs));
16765
17074
  continue;
16766
17075
  }
16767
- await new Promise((resolve16) => setTimeout(resolve16, 1e3));
17076
+ await new Promise((resolve17) => setTimeout(resolve17, 1e3));
16768
17077
  }
16769
17078
  if (!accessToken) {
16770
17079
  throw new Error(
@@ -16811,7 +17120,7 @@ Code: ${device.user_code}`);
16811
17120
  }
16812
17121
  }
16813
17122
  async function startBackgroundIndexing(projectPath) {
16814
- const projectLabel = path11.basename(projectPath);
17123
+ const projectLabel = path10.basename(projectPath);
16815
17124
  try {
16816
17125
  const { spawn: spawn2 } = await import("node:child_process");
16817
17126
  const entry = process.argv[1];
@@ -17079,7 +17388,7 @@ Editors (from --editors): ${selectedEditors.join(", ")}`);
17079
17388
  if (mcpScope === "project" && editor !== "codex") continue;
17080
17389
  try {
17081
17390
  if (editor === "codex") {
17082
- const filePath = path11.join(homedir8(), ".codex", "config.toml");
17391
+ const filePath = path10.join(homedir7(), ".codex", "config.toml");
17083
17392
  if (dryRun) {
17084
17393
  writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
17085
17394
  console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
@@ -17095,7 +17404,7 @@ Editors (from --editors): ${selectedEditors.join(", ")}`);
17095
17404
  continue;
17096
17405
  }
17097
17406
  if (editor === "copilot") {
17098
- const filePath = path11.join(homedir8(), ".copilot", "mcp-config.json");
17407
+ const filePath = path10.join(homedir7(), ".copilot", "mcp-config.json");
17099
17408
  if (dryRun) {
17100
17409
  writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
17101
17410
  console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
@@ -17150,7 +17459,7 @@ Editors (from --editors): ${selectedEditors.join(", ")}`);
17150
17459
  continue;
17151
17460
  }
17152
17461
  if (editor === "cursor") {
17153
- const filePath = path11.join(homedir8(), ".cursor", "mcp.json");
17462
+ const filePath = path10.join(homedir7(), ".cursor", "mcp.json");
17154
17463
  if (dryRun) {
17155
17464
  writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
17156
17465
  console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
@@ -17162,7 +17471,7 @@ Editors (from --editors): ${selectedEditors.join(", ")}`);
17162
17471
  continue;
17163
17472
  }
17164
17473
  if (editor === "windsurf") {
17165
- const filePath = path11.join(homedir8(), ".codeium", "windsurf", "mcp_config.json");
17474
+ const filePath = path10.join(homedir7(), ".codeium", "windsurf", "mcp_config.json");
17166
17475
  if (dryRun) {
17167
17476
  writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
17168
17477
  console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
@@ -17180,7 +17489,7 @@ Editors (from --editors): ${selectedEditors.join(", ")}`);
17180
17489
  continue;
17181
17490
  }
17182
17491
  if (editor === "kilo") {
17183
- const filePath = path11.join(kiloConfigDir(), "kilo.jsonc");
17492
+ const filePath = path10.join(kiloConfigDir(), "kilo.jsonc");
17184
17493
  if (dryRun) {
17185
17494
  writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
17186
17495
  console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
@@ -17246,7 +17555,7 @@ Editors (from --editors): ${selectedEditors.join(", ")}`);
17246
17555
  });
17247
17556
  for (const script of result.installed) {
17248
17557
  writeActions.push({ kind: "hooks", target: script, status: "created" });
17249
- console.log(`- ${EDITOR_LABELS[editor]}: installed ${path11.basename(script)}`);
17558
+ console.log(`- ${EDITOR_LABELS[editor]}: installed ${path10.basename(script)}`);
17250
17559
  }
17251
17560
  if (editor === "cursor") {
17252
17561
  console.log(
@@ -17318,11 +17627,18 @@ Editors (from --editors): ${selectedEditors.join(", ")}`);
17318
17627
  const needsProjects = scope === "project" || scope === "both" || (mcpScope === "project" || mcpScope === "both") && hasProjectMcpEditors || enforceCopilotCanonicalPair;
17319
17628
  if (needsProjects) {
17320
17629
  console.log("\nProject setup...");
17321
- const addCwd = normalizeInput(
17322
- await question(`Add current folder as a project? [Y/n] (${process.cwd()}): `)
17323
- );
17324
- if (addCwd.toLowerCase() !== "n" && addCwd.toLowerCase() !== "no") {
17325
- projectPaths.add(path11.resolve(process.cwd()));
17630
+ const currentFolder = path10.resolve(process.cwd());
17631
+ if (isInvariantIgnoredPath(currentFolder)) {
17632
+ console.log(`- ${invariantProjectPathMessage(currentFolder)}`);
17633
+ } else if (isBroadProjectPath(currentFolder) && !broadIngestAllowed()) {
17634
+ console.log(`- ${broadProjectPathMessage(currentFolder)}`);
17635
+ } else {
17636
+ const addCwd = normalizeInput(
17637
+ await question(`Add current folder as a project? [Y/n] (${currentFolder}): `)
17638
+ );
17639
+ if (addCwd.toLowerCase() !== "n" && addCwd.toLowerCase() !== "no") {
17640
+ projectPaths.add(currentFolder);
17641
+ }
17326
17642
  }
17327
17643
  while (true) {
17328
17644
  console.log("\n 1) Add another project path");
@@ -17332,13 +17648,22 @@ Editors (from --editors): ${selectedEditors.join(", ")}`);
17332
17648
  if (choice === "3") break;
17333
17649
  if (choice === "1") {
17334
17650
  const p = normalizeInput(await question("Project folder path: "));
17335
- if (p) projectPaths.add(path11.resolve(p));
17651
+ if (p) {
17652
+ const resolved = path10.resolve(p);
17653
+ if (isInvariantIgnoredPath(resolved)) {
17654
+ console.log(`- ${invariantProjectPathMessage(resolved)}`);
17655
+ } else if (isBroadProjectPath(resolved) && !broadIngestAllowed()) {
17656
+ console.log(`- ${broadProjectPathMessage(resolved)}`);
17657
+ } else {
17658
+ projectPaths.add(resolved);
17659
+ }
17660
+ }
17336
17661
  continue;
17337
17662
  }
17338
17663
  if (choice === "2") {
17339
17664
  const parent = normalizeInput(await question("Parent folder path: "));
17340
17665
  if (!parent) continue;
17341
- const parentAbs = path11.resolve(parent);
17666
+ const parentAbs = path10.resolve(parent);
17342
17667
  const projects2 = await discoverProjectsUnderFolder(parentAbs);
17343
17668
  if (projects2.length === 0) {
17344
17669
  console.log(
@@ -17371,7 +17696,7 @@ Applying to ${projects.length} project(s)...`);
17371
17696
  workspace_id: workspaceId,
17372
17697
  workspace_name: workspaceName,
17373
17698
  create_parent_mapping: createParentMapping,
17374
- ...path11.resolve(projectPath) === path11.resolve(process.cwd()) && selectedCurrentProject ? {
17699
+ ...path10.resolve(projectPath) === path10.resolve(process.cwd()) && selectedCurrentProject ? {
17375
17700
  project_id: selectedCurrentProject.id,
17376
17701
  project_name: selectedCurrentProject.name
17377
17702
  } : {},
@@ -17383,7 +17708,7 @@ Applying to ${projects.length} project(s)...`);
17383
17708
  });
17384
17709
  writeActions.push({
17385
17710
  kind: "workspace-config",
17386
- target: path11.join(projectPath, ".contextstream", "config.json"),
17711
+ target: path10.join(projectPath, ".contextstream", "config.json"),
17387
17712
  status: "created"
17388
17713
  });
17389
17714
  console.log(`- Linked workspace in ${projectPath}`);
@@ -17394,11 +17719,11 @@ Applying to ${projects.length} project(s)...`);
17394
17719
  } else if (workspaceId && workspaceId !== "dry-run" && workspaceName && dryRun) {
17395
17720
  writeActions.push({
17396
17721
  kind: "workspace-config",
17397
- target: path11.join(projectPath, ".contextstream", "config.json"),
17722
+ target: path10.join(projectPath, ".contextstream", "config.json"),
17398
17723
  status: "dry-run"
17399
17724
  });
17400
17725
  }
17401
- const isCurrentProject = path11.resolve(projectPath) === path11.resolve(process.cwd());
17726
+ const isCurrentProject = path10.resolve(projectPath) === path10.resolve(process.cwd());
17402
17727
  const projectIdForPath = isCurrentProject ? selectedCurrentProject?.id : void 0;
17403
17728
  const projectSetupEnv = {
17404
17729
  apiUrl,
@@ -17417,8 +17742,8 @@ Applying to ${projects.length} project(s)...`);
17417
17742
  if (!shouldWriteProjectMcp) continue;
17418
17743
  try {
17419
17744
  if (editor === "cursor") {
17420
- const cursorPath = path11.join(projectPath, ".cursor", "mcp.json");
17421
- const vscodePath = path11.join(projectPath, ".vscode", "mcp.json");
17745
+ const cursorPath = path10.join(projectPath, ".cursor", "mcp.json");
17746
+ const vscodePath = path10.join(projectPath, ".vscode", "mcp.json");
17422
17747
  if (dryRun) {
17423
17748
  writeActions.push({ kind: "mcp-config", target: cursorPath, status: "dry-run" });
17424
17749
  writeActions.push({ kind: "mcp-config", target: vscodePath, status: "dry-run" });
@@ -17432,12 +17757,12 @@ Applying to ${projects.length} project(s)...`);
17432
17757
  }
17433
17758
  if (editor === "windsurf") {
17434
17759
  console.log(
17435
- `- ${EDITOR_LABELS[editor]}: uses global MCP config only (${path11.join(homedir8(), ".codeium", "windsurf", "mcp_config.json")}).`
17760
+ `- ${EDITOR_LABELS[editor]}: uses global MCP config only (${path10.join(homedir7(), ".codeium", "windsurf", "mcp_config.json")}).`
17436
17761
  );
17437
17762
  continue;
17438
17763
  }
17439
17764
  if (editor === "claude") {
17440
- const mcpPath = path11.join(projectPath, ".mcp.json");
17765
+ const mcpPath = path10.join(projectPath, ".mcp.json");
17441
17766
  if (dryRun) {
17442
17767
  writeActions.push({ kind: "mcp-config", target: mcpPath, status: "dry-run" });
17443
17768
  } else {
@@ -17447,7 +17772,7 @@ Applying to ${projects.length} project(s)...`);
17447
17772
  continue;
17448
17773
  }
17449
17774
  if (editor === "opencode") {
17450
- const openCodePath = path11.join(projectPath, "opencode.json");
17775
+ const openCodePath = path10.join(projectPath, "opencode.json");
17451
17776
  if (dryRun) {
17452
17777
  writeActions.push({
17453
17778
  kind: "mcp-config",
@@ -17463,7 +17788,7 @@ Applying to ${projects.length} project(s)...`);
17463
17788
  continue;
17464
17789
  }
17465
17790
  if (editor === "copilot") {
17466
- const vsCodePath = path11.join(projectPath, ".vscode", "mcp.json");
17791
+ const vsCodePath = path10.join(projectPath, ".vscode", "mcp.json");
17467
17792
  if (dryRun) {
17468
17793
  writeActions.push({ kind: "mcp-config", target: vsCodePath, status: "dry-run" });
17469
17794
  } else {
@@ -17473,7 +17798,7 @@ Applying to ${projects.length} project(s)...`);
17473
17798
  continue;
17474
17799
  }
17475
17800
  if (editor === "kilo") {
17476
- const kiloPath = path11.join(projectPath, "kilo.jsonc");
17801
+ const kiloPath = path10.join(projectPath, "kilo.jsonc");
17477
17802
  if (dryRun) {
17478
17803
  writeActions.push({ kind: "mcp-config", target: kiloPath, status: "dry-run" });
17479
17804
  } else {
@@ -17484,7 +17809,7 @@ Applying to ${projects.length} project(s)...`);
17484
17809
  continue;
17485
17810
  }
17486
17811
  if (editor === "roo") {
17487
- const rooPath = path11.join(projectPath, ".roo", "mcp.json");
17812
+ const rooPath = path10.join(projectPath, ".roo", "mcp.json");
17488
17813
  if (dryRun) {
17489
17814
  writeActions.push({ kind: "mcp-config", target: rooPath, status: "dry-run" });
17490
17815
  } else {
@@ -17507,11 +17832,11 @@ Applying to ${projects.length} project(s)...`);
17507
17832
  const rule = generateRuleContent(editor, {
17508
17833
  workspaceName,
17509
17834
  workspaceId: workspaceId && workspaceId !== "dry-run" ? workspaceId : void 0,
17510
- projectName: path11.basename(projectPath),
17835
+ projectName: path10.basename(projectPath),
17511
17836
  mode: getModeForEditor(editor)
17512
17837
  });
17513
17838
  if (!rule) continue;
17514
- const filePath = path11.join(projectPath, rule.filename);
17839
+ const filePath = path10.join(projectPath, rule.filename);
17515
17840
  if (dryRun) {
17516
17841
  writeActions.push({ kind: "rules", target: filePath, status: "dry-run" });
17517
17842
  continue;
@@ -17666,7 +17991,7 @@ Applying to ${projects.length} project(s)...`);
17666
17991
  rl.close();
17667
17992
  }
17668
17993
  }
17669
- var EDITOR_LABELS, CONTEXTSTREAM_START_MARKER2, CONTEXTSTREAM_END_MARKER2, LEGACY_CONTEXTSTREAM_HINTS2, LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS2, CONTEXTSTREAM_PREAMBLE_PATTERNS2, IS_WINDOWS, DEFAULT_CONTEXTSTREAM_API_URL, DEFAULT_CONTEXTSTREAM_MCP_URL, DEFAULT_CONTEXTSTREAM_TOOLSET, DEFAULT_CONTEXTSTREAM_OUTPUT_FORMAT, DEFAULT_CONTEXTSTREAM_SEARCH_LIMIT, DEFAULT_CONTEXTSTREAM_SEARCH_MAX_CHARS, DEFAULT_CONTEXTSTREAM_TRANSCRIPTS_ENABLED, DEFAULT_CONTEXTSTREAM_HOOK_TRANSCRIPTS_ENABLED, VSCODE_MCP_MODE_ENV, OPENCODE_CONFIG_SCHEMA_URL, REMOTE_HEADER_TOOLSET, REMOTE_HEADER_OUTPUT_FORMAT, REMOTE_HEADER_SEARCH_LIMIT, REMOTE_HEADER_SEARCH_MAX_CHARS, REMOTE_HEADER_TRANSCRIPTS_ENABLED, REMOTE_HEADER_CONSOLIDATED, __setupTestUtils, colors, spinnerFrames;
17994
+ var EDITOR_LABELS, CONTEXTSTREAM_START_MARKER, CONTEXTSTREAM_END_MARKER, LEGACY_CONTEXTSTREAM_HINTS, LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS, CONTEXTSTREAM_PREAMBLE_PATTERNS, IS_WINDOWS, DEFAULT_CONTEXTSTREAM_API_URL, DEFAULT_CONTEXTSTREAM_MCP_URL, DEFAULT_CONTEXTSTREAM_TOOLSET, DEFAULT_CONTEXTSTREAM_OUTPUT_FORMAT, DEFAULT_CONTEXTSTREAM_SEARCH_LIMIT, DEFAULT_CONTEXTSTREAM_SEARCH_MAX_CHARS, DEFAULT_CONTEXTSTREAM_TRANSCRIPTS_ENABLED, DEFAULT_CONTEXTSTREAM_HOOK_TRANSCRIPTS_ENABLED, VSCODE_MCP_MODE_ENV, OPENCODE_CONFIG_SCHEMA_URL, REMOTE_HEADER_TOOLSET, REMOTE_HEADER_OUTPUT_FORMAT, REMOTE_HEADER_SEARCH_LIMIT, REMOTE_HEADER_SEARCH_MAX_CHARS, REMOTE_HEADER_TRANSCRIPTS_ENABLED, REMOTE_HEADER_CONSOLIDATED, __setupTestUtils, colors, spinnerFrames;
17670
17995
  var init_setup = __esm({
17671
17996
  "src/setup.ts"() {
17672
17997
  "use strict";
@@ -17677,6 +18002,7 @@ var init_setup = __esm({
17677
18002
  init_credentials();
17678
18003
  init_hooks_config();
17679
18004
  init_files();
18005
+ init_ignore();
17680
18006
  init_workspace_config();
17681
18007
  EDITOR_LABELS = {
17682
18008
  codex: "Codex CLI",
@@ -17691,9 +18017,9 @@ var init_setup = __esm({
17691
18017
  aider: "Aider",
17692
18018
  antigravity: "Antigravity (Google)"
17693
18019
  };
17694
- CONTEXTSTREAM_START_MARKER2 = "<!-- BEGIN ContextStream -->";
17695
- CONTEXTSTREAM_END_MARKER2 = "<!-- END ContextStream -->";
17696
- LEGACY_CONTEXTSTREAM_HINTS2 = [
18020
+ CONTEXTSTREAM_START_MARKER = "<!-- BEGIN ContextStream -->";
18021
+ CONTEXTSTREAM_END_MARKER = "<!-- END ContextStream -->";
18022
+ LEGACY_CONTEXTSTREAM_HINTS = [
17697
18023
  "contextstream integration",
17698
18024
  "contextstream v0.4",
17699
18025
  "contextstream v0.3",
@@ -17702,7 +18028,7 @@ var init_setup = __esm({
17702
18028
  "contextstream mcp",
17703
18029
  "contextstream tools"
17704
18030
  ];
17705
- LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS2 = [
18031
+ LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS = [
17706
18032
  "contextstream",
17707
18033
  "tl;dr",
17708
18034
  "required every message",
@@ -17723,7 +18049,7 @@ var init_setup = __esm({
17723
18049
  "plans & tasks",
17724
18050
  "complete action reference"
17725
18051
  ];
17726
- CONTEXTSTREAM_PREAMBLE_PATTERNS2 = [
18052
+ CONTEXTSTREAM_PREAMBLE_PATTERNS = [
17727
18053
  /^#\s+workspace:/i,
17728
18054
  /^#\s+project:/i,
17729
18055
  /^#\s+workspace id:/i,
@@ -17785,11 +18111,13 @@ var init_setup = __esm({
17785
18111
  // src/hooks/post-write.ts
17786
18112
  var post_write_exports = {};
17787
18113
  __export(post_write_exports, {
17788
- runPostWriteHook: () => runPostWriteHook
18114
+ runPostWriteHook: () => runPostWriteHook,
18115
+ shouldIgnoreHookPath: () => shouldIgnoreHookPath
17789
18116
  });
17790
18117
  import * as fs10 from "node:fs";
17791
18118
  import * as path12 from "node:path";
17792
18119
  import { homedir as homedir9 } from "node:os";
18120
+ import { execFileSync } from "node:child_process";
17793
18121
  function extractFilePath(input) {
17794
18122
  if (input.tool_input) {
17795
18123
  const filePath = input.tool_input.file_path || input.tool_input.notebook_path || input.tool_input.path;
@@ -17895,6 +18223,29 @@ function shouldIndexFile(filePath) {
17895
18223
  }
17896
18224
  return true;
17897
18225
  }
18226
+ function shouldIgnoreHookPath(projectRoot, filePath) {
18227
+ const root = path12.resolve(projectRoot);
18228
+ const absolutePath = path12.resolve(filePath);
18229
+ if (isInvariantIgnoredPath(absolutePath)) {
18230
+ return true;
18231
+ }
18232
+ const relativePath = path12.relative(root, absolutePath).replace(/\\/g, "/");
18233
+ if (!relativePath || relativePath === ".." || relativePath.startsWith("../")) {
18234
+ return true;
18235
+ }
18236
+ if (loadIgnorePatternsSync(root).ignores(relativePath)) {
18237
+ return true;
18238
+ }
18239
+ try {
18240
+ execFileSync("git", ["-C", root, "check-ignore", "-q", "--", relativePath], {
18241
+ stdio: "ignore",
18242
+ timeout: 2e3
18243
+ });
18244
+ return true;
18245
+ } catch {
18246
+ return false;
18247
+ }
18248
+ }
17898
18249
  function detectLanguage2(filePath) {
17899
18250
  const ext = path12.extname(filePath).toLowerCase();
17900
18251
  const langMap = {
@@ -18040,6 +18391,9 @@ async function runPostWriteHook() {
18040
18391
  if (!projectRoot) {
18041
18392
  process.exit(0);
18042
18393
  }
18394
+ if (shouldIgnoreHookPath(projectRoot, absolutePath)) {
18395
+ process.exit(0);
18396
+ }
18043
18397
  const localConfig = findLocalConfig(projectRoot);
18044
18398
  if (!localConfig?.project_id) {
18045
18399
  process.exit(0);
@@ -18058,6 +18412,7 @@ var API_URL, API_KEY, ENABLED, INDEXABLE_EXTENSIONS, MAX_FILE_SIZE2, isDirectRun
18058
18412
  var init_post_write = __esm({
18059
18413
  "src/hooks/post-write.ts"() {
18060
18414
  "use strict";
18415
+ init_ignore();
18061
18416
  API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
18062
18417
  API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
18063
18418
  ENABLED = process.env.CONTEXTSTREAM_POSTWRITE_ENABLED !== "false";
@@ -20020,9 +20375,10 @@ var init_user_prompt_submit = __esm({
20020
20375
  API_KEY2 = process.env.CONTEXTSTREAM_API_KEY || "";
20021
20376
  WORKSPACE_ID = null;
20022
20377
  PROJECT_ID = null;
20023
- REMINDER = `[CONTEXTSTREAM] On the first message in every session call mcp__contextstream__init(...), then call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>") FIRST before any other tool. On subsequent messages, default to context first. Narrow bypass is allowed only for immediate read-only ContextStream calls when prior context is fresh and no state-changing tool has run. Response contains dynamic rules, lessons, preferences.
20378
+ REMINDER = `[CONTEXTSTREAM] On the first message in every session call mcp__contextstream__init(...), then call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>") FIRST before any other tool. On subsequent messages, default to context first. The fresh-context direct-read lane is limited to workspace list/get; memory list_*; help version/tools/auth; project list/get/index_status; and reminder list/active. Recall, decisions, searches, specific reads, writes, and indexing require context first. Fresh [GROUNDING] is the first continuation retrieval; do not immediately duplicate it with recall. Response contains dynamic rules, lessons, preferences.
20024
20379
  SEARCH-FIRST: Use mcp__contextstream__search(mode="auto") before Glob/Grep/Read/Explore/Task/EnterPlanMode. In planning, never use EnterPlanMode or Task(Explore) for file-by-file discovery.
20025
20380
  COMMON MEMORY CALLS: list docs via memory(action="list_docs"), list lessons via session(action="get_lessons"), list plans via session(action="list_plans"), list tasks/todos via memory(action="list_tasks"|"list_todos").
20381
+ HANDOFFS: A generic handoff must create entity(kind="handoff", action="create", body={title,summary,scope,next_steps}); add capsule only for a requested portable bundle/share link. HANDOFF.md or prose alone is not the canonical handoff.
20026
20382
  [END]`;
20027
20383
  FULL_REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]
20028
20384
 
@@ -20031,7 +20387,8 @@ COMMON MEMORY CALLS: list docs via memory(action="list_docs"), list lessons via
20031
20387
  - Check response for: [LESSONS_WARNING], [RULES_NOTICE], preferences
20032
20388
  - save_exchange=true saves each conversation turn for later retrieval
20033
20389
  - Use a consistent session_id for the entire conversation (generate once on first message)
20034
- - On subsequent messages, default to context() first. Narrow bypass: immediate read-only ContextStream calls when context is fresh and no state-changing tool has run.
20390
+ - On subsequent messages, default to context() first. Direct-read bypass is limited to workspace list/get; memory list_*; help version/tools/auth; project list/get/index_status; and reminder list/active when context is fresh and no state-changing tool has run.
20391
+ - Fresh [GROUNDING] is the first continuation retrieval. Do not immediately duplicate sufficient grounding with session(action="recall").
20035
20392
 
20036
20393
  2. FOR CODE SEARCH: Check index status, then search appropriately
20037
20394
  \u2705 Use mcp__contextstream__search(mode="auto", query="...") BEFORE Glob/Grep/Read/Explore/Task/EnterPlanMode
@@ -20049,13 +20406,18 @@ COMMON MEMORY CALLS: list docs via memory(action="list_docs"), list lessons via
20049
20406
  \u274C DO NOT use EnterPlanMode or Task(subagent_type="Explore") for file-by-file discovery
20050
20407
  \u2705 For planning discovery: mcp__contextstream__search(mode="auto", query="...", output_format="paths")
20051
20408
 
20052
- 5. CHECK THESE from context() response:
20409
+ 5. FOR HANDOFFS: Create the canonical durable handoff entity
20410
+ \u2705 mcp__contextstream__entity(kind="handoff", action="create", body={title,summary,scope,next_steps})
20411
+ \u2705 Add a capsule only when a portable bundle or share link is requested
20412
+ \u274C HANDOFF.md, a scratch prompt, or prose alone is not the canonical handoff
20413
+
20414
+ 6. CHECK THESE from context() response:
20053
20415
  - Lessons: Past mistakes to avoid (shown as warnings)
20054
20416
  - Reminders: Active reminders for this project
20055
20417
  - Preferences: User's coding style and preferences
20056
20418
  - Rules: Dynamic rules matched to current task
20057
20419
 
20058
- 6. SKIP CONTEXTSTREAM: If user preference says "skip contextstream", use local tools instead
20420
+ 7. SKIP CONTEXTSTREAM: If user preference says "skip contextstream", use local tools instead
20059
20421
  [END]`;
20060
20422
  ENHANCED_REMINDER_HEADER = `\u2B21 ContextStream \u2014 Smart Context & Memory
20061
20423
 
@@ -21629,10 +21991,10 @@ init_client();
21629
21991
  init_files();
21630
21992
  init_rules_templates();
21631
21993
  init_version();
21632
- import * as fs7 from "node:fs";
21633
- import * as path8 from "node:path";
21994
+ import * as fs9 from "node:fs";
21995
+ import * as path11 from "node:path";
21634
21996
  import { execFile as execFile2 } from "node:child_process";
21635
- import { homedir as homedir6 } from "node:os";
21997
+ import { homedir as homedir8 } from "node:os";
21636
21998
  import { promisify as promisify3 } from "node:util";
21637
21999
 
21638
22000
  // src/tool-catalog.ts
@@ -22455,6 +22817,71 @@ var HotPathStore = class {
22455
22817
  var globalHotPathStore = new HotPathStore();
22456
22818
 
22457
22819
  // src/tools.ts
22820
+ init_setup();
22821
+ init_ignore();
22822
+ var MCP_RELEASES_URL = "https://github.com/contextstream/mcp-server/releases";
22823
+ var ENTITY_TOOL_DESCRIPTION = "Structured taxonomy entities \u2014 tickets, handoffs, incidents, releases, experiments, goals, key_results, sprints, reviews, risks, and backlog views. Use kind=handoff with action=create when the user asks to create or prepare a handoff, hand work over, or continue in another agent/session; include title, summary, scope, and next_steps. Add to_user_id only when the recipient is known; omit to_user_id rather than inventing a recipient. A handoff entity is the canonical durable handoff. A capsule is an optional additional portable artifact when the user explicitly asks for a capsule, bundle, or share link. Never substitute HANDOFF.md, a scratch prompt, a generic document/event, or prose alone. Use memory(create_task) for lightweight tasks, memory(create_doc) for runbooks/docs, and session(capture) for timeline events.";
22824
+ var CAPSULE_TOOL_DESCRIPTION = "ContextCapsule creates portable, shareable, hydrate-on-demand context artifacts. Use it when the user explicitly asks for a capsule, portable bundle, share link, team link, external-agent link, bootstrap prompt, share-token graph, or capsule audit. For a generic handoff, first create entity(kind=handoff); if a portable bundle or link is also requested, create both the handoff entity and capsule. A capsule is the artifact, not a replacement for the canonical handoff. Do not use capsule for normal turn-by-turn retrieval; use context instead.";
22825
+ function taskUpdateIsStatusOnly(update) {
22826
+ return typeof update.status === "string" && update.status.length > 0 && update.title === void 0 && update.content === void 0 && update.description === void 0 && update.priority === void 0 && update.order === void 0 && update.plan_id === void 0 && update.plan_step_id === void 0 && update.code_refs === void 0 && update.tags === void 0 && update.blocked_reason === void 0;
22827
+ }
22828
+ function extractTaskStatus(result) {
22829
+ if (!result || typeof result !== "object") return void 0;
22830
+ const value = result;
22831
+ const candidates = [
22832
+ value.status,
22833
+ value.task?.status,
22834
+ value.data?.status,
22835
+ value.data?.task?.status
22836
+ ];
22837
+ return candidates.find((candidate) => typeof candidate === "string");
22838
+ }
22839
+ function buildMcpVersionInfo(currentVersion, remote) {
22840
+ const wrapped = remote && typeof remote === "object" ? remote : {};
22841
+ const payload = wrapped.data && typeof wrapped.data === "object" ? wrapped.data : wrapped;
22842
+ return {
22843
+ ...payload,
22844
+ name: "contextstream-mcp",
22845
+ runtime_type: "legacy-typescript-mcp",
22846
+ version: currentVersion,
22847
+ latest_version: typeof payload.latest_version === "string" ? payload.latest_version : currentVersion,
22848
+ release_url: typeof payload.release_url === "string" ? payload.release_url : MCP_RELEASES_URL
22849
+ };
22850
+ }
22851
+ function dailyRecapItems(result) {
22852
+ if (Array.isArray(result)) return result;
22853
+ if (!result || typeof result !== "object") return [];
22854
+ const value = result;
22855
+ const candidates = [
22856
+ value.recaps,
22857
+ value.items,
22858
+ value.data,
22859
+ value.data?.recaps,
22860
+ value.data?.items
22861
+ ];
22862
+ return candidates.find(Array.isArray) || [];
22863
+ }
22864
+ function formatDailyRecaps(result) {
22865
+ const recaps = dailyRecapItems(result);
22866
+ if (recaps.length === 0) {
22867
+ return `No completed Daily Recaps found for this workspace. Automatic recaps run around 23:00 in the user's configured timezone; use session(action="trigger_recap") to queue one now.`;
22868
+ }
22869
+ const lines = recaps.map((recap) => {
22870
+ const date = typeof recap.recap_date === "string" ? recap.recap_date : "unknown date";
22871
+ const generatedAt = typeof recap.generated_at === "string" ? recap.generated_at : "generation timestamp unavailable";
22872
+ const headline = typeof recap.headline === "string" ? recap.headline.trim() : "";
22873
+ return `- ${date} \u2014 generated ${generatedAt}${headline ? ` \u2014 ${headline}` : ""}`;
22874
+ });
22875
+ return [`Daily Recaps (${recaps.length}), newest first:`, ...lines].join("\n");
22876
+ }
22877
+ async function loadMcpVersionInfo(client) {
22878
+ try {
22879
+ return buildMcpVersionInfo(VERSION, await client.getMcpVersion());
22880
+ } catch (error) {
22881
+ logDebug(`MCP release metadata unavailable: ${error instanceof Error ? error.message : String(error)}`);
22882
+ return buildMcpVersionInfo(VERSION, void 0);
22883
+ }
22884
+ }
22458
22885
  function stringOrArray(itemSchema) {
22459
22886
  return external_exports.union([
22460
22887
  external_exports.array(itemSchema),
@@ -22954,22 +23381,22 @@ async function resolveIdOrPrefix(client, value, fieldName, workspaceId, projectI
22954
23381
  }
22955
23382
  var RULES_NOTICE_CACHE_TTL_MS = 10 * 60 * 1e3;
22956
23383
  var RULES_VERSION_REGEX = /(?:Rules Version:\s*|^v)([0-9][0-9A-Za-z.\-]*)/im;
22957
- var CONTEXTSTREAM_START_MARKER = "<!-- BEGIN ContextStream -->";
22958
- var CONTEXTSTREAM_END_MARKER = "<!-- END ContextStream -->";
23384
+ var CONTEXTSTREAM_START_MARKER2 = "<!-- BEGIN ContextStream -->";
23385
+ var CONTEXTSTREAM_END_MARKER2 = "<!-- END ContextStream -->";
22959
23386
  var RULES_PROJECT_FILES = {
22960
23387
  codex: "AGENTS.md",
22961
23388
  claude: "CLAUDE.md",
22962
- copilot: path8.join(".github", "copilot-instructions.md"),
23389
+ copilot: path11.join(".github", "copilot-instructions.md"),
22963
23390
  cursor: ".cursorrules",
22964
23391
  cline: ".clinerules",
22965
- kilo: path8.join(".kilocode", "rules", "contextstream.md"),
22966
- roo: path8.join(".roo", "rules", "contextstream.md"),
23392
+ kilo: path11.join(".kilocode", "rules", "contextstream.md"),
23393
+ roo: path11.join(".roo", "rules", "contextstream.md"),
22967
23394
  aider: ".aider.conf.yml"
22968
23395
  };
22969
23396
  var RULES_GLOBAL_FILES = {
22970
- codex: [path8.join(homedir6(), ".codex", "AGENTS.md")],
22971
- kilo: [path8.join(homedir6(), ".kilocode", "rules", "contextstream.md")],
22972
- roo: [path8.join(homedir6(), ".roo", "rules", "contextstream.md")]
23397
+ codex: [path11.join(homedir8(), ".codex", "AGENTS.md")],
23398
+ kilo: [path11.join(homedir8(), ".kilocode", "rules", "contextstream.md")],
23399
+ roo: [path11.join(homedir8(), ".roo", "rules", "contextstream.md")]
22973
23400
  };
22974
23401
  var rulesNoticeCache = /* @__PURE__ */ new Map();
22975
23402
  function compareVersions2(v1, v2) {
@@ -22988,12 +23415,12 @@ function extractRulesVersions(content) {
22988
23415
  return Array.from(content.matchAll(regex)).map((match) => match[1]?.trim()).filter((version) => Boolean(version));
22989
23416
  }
22990
23417
  function extractContextStreamMarkerBlock(content) {
22991
- const startIdx = content.indexOf(CONTEXTSTREAM_START_MARKER);
22992
- const endIdx = content.indexOf(CONTEXTSTREAM_END_MARKER);
23418
+ const startIdx = content.indexOf(CONTEXTSTREAM_START_MARKER2);
23419
+ const endIdx = content.indexOf(CONTEXTSTREAM_END_MARKER2);
22993
23420
  if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
22994
23421
  return null;
22995
23422
  }
22996
- return content.slice(startIdx + CONTEXTSTREAM_START_MARKER.length, endIdx).trim();
23423
+ return content.slice(startIdx + CONTEXTSTREAM_START_MARKER2.length, endIdx).trim();
22997
23424
  }
22998
23425
  function extractRulesVersion(content) {
22999
23426
  const markerBlock = extractContextStreamMarkerBlock(content);
@@ -23021,7 +23448,7 @@ function resolveRulesCandidatePaths(folderPath, editorKey) {
23021
23448
  if (!folderPath) return;
23022
23449
  const rel = RULES_PROJECT_FILES[key];
23023
23450
  if (rel) {
23024
- candidates.add(path8.join(folderPath, rel));
23451
+ candidates.add(path11.join(folderPath, rel));
23025
23452
  }
23026
23453
  };
23027
23454
  const addGlobal = (key) => {
@@ -23053,7 +23480,7 @@ function resolveFolderPath(inputPath, sessionManager) {
23053
23480
  const indicators = [".git", "package.json", "Cargo.toml", "pyproject.toml", ".contextstream"];
23054
23481
  const hasIndicator = indicators.some((entry) => {
23055
23482
  try {
23056
- return fs7.existsSync(path8.join(cwd, entry));
23483
+ return fs9.existsSync(path11.join(cwd, entry));
23057
23484
  } catch {
23058
23485
  return false;
23059
23486
  }
@@ -23072,7 +23499,7 @@ function getRulesNotice(folderPath, clientName) {
23072
23499
  return cached.notice;
23073
23500
  }
23074
23501
  const candidates = resolveRulesCandidatePaths(folderPath, editorKey);
23075
- const existing = candidates.filter((filePath) => fs7.existsSync(filePath));
23502
+ const existing = candidates.filter((filePath) => fs9.existsSync(filePath));
23076
23503
  if (existing.length === 0) {
23077
23504
  const updateCommand2 = "generate_rules()";
23078
23505
  const notice2 = {
@@ -23094,7 +23521,7 @@ function getRulesNotice(folderPath, clientName) {
23094
23521
  const versions = [];
23095
23522
  for (const filePath of existing) {
23096
23523
  try {
23097
- const content = fs7.readFileSync(filePath, "utf-8");
23524
+ const content = fs9.readFileSync(filePath, "utf-8");
23098
23525
  const version = extractRulesVersion(content);
23099
23526
  if (!version) {
23100
23527
  filesMissingVersion.push(filePath);
@@ -23131,7 +23558,7 @@ function getRulesNotice(folderPath, clientName) {
23131
23558
  rulesNoticeCache.set(cacheKey, { checkedAt: Date.now(), notice });
23132
23559
  return notice;
23133
23560
  }
23134
- var LEGACY_CONTEXTSTREAM_HINTS = [
23561
+ var LEGACY_CONTEXTSTREAM_HINTS2 = [
23135
23562
  "contextstream integration",
23136
23563
  "contextstream v0.4",
23137
23564
  "contextstream v0.3",
@@ -23140,7 +23567,7 @@ var LEGACY_CONTEXTSTREAM_HINTS = [
23140
23567
  "contextstream mcp",
23141
23568
  "contextstream tools"
23142
23569
  ];
23143
- var LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS = [
23570
+ var LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS2 = [
23144
23571
  "contextstream",
23145
23572
  "tl;dr",
23146
23573
  "required every message",
@@ -23161,7 +23588,7 @@ var LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS = [
23161
23588
  "plans & tasks",
23162
23589
  "complete action reference"
23163
23590
  ];
23164
- var CONTEXTSTREAM_PREAMBLE_PATTERNS = [
23591
+ var CONTEXTSTREAM_PREAMBLE_PATTERNS2 = [
23165
23592
  /^#\s+workspace:/i,
23166
23593
  /^#\s+project:/i,
23167
23594
  /^#\s+workspace id:/i,
@@ -23173,34 +23600,34 @@ var CONTEXTSTREAM_PREAMBLE_PATTERNS = [
23173
23600
  /^#\s+roo code rules$/i,
23174
23601
  /^#\s+aider configuration$/i
23175
23602
  ];
23176
- function wrapWithMarkers(content) {
23177
- return `${CONTEXTSTREAM_START_MARKER}
23603
+ function wrapWithMarkers2(content) {
23604
+ return `${CONTEXTSTREAM_START_MARKER2}
23178
23605
  ${content.trim()}
23179
- ${CONTEXTSTREAM_END_MARKER}`;
23606
+ ${CONTEXTSTREAM_END_MARKER2}`;
23180
23607
  }
23181
- function isLegacyContextStreamRules(content) {
23608
+ function isLegacyContextStreamRules2(content) {
23182
23609
  const lower = content.toLowerCase();
23183
23610
  if (!lower.includes("contextstream")) return false;
23184
- if (!LEGACY_CONTEXTSTREAM_HINTS.some((hint) => lower.includes(hint))) return false;
23611
+ if (!LEGACY_CONTEXTSTREAM_HINTS2.some((hint) => lower.includes(hint))) return false;
23185
23612
  const headingRegex = /^#{1,6}\s+(.+)$/gm;
23186
23613
  let hasHeading = false;
23187
23614
  let match;
23188
23615
  while ((match = headingRegex.exec(content)) !== null) {
23189
23616
  hasHeading = true;
23190
23617
  const heading = match[1].trim().toLowerCase();
23191
- const allowed = LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS.some(
23618
+ const allowed = LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS2.some(
23192
23619
  (prefix) => heading.startsWith(prefix)
23193
23620
  );
23194
23621
  if (!allowed) return false;
23195
23622
  }
23196
23623
  return hasHeading;
23197
23624
  }
23198
- function isContextStreamPreamble(line) {
23625
+ function isContextStreamPreamble2(line) {
23199
23626
  const trimmed = line.trim();
23200
23627
  if (!trimmed) return false;
23201
- return CONTEXTSTREAM_PREAMBLE_PATTERNS.some((pattern) => pattern.test(trimmed));
23628
+ return CONTEXTSTREAM_PREAMBLE_PATTERNS2.some((pattern) => pattern.test(trimmed));
23202
23629
  }
23203
- function findContextStreamHeading(lines) {
23630
+ function findContextStreamHeading2(lines) {
23204
23631
  const headingRegex = /^(#{1,6})\s+(.+)$/;
23205
23632
  for (let i = 0; i < lines.length; i += 1) {
23206
23633
  const match = headingRegex.exec(lines[i]);
@@ -23211,7 +23638,7 @@ function findContextStreamHeading(lines) {
23211
23638
  }
23212
23639
  return null;
23213
23640
  }
23214
- function findSectionEnd(lines, startLine, level) {
23641
+ function findSectionEnd2(lines, startLine, level) {
23215
23642
  const headingRegex = /^(#{1,6})\s+(.+)$/;
23216
23643
  for (let i = startLine + 1; i < lines.length; i += 1) {
23217
23644
  const match = headingRegex.exec(lines[i]);
@@ -23220,16 +23647,16 @@ function findSectionEnd(lines, startLine, level) {
23220
23647
  }
23221
23648
  return lines.length;
23222
23649
  }
23223
- function extractContextStreamBlock(content) {
23650
+ function extractContextStreamBlock2(content) {
23224
23651
  const lines = content.split(/\r?\n/);
23225
- const heading = findContextStreamHeading(lines);
23652
+ const heading = findContextStreamHeading2(lines);
23226
23653
  if (!heading) return content.trim();
23227
- const endLine = findSectionEnd(lines, heading.index, heading.level);
23654
+ const endLine = findSectionEnd2(lines, heading.index, heading.level);
23228
23655
  return lines.slice(heading.index, endLine).join("\n").trim();
23229
23656
  }
23230
- function findLegacyContextStreamSection(content) {
23657
+ function findLegacyContextStreamSection2(content) {
23231
23658
  const lines = content.split(/\r?\n/);
23232
- const heading = findContextStreamHeading(lines);
23659
+ const heading = findContextStreamHeading2(lines);
23233
23660
  if (!heading) return null;
23234
23661
  let startLine = heading.index;
23235
23662
  for (let i = heading.index - 1; i >= 0; i -= 1) {
@@ -23238,39 +23665,39 @@ function findLegacyContextStreamSection(content) {
23238
23665
  startLine = i;
23239
23666
  continue;
23240
23667
  }
23241
- if (isContextStreamPreamble(line)) {
23668
+ if (isContextStreamPreamble2(line)) {
23242
23669
  startLine = i;
23243
23670
  continue;
23244
23671
  }
23245
23672
  break;
23246
23673
  }
23247
- const endLine = findSectionEnd(lines, heading.index, heading.level);
23674
+ const endLine = findSectionEnd2(lines, heading.index, heading.level);
23248
23675
  return { startLine, endLine, contextLine: heading.index };
23249
23676
  }
23250
- function blockHasPreamble(block) {
23677
+ function blockHasPreamble2(block) {
23251
23678
  const lines = block.split(/\r?\n/);
23252
23679
  for (const line of lines) {
23253
23680
  const trimmed = line.trim();
23254
23681
  if (!trimmed) continue;
23255
- if (isContextStreamPreamble(trimmed)) return true;
23682
+ if (isContextStreamPreamble2(trimmed)) return true;
23256
23683
  if (/^#{1,6}\s+/.test(trimmed)) return false;
23257
23684
  }
23258
23685
  return false;
23259
23686
  }
23260
- function replaceContextStreamBlock(existing, content) {
23261
- const fullWrapped = wrapWithMarkers(content);
23262
- const blockWrapped = wrapWithMarkers(extractContextStreamBlock(content));
23263
- const startIdx = existing.indexOf(CONTEXTSTREAM_START_MARKER);
23264
- const endIdx = existing.indexOf(CONTEXTSTREAM_END_MARKER);
23687
+ function replaceContextStreamBlock2(existing, content) {
23688
+ const fullWrapped = wrapWithMarkers2(content);
23689
+ const blockWrapped = wrapWithMarkers2(extractContextStreamBlock2(content));
23690
+ const startIdx = existing.indexOf(CONTEXTSTREAM_START_MARKER2);
23691
+ const endIdx = existing.indexOf(CONTEXTSTREAM_END_MARKER2);
23265
23692
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
23266
- const existingBlock = existing.slice(startIdx + CONTEXTSTREAM_START_MARKER.length, endIdx);
23267
- const replacement = blockHasPreamble(existingBlock) ? fullWrapped : blockWrapped;
23693
+ const existingBlock = existing.slice(startIdx + CONTEXTSTREAM_START_MARKER2.length, endIdx);
23694
+ const replacement = blockHasPreamble2(existingBlock) ? fullWrapped : blockWrapped;
23268
23695
  const before = existing.substring(0, startIdx).trimEnd();
23269
- const after = existing.substring(endIdx + CONTEXTSTREAM_END_MARKER.length).trimStart();
23696
+ const after = existing.substring(endIdx + CONTEXTSTREAM_END_MARKER2.length).trimStart();
23270
23697
  const merged = [before, replacement, after].filter((part) => part.length > 0).join("\n\n");
23271
23698
  return { content: merged.trim() + "\n", status: "updated" };
23272
23699
  }
23273
- const legacy = findLegacyContextStreamSection(existing);
23700
+ const legacy = findLegacyContextStreamSection2(existing);
23274
23701
  if (legacy) {
23275
23702
  const lines = existing.split(/\r?\n/);
23276
23703
  const before = lines.slice(0, legacy.startLine).join("\n").trimEnd();
@@ -23279,39 +23706,39 @@ function replaceContextStreamBlock(existing, content) {
23279
23706
  const merged = [before, replacement, after].filter((part) => part.length > 0).join("\n\n");
23280
23707
  return { content: merged.trim() + "\n", status: "updated" };
23281
23708
  }
23282
- if (isLegacyContextStreamRules(existing)) {
23709
+ if (isLegacyContextStreamRules2(existing)) {
23283
23710
  return { content: fullWrapped + "\n", status: "updated" };
23284
23711
  }
23285
23712
  const appended = existing.trimEnd() + "\n\n" + blockWrapped + "\n";
23286
23713
  return { content: appended, status: "appended" };
23287
23714
  }
23288
23715
  async function upsertRuleFile(filePath, content, writeMode = "contextstream_block") {
23289
- await fs7.promises.mkdir(path8.dirname(filePath), { recursive: true });
23716
+ await fs9.promises.mkdir(path11.dirname(filePath), { recursive: true });
23290
23717
  if (writeMode === "full_file") {
23291
23718
  let existing2 = "";
23292
23719
  try {
23293
- existing2 = await fs7.promises.readFile(filePath, "utf8");
23720
+ existing2 = await fs9.promises.readFile(filePath, "utf8");
23294
23721
  } catch {
23295
23722
  }
23296
- await fs7.promises.writeFile(filePath, content.trimEnd() + "\n", "utf8");
23723
+ await fs9.promises.writeFile(filePath, content.trimEnd() + "\n", "utf8");
23297
23724
  return existing2 ? "updated" : "created";
23298
23725
  }
23299
- const wrappedContent = wrapWithMarkers(content);
23726
+ const wrappedContent = wrapWithMarkers2(content);
23300
23727
  let existing = "";
23301
23728
  try {
23302
- existing = await fs7.promises.readFile(filePath, "utf8");
23729
+ existing = await fs9.promises.readFile(filePath, "utf8");
23303
23730
  } catch {
23304
23731
  }
23305
23732
  if (!existing) {
23306
- await fs7.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
23733
+ await fs9.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
23307
23734
  return "created";
23308
23735
  }
23309
23736
  if (!existing.trim()) {
23310
- await fs7.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
23737
+ await fs9.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
23311
23738
  return "updated";
23312
23739
  }
23313
- const replaced = replaceContextStreamBlock(existing, content);
23314
- await fs7.promises.writeFile(filePath, replaced.content, "utf8");
23740
+ const replaced = replaceContextStreamBlock2(existing, content);
23741
+ await fs9.promises.writeFile(filePath, replaced.content, "utf8");
23315
23742
  return replaced.status;
23316
23743
  }
23317
23744
  async function writeEditorRules(options) {
@@ -23330,8 +23757,8 @@ async function writeEditorRules(options) {
23330
23757
  continue;
23331
23758
  }
23332
23759
  for (const rule of ruleFiles) {
23333
- const filePath = path8.join(options.folderPath, rule.filename);
23334
- if (fs7.existsSync(filePath) && !options.overwriteExisting) {
23760
+ const filePath = path11.join(options.folderPath, rule.filename);
23761
+ if (fs9.existsSync(filePath) && !options.overwriteExisting) {
23335
23762
  results.push({ editor, filename: rule.filename, status: "skipped (exists)" });
23336
23763
  continue;
23337
23764
  }
@@ -23388,7 +23815,7 @@ async function writeGlobalRules(options) {
23388
23815
  continue;
23389
23816
  }
23390
23817
  for (const filePath of globalPaths) {
23391
- if (fs7.existsSync(filePath) && !options.overwriteExisting) {
23818
+ if (fs9.existsSync(filePath) && !options.overwriteExisting) {
23392
23819
  results.push({ editor, filename: filePath, status: "skipped (exists)", scope: "global" });
23393
23820
  continue;
23394
23821
  }
@@ -24578,6 +25005,27 @@ function scoreConfidenceBand(score) {
24578
25005
  if (score >= 0.6) return "medium";
24579
25006
  return "low";
24580
25007
  }
25008
+ function isExactIdentifierQuery(query) {
25009
+ const trimmed = query.trim();
25010
+ if (!trimmed || trimmed.length < 2 || trimmed.includes(" ")) return false;
25011
+ if (!/^[A-Za-z0-9_:]+$/.test(trimmed)) return false;
25012
+ const hasMixedCase = /[a-z]/.test(trimmed) && /[A-Z]/.test(trimmed);
25013
+ const hasUnderscore = trimmed.includes("_");
25014
+ const isAllCaps = trimmed.length >= 3 && /^[A-Z0-9_]+$/.test(trimmed);
25015
+ return hasMixedCase || hasUnderscore || isAllCaps;
25016
+ }
25017
+ function exactQuotedLiteral(query) {
25018
+ const trimmed = query.trim();
25019
+ if (trimmed.length < 2) return void 0;
25020
+ const isDoubleQuoted = trimmed.startsWith('"') && trimmed.endsWith('"');
25021
+ const isSingleQuoted = trimmed.startsWith("'") && trimmed.endsWith("'");
25022
+ if (!isDoubleQuoted && !isSingleQuoted) return void 0;
25023
+ const literal = trimmed.slice(1, -1).trim();
25024
+ return literal || void 0;
25025
+ }
25026
+ function allowBroadSearchFallbacks(modeAutoSelected, query) {
25027
+ return modeAutoSelected && !isExactIdentifierQuery(query) && exactQuotedLiteral(query) === void 0;
25028
+ }
24581
25029
  function queryHasCodeIntent(query) {
24582
25030
  const trimmed = query.trim();
24583
25031
  if (!trimmed) return false;
@@ -25114,7 +25562,7 @@ async function resolveTargetProjectId(client, workspaceId, targetProject) {
25114
25562
  const names = [
25115
25563
  project?.name,
25116
25564
  project?.slug,
25117
- project?.path ? path8.basename(String(project.path)) : void 0
25565
+ project?.path ? path11.basename(String(project.path)) : void 0
25118
25566
  ].filter((item) => typeof item === "string").map((item) => item.trim().toLowerCase());
25119
25567
  return names.includes(normalized);
25120
25568
  });
@@ -26085,7 +26533,7 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
26085
26533
  );
26086
26534
  }
26087
26535
  function projectScopeRequiredResult(action, workspaceId, folderPath) {
26088
- const suggestedProjectName = folderPath ? path8.basename(folderPath) || "default-project" : "default-project";
26536
+ const suggestedProjectName = folderPath ? path11.basename(folderPath) || "default-project" : "default-project";
26089
26537
  const createCommand = workspaceId ? folderPath ? `project(action="create", name="${suggestedProjectName}", workspace_id="${workspaceId}", folder_path="${folderPath}")` : `project(action="create", name="${suggestedProjectName}", workspace_id="${workspaceId}")` : folderPath ? `init(folder_path="${folderPath}")` : `init(folder_path="<your_project_path>")`;
26090
26538
  const scopeHint = workspaceId ? "Workspace-only fallback is active for this folder, but this action requires a project scope." : "No workspace/project scope is currently resolved for this folder.";
26091
26539
  return errorResult(
@@ -26270,16 +26718,7 @@ Next step: ${createCommand}`
26270
26718
  return max;
26271
26719
  }
26272
26720
  function extractQuotedLiteral(query) {
26273
- const trimmed = query.trim();
26274
- if (trimmed.length < 2) return void 0;
26275
- const isDoubleQuoted = trimmed.startsWith('"') && trimmed.endsWith('"');
26276
- const isSingleQuoted = trimmed.startsWith("'") && trimmed.endsWith("'");
26277
- if (!isDoubleQuoted && !isSingleQuoted) return void 0;
26278
- const literal = trimmed.slice(1, -1).trim();
26279
- return literal || void 0;
26280
- }
26281
- function escapeRegexLiteral(input) {
26282
- return input.replace(/[\\.+*?()[\]{}|^$]/g, "\\$&");
26721
+ return exactQuotedLiteral(query);
26283
26722
  }
26284
26723
  const COUNT_QUERY_PREFIXES = ["how many ", "count ", "count of ", "number of ", "total "];
26285
26724
  const ALL_MATCH_KEYWORDS = [
@@ -26315,13 +26754,7 @@ Next step: ${createCommand}`
26315
26754
  return containsCodeIdentifiers(query) || isIdentifierQuery(query) ? 0.04 : SEMANTIC_SWITCH_MIN_IMPROVEMENT;
26316
26755
  }
26317
26756
  function isIdentifierQuery(query) {
26318
- const trimmed = query.trim();
26319
- if (!trimmed || trimmed.length < 2 || trimmed.includes(" ")) return false;
26320
- if (!/^[A-Za-z0-9_:]+$/.test(trimmed)) return false;
26321
- const hasMixedCase = /[a-z]/.test(trimmed) && /[A-Z]/.test(trimmed);
26322
- const hasUnderscore = trimmed.includes("_");
26323
- const isAllCaps = trimmed.length >= 3 && /^[A-Z0-9_]+$/.test(trimmed);
26324
- return hasMixedCase || hasUnderscore || isAllCaps;
26757
+ return isExactIdentifierQuery(query);
26325
26758
  }
26326
26759
  function hasRegexCharacters(query) {
26327
26760
  const trimmed = query.trim();
@@ -26602,12 +27035,12 @@ Next step: ${createCommand}`
26602
27035
  async function indexedProjectIdForFolder(folderPath) {
26603
27036
  const status = await readIndexStatus();
26604
27037
  const projects = status.projects ?? {};
26605
- const resolvedFolder = path8.resolve(folderPath);
27038
+ const resolvedFolder = path11.resolve(folderPath);
26606
27039
  let bestMatch;
26607
27040
  let currentRemotePromise;
26608
27041
  for (const [projectPath, info] of Object.entries(projects)) {
26609
- const resolvedProjectPath = path8.resolve(projectPath);
26610
- const matches = resolvedFolder === resolvedProjectPath || resolvedFolder.startsWith(`${resolvedProjectPath}${path8.sep}`) || resolvedProjectPath.startsWith(`${resolvedFolder}${path8.sep}`);
27042
+ const resolvedProjectPath = path11.resolve(projectPath);
27043
+ const matches = resolvedFolder === resolvedProjectPath || resolvedFolder.startsWith(`${resolvedProjectPath}${path11.sep}`) || resolvedProjectPath.startsWith(`${resolvedFolder}${path11.sep}`);
26611
27044
  if (!matches) continue;
26612
27045
  if (!info?.indexed_at) continue;
26613
27046
  if (info?.git_remote) {
@@ -26677,10 +27110,22 @@ Next step: ${createCommand}`
26677
27110
  }
26678
27111
  }
26679
27112
  async function validateReadableDirectory(inputPath) {
26680
- const resolvedPath = path8.resolve(inputPath);
27113
+ const resolvedPath = path11.resolve(inputPath);
27114
+ if (isInvariantIgnoredPath(resolvedPath)) {
27115
+ return {
27116
+ ok: false,
27117
+ error: `Error: refusing to index a path inside agent state or a credential store: ${inputPath}. Pass the actual repository checkout instead.`
27118
+ };
27119
+ }
27120
+ if (isBroadProjectPath(resolvedPath) && process.env.CONTEXTSTREAM_ALLOW_BROAD_INGEST !== "1") {
27121
+ return {
27122
+ ok: false,
27123
+ error: `Error: refusing to index filesystem root or home directory: ${inputPath}. Pass a narrower project path, or set CONTEXTSTREAM_ALLOW_BROAD_INGEST=1 for an intentional broad ingest.`
27124
+ };
27125
+ }
26681
27126
  let stats;
26682
27127
  try {
26683
- stats = await fs7.promises.stat(resolvedPath);
27128
+ stats = await fs9.promises.stat(resolvedPath);
26684
27129
  } catch (error) {
26685
27130
  if (error?.code === "ENOENT") {
26686
27131
  return { ok: false, error: `Error: path does not exist: ${inputPath}` };
@@ -26694,7 +27139,7 @@ Next step: ${createCommand}`
26694
27139
  return { ok: false, error: `Error: path is not a directory: ${inputPath}` };
26695
27140
  }
26696
27141
  try {
26697
- await fs7.promises.access(resolvedPath, fs7.constants.R_OK | fs7.constants.X_OK);
27142
+ await fs9.promises.access(resolvedPath, fs9.constants.R_OK | fs9.constants.X_OK);
26698
27143
  } catch (error) {
26699
27144
  return {
26700
27145
  ok: false,
@@ -26703,6 +27148,40 @@ Next step: ${createCommand}`
26703
27148
  }
26704
27149
  return { ok: true, resolvedPath };
26705
27150
  }
27151
+ async function remoteIndexStateResult(action, projectId, requestedPath) {
27152
+ let statusResult = {};
27153
+ try {
27154
+ statusResult = await client.projectIndexStatus(projectId);
27155
+ } catch (error) {
27156
+ if (!isNotFoundError(error)) throw error;
27157
+ }
27158
+ const status = classifyRemoteIndexState(statusResult);
27159
+ const filesIndexed = extractIndexedFileCount(statusResult);
27160
+ const target = requestedPath ? `'${requestedPath}'` : "the project folder";
27161
+ const remoteNote = `This MCP process cannot read ${target}; no workstation files were ingested by this call.`;
27162
+ let message;
27163
+ if (status === "server_index_ready") {
27164
+ const countNote = filesIndexed !== void 0 ? ` (${filesIndexed} files)` : "";
27165
+ message = `Server-side index is ready for project ${projectId}${countNote}. ${remoteNote} Keep local changes current with the ContextStream sync bridge, editor hooks, or ContextStream Desktop, and verify freshness with project(action="index_status").`;
27166
+ } else if (status === "server_indexing") {
27167
+ message = `A server-side index run is already in progress for project ${projectId}. ${remoteNote} Monitor it with project(action="index_status").`;
27168
+ } else {
27169
+ message = `No exact-checkout content upload has completed for project ${projectId}. ${remoteNote} Keep hosted MCP configured and index from a process that can read the checkout, the ContextStream sync bridge, editor hooks, or ContextStream Desktop. Then retry project(action="index") and verify with project(action="index_status").`;
27170
+ }
27171
+ const structured = {
27172
+ status,
27173
+ invoked_action: action,
27174
+ project_id: projectId,
27175
+ path: requestedPath ?? null,
27176
+ local_path_readable: false,
27177
+ files_indexed: filesIndexed ?? null,
27178
+ message
27179
+ };
27180
+ return {
27181
+ content: [{ type: "text", text: message }],
27182
+ structuredContent: structured
27183
+ };
27184
+ }
26706
27185
  function startBackgroundIngest(projectId, resolvedPath, ingestOptions, options2 = {}) {
26707
27186
  void (async () => {
26708
27187
  const [gitHead, gitRemote] = await Promise.all([
@@ -26733,6 +27212,9 @@ Next step: ${createCommand}`
26733
27212
  force: ingestOptions.force,
26734
27213
  ingestOptions
26735
27214
  });
27215
+ if (result.status === "error") {
27216
+ throw new Error(result.lastError || "all changed-file ingest batches failed");
27217
+ }
26736
27218
  logTool(
26737
27219
  "ingest",
26738
27220
  "done",
@@ -27098,9 +27580,10 @@ ${formatContent(result)}`
27098
27580
  inputSchema: external_exports.object({})
27099
27581
  },
27100
27582
  async () => {
27101
- const result = { name: "contextstream-mcp", version: VERSION };
27583
+ const result = await loadMcpVersionInfo(client);
27102
27584
  return {
27103
- content: [{ type: "text", text: formatContent(result) }]
27585
+ content: [{ type: "text", text: formatContent(result) }],
27586
+ structuredContent: result
27104
27587
  };
27105
27588
  }
27106
27589
  );
@@ -27695,17 +28178,17 @@ Access: Free`,
27695
28178
  let rulesSkipped = [];
27696
28179
  if (input.folder_path && projectData.id) {
27697
28180
  try {
27698
- const configDir = path8.join(input.folder_path, ".contextstream");
27699
- const configPath = path8.join(configDir, "config.json");
27700
- if (!fs7.existsSync(configDir)) {
27701
- fs7.mkdirSync(configDir, { recursive: true });
28181
+ const configDir = path11.join(input.folder_path, ".contextstream");
28182
+ const configPath = path11.join(configDir, "config.json");
28183
+ if (!fs9.existsSync(configDir)) {
28184
+ fs9.mkdirSync(configDir, { recursive: true });
27702
28185
  }
27703
28186
  const config = {
27704
28187
  workspace_id: workspaceId,
27705
28188
  project_id: projectData.id,
27706
28189
  project_name: input.name
27707
28190
  };
27708
- fs7.writeFileSync(configPath, JSON.stringify(config, null, 2));
28191
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2));
27709
28192
  if (input.generate_editor_rules) {
27710
28193
  const ruleResults = await writeEditorRules({
27711
28194
  folderPath: input.folder_path,
@@ -29291,7 +29774,7 @@ This does semantic search on the first message. You only need context on subsequ
29291
29774
  formatContent(result)
29292
29775
  ].join("\n");
29293
29776
  } else if (status === "requires_workspace_selection") {
29294
- const folderName = typeof result.folder_name === "string" ? result.folder_name : typeof input.folder_path === "string" ? path8.basename(input.folder_path) || "this folder" : "this folder";
29777
+ const folderName = typeof result.folder_name === "string" ? result.folder_name : typeof input.folder_path === "string" ? path11.basename(input.folder_path) || "this folder" : "this folder";
29295
29778
  const candidates = Array.isArray(result.workspace_candidates) ? result.workspace_candidates : [];
29296
29779
  const lines = [];
29297
29780
  lines.push(
@@ -29544,7 +30027,17 @@ Behavior:
29544
30027
  "Error: folder_path is required. Provide folder_path or run from a project directory."
29545
30028
  );
29546
30029
  }
29547
- const folderName = path8.basename(folderPath) || "My Project";
30030
+ if (isInvariantIgnoredPath(folderPath)) {
30031
+ return errorResult(
30032
+ `Refusing to bootstrap a path inside agent state or a credential store: ${folderPath}. Pass the actual repository checkout instead.`
30033
+ );
30034
+ }
30035
+ if (isBroadProjectPath(folderPath) && process.env.CONTEXTSTREAM_ALLOW_BROAD_INGEST !== "1") {
30036
+ return errorResult(
30037
+ `Refusing to bootstrap filesystem root or home directory as a project: ${folderPath}. Pass a narrower repository/project path.`
30038
+ );
30039
+ }
30040
+ const folderName = path11.basename(folderPath) || "My Project";
29548
30041
  let newWorkspace;
29549
30042
  try {
29550
30043
  newWorkspace = await client.createWorkspace({
@@ -32383,6 +32876,10 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32383
32876
  const modeRecommendation = recommendSearchMode(input.query);
32384
32877
  const modeAutoSelected = modeInput === "auto";
32385
32878
  const requestedMode = modeAutoSelected ? modeRecommendation.mode : modeInput;
32879
+ const broadSearchFallbacksAllowed = allowBroadSearchFallbacks(
32880
+ modeAutoSelected,
32881
+ input.query
32882
+ );
32386
32883
  let workspaceId = resolveWorkspaceId(input.workspace_id);
32387
32884
  const sessionProjectId = resolveProjectId(void 0);
32388
32885
  const requestedExplicitProjectId = normalizeUuid(input.project_id);
@@ -32446,7 +32943,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32446
32943
  crawl: "search_crawl",
32447
32944
  team: "search_hybrid"
32448
32945
  };
32449
- const runSearchForMode = async (mode, baseParams2) => {
32946
+ const runSearchForMode = async (mode, baseParams2, allowBroadFallbacks) => {
32450
32947
  if (mode === "team") {
32451
32948
  const isTeamPlanForSearch = await client.isTeamPlan();
32452
32949
  if (!isTeamPlanForSearch) {
@@ -32494,8 +32991,26 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32494
32991
  let result;
32495
32992
  let executedMode = dispatchMode;
32496
32993
  let fallbackNote;
32994
+ const quotedLiteral = dispatchMode === "keyword" ? extractQuotedLiteral(input.query) : void 0;
32995
+ const identifierExhaustiveFastPath = dispatchMode === "keyword" && quotedLiteral === void 0 && baseParams2.include_memory !== true && isIdentifierQuery(baseParams2.query);
32996
+ const primaryParams = quotedLiteral ? { ...baseParams2, query: quotedLiteral } : baseParams2;
32497
32997
  try {
32498
- result = await executeSearchMode(dispatchMode, baseParams2);
32998
+ if (identifierExhaustiveFastPath) {
32999
+ try {
33000
+ result = await executeSearchMode("exhaustive", baseParams2);
33001
+ executedMode = "exhaustive";
33002
+ fallbackNote = "Exact identifier query used the exhaustive literal fast path.";
33003
+ } catch {
33004
+ result = await executeSearchMode("keyword", baseParams2);
33005
+ executedMode = "keyword";
33006
+ fallbackNote = "Exact identifier exhaustive fast path was unavailable; used keyword search.";
33007
+ }
33008
+ } else {
33009
+ result = await executeSearchMode(dispatchMode, primaryParams);
33010
+ if (quotedLiteral) {
33011
+ fallbackNote = "Normalized surrounding quotes before exact keyword search.";
33012
+ }
33013
+ }
32499
33014
  } catch (execError) {
32500
33015
  const execMsg = execError instanceof Error ? execError.message : String(execError);
32501
33016
  const isEmbedTimeout = execMsg.toLowerCase().includes("embedding timed out") || execMsg.toLowerCase().includes("embedding timeout");
@@ -32507,7 +33022,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32507
33022
  throw execError;
32508
33023
  }
32509
33024
  }
32510
- if (shouldRetrySemanticFallback(input.query, dispatchMode, result)) {
33025
+ if (allowBroadFallbacks && shouldRetrySemanticFallback(input.query, dispatchMode, result)) {
32511
33026
  try {
32512
33027
  const semanticResult = await executeSearchMode("semantic", baseParams2);
32513
33028
  if (shouldPreferSemanticResults(input.query, result, semanticResult)) {
@@ -32522,63 +33037,38 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32522
33037
  }
32523
33038
  }
32524
33039
  const currentResults = () => extractSearchEnvelope(result).results;
32525
- if (dispatchMode === "keyword" && currentResults().length === 0) {
32526
- const literal = extractQuotedLiteral(input.query);
32527
- if (literal) {
32528
- if (literal !== baseParams2.query) {
32529
- try {
32530
- const keywordUnquoted = await executeSearchMode("keyword", {
32531
- ...baseParams2,
32532
- query: literal
32533
- });
32534
- if (extractSearchEnvelope(keywordUnquoted).results.length > 0) {
32535
- result = keywordUnquoted;
32536
- executedMode = "keyword";
32537
- fallbackNote = appendNote(
32538
- fallbackNote,
32539
- "Quoted keyword search returned no results; retried keyword search without quotes."
32540
- );
32541
- }
32542
- } catch {
32543
- }
32544
- }
32545
- if (currentResults().length === 0) {
32546
- try {
32547
- const patternResult = await executeSearchMode("pattern", {
32548
- ...baseParams2,
32549
- query: escapeRegexLiteral(literal)
32550
- });
32551
- if (extractSearchEnvelope(patternResult).results.length > 0) {
32552
- result = patternResult;
32553
- executedMode = "pattern";
32554
- fallbackNote = appendNote(
32555
- fallbackNote,
32556
- "Quoted keyword search returned no results; retried literal pattern search."
32557
- );
32558
- }
32559
- } catch {
32560
- }
33040
+ if (quotedLiteral && currentResults().length === 0) {
33041
+ try {
33042
+ const exhaustiveResult = await executeSearchMode("exhaustive", {
33043
+ ...baseParams2,
33044
+ query: quotedLiteral
33045
+ });
33046
+ if (extractSearchEnvelope(exhaustiveResult).results.length > 0) {
33047
+ result = exhaustiveResult;
33048
+ executedMode = "exhaustive";
33049
+ fallbackNote = appendNote(
33050
+ fallbackNote,
33051
+ "Exact keyword search returned no results; retried exhaustive search for complete literal coverage."
33052
+ );
32561
33053
  }
32562
- if (currentResults().length === 0) {
32563
- try {
32564
- const exhaustiveResult = await executeSearchMode("exhaustive", {
32565
- ...baseParams2,
32566
- query: literal
32567
- });
32568
- if (extractSearchEnvelope(exhaustiveResult).results.length > 0) {
32569
- result = exhaustiveResult;
32570
- executedMode = "exhaustive";
32571
- fallbackNote = appendNote(
32572
- fallbackNote,
32573
- "Quoted keyword search returned no results; retried exhaustive search for complete literal coverage."
32574
- );
32575
- }
32576
- } catch {
32577
- }
33054
+ } catch {
33055
+ }
33056
+ }
33057
+ if (identifierExhaustiveFastPath && currentResults().length === 0) {
33058
+ try {
33059
+ const keywordResult = await executeSearchMode("keyword", baseParams2);
33060
+ if (extractSearchEnvelope(keywordResult).results.length > 0) {
33061
+ result = keywordResult;
33062
+ executedMode = "keyword";
33063
+ fallbackNote = appendNote(
33064
+ fallbackNote,
33065
+ "Exact identifier exhaustive lookup returned no results; keyword fallback found tokenized matches."
33066
+ );
32578
33067
  }
33068
+ } catch {
32579
33069
  }
32580
33070
  }
32581
- if ((dispatchMode === "refactor" || dispatchMode === "exhaustive") && currentResults().length === 0) {
33071
+ if (allowBroadFallbacks && (dispatchMode === "refactor" || dispatchMode === "exhaustive") && currentResults().length === 0) {
32582
33072
  try {
32583
33073
  const keywordResult = await executeSearchMode("keyword", baseParams2);
32584
33074
  if (extractSearchEnvelope(keywordResult).results.length > 0) {
@@ -32606,7 +33096,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32606
33096
  }
32607
33097
  } catch {
32608
33098
  }
32609
- if (currentResults().length === 0) {
33099
+ if (currentResults().length === 0 && !identifierExhaustiveFastPath) {
32610
33100
  try {
32611
33101
  const exhaustiveResult = await executeSearchMode("exhaustive", baseParams2);
32612
33102
  if (extractSearchEnvelope(exhaustiveResult).results.length > 0) {
@@ -32621,7 +33111,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32621
33111
  }
32622
33112
  }
32623
33113
  }
32624
- if (currentResults().length === 0 && shouldRetryKeywordWithSemantic(input.query)) {
33114
+ if (allowBroadFallbacks && currentResults().length === 0 && shouldRetryKeywordWithSemantic(input.query)) {
32625
33115
  try {
32626
33116
  const semanticResult = await executeSearchMode("semantic", baseParams2);
32627
33117
  if (extractSearchEnvelope(semanticResult).results.length > 0) {
@@ -32635,7 +33125,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32635
33125
  } catch {
32636
33126
  }
32637
33127
  }
32638
- if (currentResults().length === 0) {
33128
+ if (allowBroadFallbacks && currentResults().length === 0) {
32639
33129
  try {
32640
33130
  const hybridResult = await executeSearchMode("hybrid", baseParams2);
32641
33131
  if (extractSearchEnvelope(hybridResult).results.length > 0) {
@@ -32675,10 +33165,14 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32675
33165
  }
32676
33166
  if (requestedMode === "team") {
32677
33167
  try {
32678
- const teamResult = await runSearchForMode("team", {
32679
- ...baseParams,
32680
- project_id: void 0
32681
- });
33168
+ const teamResult = await runSearchForMode(
33169
+ "team",
33170
+ {
33171
+ ...baseParams,
33172
+ project_id: void 0
33173
+ },
33174
+ broadSearchFallbacksAllowed
33175
+ );
32682
33176
  selected = {
32683
33177
  index: 0,
32684
33178
  project_id: void 0,
@@ -32698,7 +33192,11 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32698
33192
  project_id: candidateProjectId
32699
33193
  };
32700
33194
  try {
32701
- const modeResult = await runSearchForMode(requestedMode, paramsForCandidate);
33195
+ const modeResult = await runSearchForMode(
33196
+ requestedMode,
33197
+ paramsForCandidate,
33198
+ broadSearchFallbacksAllowed
33199
+ );
32702
33200
  const envelope = extractSearchEnvelope(modeResult.result);
32703
33201
  if (isScopeInvalidResult(modeResult.result)) {
32704
33202
  if (!selected) {
@@ -32742,7 +33240,11 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32742
33240
  const isEmbeddingTimeout = errMsg.toLowerCase().includes("embedding timed out") || errMsg.toLowerCase().includes("embedding timeout");
32743
33241
  if (isEmbeddingTimeout && requestedMode !== "keyword" && requestedMode !== "exhaustive") {
32744
33242
  try {
32745
- const keywordFallback = await runSearchForMode("keyword", paramsForCandidate);
33243
+ const keywordFallback = await runSearchForMode(
33244
+ "keyword",
33245
+ paramsForCandidate,
33246
+ false
33247
+ );
32746
33248
  const kwEnvelope = extractSearchEnvelope(keywordFallback.result);
32747
33249
  selected = {
32748
33250
  index,
@@ -32842,9 +33344,9 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32842
33344
  const beforePrune = results.length;
32843
33345
  results = results.filter((r) => {
32844
33346
  if (typeof r?.file_path !== "string" || !r.file_path) return true;
32845
- if (path8.isAbsolute(r.file_path)) return true;
33347
+ if (path11.isAbsolute(r.file_path)) return true;
32846
33348
  try {
32847
- return fs7.existsSync(path8.join(folderPath, r.file_path));
33349
+ return fs9.existsSync(path11.join(folderPath, r.file_path));
32848
33350
  } catch {
32849
33351
  return true;
32850
33352
  }
@@ -32857,7 +33359,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32857
33359
  }
32858
33360
  try {
32859
33361
  const indexStatusFile = await readIndexStatus();
32860
- const indexEntry = indexStatusFile.projects?.[path8.resolve(folderPath)];
33362
+ const indexEntry = indexStatusFile.projects?.[path11.resolve(folderPath)];
32861
33363
  if (indexEntry?.indexed_at) {
32862
33364
  const indexedAtMs = Date.parse(indexEntry.indexed_at);
32863
33365
  const dirtyCount = await countDirtyFilesSince(
@@ -32874,7 +33376,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32874
33376
  } catch {
32875
33377
  }
32876
33378
  }
32877
- if (results.length === 0 && !scopeInvalid) {
33379
+ if (results.length === 0 && !scopeInvalid && broadSearchFallbacksAllowed) {
32878
33380
  const escalationModes = (() => {
32879
33381
  switch (selected.executedMode) {
32880
33382
  case "semantic":
@@ -32889,16 +33391,23 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
32889
33391
  })();
32890
33392
  for (const escMode of escalationModes) {
32891
33393
  try {
32892
- const escResult = await runSearchForMode(escMode, {
32893
- ...baseParams,
32894
- workspace_id: workspaceId,
32895
- project_id: selected.project_id
32896
- });
33394
+ const escResult = await runSearchForMode(
33395
+ escMode,
33396
+ {
33397
+ ...baseParams,
33398
+ workspace_id: workspaceId,
33399
+ project_id: selected.project_id
33400
+ },
33401
+ false
33402
+ );
32897
33403
  const escEnvelope = extractSearchEnvelope(escResult.result);
32898
33404
  if (escEnvelope.results.length > 0) {
32899
33405
  results = escEnvelope.results;
32900
33406
  total = escEnvelope.total;
32901
- modeFallbackNote = appendNote(modeFallbackNote, `${selected.executedMode} returned 0 results; escalated to ${escMode}.`);
33407
+ modeFallbackNote = appendNote(
33408
+ modeFallbackNote,
33409
+ `${selected.executedMode} returned 0 results; escalated to ${escMode}.`
33410
+ );
32902
33411
  break;
32903
33412
  }
32904
33413
  } catch {
@@ -33251,7 +33760,7 @@ ${listing}`
33251
33760
  "session",
33252
33761
  {
33253
33762
  title: "Session",
33254
- description: `Session and memory management \u2014 NOT for codebase/file search (use the 'search' tool for that). LESSONS LIVE HERE: when a mistake or correction happens, call action='capture_lesson' (NEVER write lessons to ~/.claude/.../memory/, .cursorrules, or other local markdown \u2014 local files are invisible to [LESSONS_WARNING] auto-surfacing on future turns and across sessions). PAST SESSIONS LIVE HERE: use action='recall' FIRST when the user references "last time", "previous", "yesterday", or is continuing prior work \u2014 full-text transcripts are indexed across every prior session. context() may surface [GROUNDING]; use action='ground' with user_message for a one-shot bundle (recall + docs + decisions + lessons + skills) outside context(). Actions: capture (save decision/insight), retro_capture (after-the-fact decision/note/snapshot capture from prior work with source provenance \u2014 title plus content and/or query/transcript_ids), capture_lesson (mistakes/corrections \u2014 title+trigger+impact+prevention), get_lessons (retrieve lessons), update_lesson / delete_lesson (maintain a saved lesson by lesson_id \u2014 UUID or lookup text), recall (retrieve past conversation context via ranked fusion of transcripts/snapshots/docs/decisions), ground (one-shot prior-work bundle), remember (quick save), user_context (get preferences), summary (workspace summary), compress (compress chat), delta (changes since timestamp), smart_search (searches MEMORY/conversation history only, not code), decision_trace (trace decision provenance), restore_context (restore state after compaction). Plan actions: capture_plan, get_plan, update_plan, list_plans. Suggested rules actions: list_suggested_rules, suggested_rule_action, suggested_rules_stats. Team actions: team_decisions, team_lessons, team_plans. Team/personal mode: set_account_mode (team|personal|auto).`,
33763
+ description: `Session and memory management \u2014 NOT for codebase/file search (use the 'search' tool for that). LESSONS LIVE HERE: when a mistake or correction happens, call action='capture_lesson' (NEVER write lessons to ~/.claude/.../memory/, .cursorrules, or other local markdown \u2014 local files are invisible to [LESSONS_WARNING] auto-surfacing on future turns and across sessions). PAST SESSIONS LIVE HERE: use action='recall' FIRST when the user references "last time", "previous", "yesterday", or is continuing prior work \u2014 full-text transcripts are indexed across every prior session. context() may surface [GROUNDING]; use action='ground' with user_message for a one-shot bundle (recall + docs + decisions + lessons + skills) outside context(). Actions: capture (save decision/insight), retro_capture (after-the-fact decision/note/snapshot capture from prior work with source provenance \u2014 title plus content and/or query/transcript_ids), capture_lesson (mistakes/corrections \u2014 title+trigger+impact+prevention), get_lessons (retrieve lessons), update_lesson / delete_lesson (maintain a saved lesson by lesson_id \u2014 UUID or lookup text), recall (retrieve past conversation context via ranked fusion of transcripts/snapshots/docs/decisions), ground (one-shot prior-work bundle), remember (quick save), user_context (get preferences), summary (workspace summary), compress (compress chat), delta (changes since timestamp), smart_search (searches MEMORY/conversation history only, not code), decision_trace (trace decision provenance), list_recaps (timestamped Daily Recap history), trigger_recap (manual asynchronous Daily Recap generation), restore_context (restore state after compaction). Daily Recaps run around 23:00 in the user's timezone, not at MCP session boundaries. Plan actions: capture_plan, get_plan, update_plan, list_plans. Suggested rules actions: list_suggested_rules, suggested_rule_action, suggested_rules_stats. Team actions: team_decisions, team_lessons, team_plans. Team/personal mode: set_account_mode (team|personal|auto).`,
33255
33764
  inputSchema: external_exports.object({
33256
33765
  action: external_exports.enum([
33257
33766
  "capture",
@@ -33270,6 +33779,8 @@ ${listing}`
33270
33779
  "delta",
33271
33780
  "smart_search",
33272
33781
  "decision_trace",
33782
+ "list_recaps",
33783
+ "trigger_recap",
33273
33784
  // Plan actions
33274
33785
  "capture_plan",
33275
33786
  "get_plan",
@@ -34043,6 +34554,32 @@ ${listing}`
34043
34554
  };
34044
34555
  }
34045
34556
  }
34557
+ case "list_recaps": {
34558
+ if (!workspaceId) {
34559
+ return errorResult("list_recaps requires workspace_id. Call init first or pass workspace_id explicitly.");
34560
+ }
34561
+ const result = await client.listDailyRecaps({
34562
+ workspace_id: workspaceId,
34563
+ limit: input.limit
34564
+ });
34565
+ return {
34566
+ content: [{ type: "text", text: formatDailyRecaps(result) }],
34567
+ structuredContent: toStructured(result)
34568
+ };
34569
+ }
34570
+ case "trigger_recap": {
34571
+ if (!workspaceId) {
34572
+ return errorResult("trigger_recap requires workspace_id. Call init first or pass workspace_id explicitly.");
34573
+ }
34574
+ const result = await client.triggerDailyRecap({ workspace_id: workspaceId });
34575
+ return {
34576
+ content: [{
34577
+ type: "text",
34578
+ text: `Daily Recap generation queued. Automatic recaps also run around 23:00 in the user's configured timezone; generation is not tied to MCP session boundaries. Use session(action="list_recaps") to verify the completed recap and its generated_at timestamp.`
34579
+ }],
34580
+ structuredContent: toStructured(result)
34581
+ };
34582
+ }
34046
34583
  // Plan actions
34047
34584
  case "capture_plan": {
34048
34585
  if (!input.title) {
@@ -34503,15 +35040,19 @@ ${formatContent(result)}` : formatContent(result);
34503
35040
  registerTool(
34504
35041
  "entity",
34505
35042
  {
34506
- title: "Structured Entity Operations",
34507
- description: "Unified CRUD across taxonomy expansion entities. Kinds: ticket, handoff, backlog_view, incident, release, experiment, goal, key_result, sprint, review, risk. Actions: list, get, create, update, delete. Body is free-form JSON forwarded to the API; workspace_id/project_id default to active scope when omitted.",
35043
+ title: "Structured Handoffs, Tickets, and Workflow Entities",
35044
+ description: ENTITY_TOOL_DESCRIPTION,
34508
35045
  inputSchema: external_exports.object({
34509
- kind: external_exports.enum(VALID_ENTITY_KINDS).describe("Entity kind"),
35046
+ kind: external_exports.enum(VALID_ENTITY_KINDS).describe(
35047
+ "Entity kind. handoff is the canonical durable agent/session handoff; capsule is an optional additional portable artifact."
35048
+ ),
34510
35049
  action: external_exports.enum(["list", "get", "create", "update", "delete"]).describe("Action to perform"),
34511
35050
  id: external_exports.string().uuid().optional().describe("Entity ID (required for get / update / delete)"),
34512
35051
  workspace_id: external_exports.string().uuid().optional(),
34513
35052
  project_id: external_exports.string().uuid().optional(),
34514
- body: external_exports.record(external_exports.any()).optional().describe("JSON body for create / update"),
35053
+ body: external_exports.record(external_exports.any()).optional().describe(
35054
+ "JSON body for create/update. For handoff create, include title, summary, scope, and next_steps; omit to_user_id when the recipient is unknown."
35055
+ ),
34515
35056
  query: external_exports.record(external_exports.any()).optional().describe("Filter params for list")
34516
35057
  })
34517
35058
  },
@@ -34584,7 +35125,7 @@ ${formatContent(result)}` }],
34584
35125
  "capsule",
34585
35126
  {
34586
35127
  title: "ContextCapsule",
34587
- description: "ContextCapsule: portable, shareable, hydrate-on-demand snapshots of project context. Use capsule when the user pastes a /c/<token> link or capsule token, asks for a handoff/share/team/external-agent link, wants to bootstrap a fresh agent with project state, asks for a paste-ready handoff prompt (bootstrap prompt / prompt for another LLM), wants share-token graphs, or wants to list/audit capsules. Do not use capsule for normal turn-by-turn retrieval; use context instead. Team share links are authenticated and reusable by default; external_agent/public_link/support shares are token-gated and single-use by default.",
35128
+ description: CAPSULE_TOOL_DESCRIPTION,
34588
35129
  inputSchema: external_exports.object({
34589
35130
  action: external_exports.enum(CAPSULE_ACTIONS).describe("Action to perform"),
34590
35131
  capsule_id: external_exports.string().optional().describe("ContextCapsule ID"),
@@ -35807,8 +36348,7 @@ Pass the node_id UUID to delete one, or pass delete_all=true to remove ALL exact
35807
36348
  return errorResult("update_task requires: task_id");
35808
36349
  }
35809
36350
  const requestedTaskStatus = input.task_status ?? input.status;
35810
- const result = await client.updateTask({
35811
- task_id: input.task_id,
36351
+ const taskUpdate = {
35812
36352
  title: input.title,
35813
36353
  content: input.content,
35814
36354
  description: input.description,
@@ -35820,6 +36360,31 @@ Pass the node_id UUID to delete one, or pass delete_all=true to remove ALL exact
35820
36360
  code_refs: input.code_refs,
35821
36361
  tags: input.tags,
35822
36362
  blocked_reason: input.blocked_reason
36363
+ };
36364
+ if (taskUpdateIsStatusOnly(taskUpdate)) {
36365
+ const current = await client.getTask({ task_id: input.task_id });
36366
+ const currentStatus = extractTaskStatus(current);
36367
+ if (currentStatus?.toLowerCase() === requestedTaskStatus?.toLowerCase()) {
36368
+ const result2 = {
36369
+ ...current,
36370
+ operation_status: {
36371
+ operation: "update_task",
36372
+ state: "completed",
36373
+ changed: false,
36374
+ reason: "already_in_target_status",
36375
+ status: requestedTaskStatus
36376
+ },
36377
+ hint: `Task is already ${requestedTaskStatus}. No changes were needed.`
36378
+ };
36379
+ return {
36380
+ content: [{ type: "text", text: formatContent(result2) }],
36381
+ structuredContent: result2
36382
+ };
36383
+ }
36384
+ }
36385
+ const result = await client.updateTask({
36386
+ task_id: input.task_id,
36387
+ ...taskUpdate
35823
36388
  });
35824
36389
  let taskUpdateHint = "Task updated.";
35825
36390
  if (requestedTaskStatus === "completed") {
@@ -36842,6 +37407,41 @@ ${formatContent({
36842
37407
  }
36843
37408
  try {
36844
37409
  const result = await client.indexProject(projectId);
37410
+ if (apiIndexResultIsSkipped(result)) {
37411
+ if (!folderPath) {
37412
+ return remoteIndexStateResult("index", projectId);
37413
+ }
37414
+ const validPath = await validateReadableDirectory(folderPath);
37415
+ if (!validPath.ok) {
37416
+ if (isInvariantIgnoredPath(folderPath) || isBroadProjectPath(folderPath) && process.env.CONTEXTSTREAM_ALLOW_BROAD_INGEST !== "1") {
37417
+ return errorResult(validPath.error);
37418
+ }
37419
+ return remoteIndexStateResult("index", projectId, folderPath);
37420
+ }
37421
+ const ingestOptions = {
37422
+ ...input.write_to_disk !== void 0 && { write_to_disk: input.write_to_disk },
37423
+ ...input.overwrite !== void 0 && { overwrite: input.overwrite },
37424
+ ...input.force !== void 0 && { force: input.force }
37425
+ };
37426
+ startBackgroundIngest(projectId, validPath.resolvedPath, ingestOptions);
37427
+ const response = {
37428
+ status: "started",
37429
+ message: "Index endpoint skipped this local project type; started local ingest in the background",
37430
+ project_id: projectId,
37431
+ path: validPath.resolvedPath,
37432
+ fallback_action: "ingest_local",
37433
+ note: "Use 'project' with action 'index_status' to monitor progress."
37434
+ };
37435
+ return {
37436
+ content: [
37437
+ {
37438
+ type: "text",
37439
+ text: `Index endpoint skipped this local project type. Started local ingest in the background for directory: ${validPath.resolvedPath}. Use 'project' with action 'index_status' to monitor progress.`
37440
+ }
37441
+ ],
37442
+ structuredContent: response
37443
+ };
37444
+ }
36845
37445
  return {
36846
37446
  content: [{ type: "text", text: formatContent(result) }]
36847
37447
  };
@@ -36850,15 +37450,14 @@ ${formatContent({
36850
37450
  throw error;
36851
37451
  }
36852
37452
  if (!folderPath) {
36853
- return errorResult(
36854
- 'Index endpoint is unavailable for this local project type and no folder context is set. Run project(action="ingest_local", path="<folder>").'
36855
- );
37453
+ return remoteIndexStateResult("index", projectId);
36856
37454
  }
36857
37455
  const validPath = await validateReadableDirectory(folderPath);
36858
37456
  if (!validPath.ok) {
36859
- return errorResult(
36860
- `Index endpoint is unavailable for this project type and folder context path is invalid: ${folderPath}. ${validPath.error}`
36861
- );
37457
+ if (isInvariantIgnoredPath(folderPath) || isBroadProjectPath(folderPath) && process.env.CONTEXTSTREAM_ALLOW_BROAD_INGEST !== "1") {
37458
+ return errorResult(validPath.error);
37459
+ }
37460
+ return remoteIndexStateResult("index", projectId, folderPath);
36862
37461
  }
36863
37462
  const ingestOptions = {
36864
37463
  ...input.write_to_disk !== void 0 && { write_to_disk: input.write_to_disk },
@@ -37108,18 +37707,26 @@ ${formatContent(response)}`
37108
37707
  }
37109
37708
  const validPath = await validateReadableDirectory(ingestPath);
37110
37709
  if (!validPath.ok) {
37111
- try {
37112
- const pathExists = await fs7.promises.stat(ingestPath).then(() => true).catch(() => false);
37113
- if (!pathExists) {
37114
- const apiResult = await client.ingestFromPath(projectId, ingestPath, {
37115
- force: input.force
37116
- });
37117
- return {
37118
- content: [{ type: "text", text: `Delegated ingest to API for path: ${ingestPath}
37119
- ${formatContent(apiResult)}` }]
37120
- };
37710
+ const pathExists = await fs9.promises.stat(ingestPath).then(() => true).catch(() => false);
37711
+ if (!pathExists) {
37712
+ if (client.canDelegatePathIngest()) {
37713
+ try {
37714
+ const apiResult = await client.ingestFromPath(projectId, ingestPath, {
37715
+ force: input.force
37716
+ });
37717
+ return {
37718
+ content: [
37719
+ {
37720
+ type: "text",
37721
+ text: `Delegated ingest to API for path: ${ingestPath}
37722
+ ${formatContent(apiResult)}`
37723
+ }
37724
+ ]
37725
+ };
37726
+ } catch {
37727
+ }
37121
37728
  }
37122
- } catch {
37729
+ return remoteIndexStateResult("ingest_local", projectId, ingestPath);
37123
37730
  }
37124
37731
  return errorResult(validPath.error);
37125
37732
  }
@@ -37243,7 +37850,7 @@ Uncommitted changes: ${changes.diff_stat.summary}
37243
37850
  };
37244
37851
  }
37245
37852
  case "forget_local": {
37246
- const forgetPath = path8.resolve(input.folder_path || folderPath || process.cwd());
37853
+ const forgetPath = path11.resolve(input.folder_path || folderPath || process.cwd());
37247
37854
  const outcome = forgetLocalConfig(forgetPath);
37248
37855
  const lines = [
37249
37856
  outcome.local_config_removed ? `\u2713 Removed local scope binding for ${forgetPath}.` : `No local scope binding found for ${forgetPath} (nothing to remove).`,
@@ -38717,9 +39324,10 @@ Each domain tool has an 'action' parameter for specific operations.` : "";
38717
39324
  };
38718
39325
  }
38719
39326
  case "version": {
38720
- const result = { name: "contextstream-mcp", version: VERSION };
39327
+ const result = await loadMcpVersionInfo(client);
38721
39328
  return {
38722
- content: [{ type: "text", text: formatContent(result) }]
39329
+ content: [{ type: "text", text: formatContent(result) }],
39330
+ structuredContent: result
38723
39331
  };
38724
39332
  }
38725
39333
  case "editor_rules": {
@@ -41093,6 +41701,8 @@ Environment variables:
41093
41701
  CONTEXTSTREAM_SEARCH_MAX_CHARS Max chars per search result content (default: 400)
41094
41702
  CONTEXTSTREAM_CONSOLIDATED Consolidated domain tools: true|false (default: true in v0.4.x, ~75% token reduction)
41095
41703
  CONTEXTSTREAM_CONTEXT_PACK Enable Context Pack in context_smart: true|false (default: true)
41704
+ CONTEXTSTREAM_ALLOW_BROAD_INGEST Allow filesystem-root/home indexing only when explicitly set to 1
41705
+ CONTEXTSTREAM_ALLOW_API_PATH_INGEST Allow a non-loopback API to resolve local paths only when explicitly set to 1
41096
41706
  CONTEXTSTREAM_PRO_TOOLS Optional comma-separated PRO tool names (default: AI tools)
41097
41707
  CONTEXTSTREAM_UPGRADE_URL Optional upgrade URL shown for PRO tools on Free plan
41098
41708
  CONTEXTSTREAM_ENABLE_PROMPTS Enable MCP prompts list (default: true)