@contextstream/mcp-server 0.4.34 → 0.4.35

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.
Files changed (2) hide show
  1. package/dist/index.js +111 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4297,7 +4297,7 @@ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504])
4297
4297
  var MAX_RETRIES = 3;
4298
4298
  var BASE_DELAY = 1e3;
4299
4299
  async function sleep(ms) {
4300
- return new Promise((resolve3) => setTimeout(resolve3, ms));
4300
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
4301
4301
  }
4302
4302
  async function request(config, path8, options = {}) {
4303
4303
  const { apiUrl, userAgent } = config;
@@ -8925,13 +8925,21 @@ var PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
8925
8925
  """
8926
8926
  ContextStream PreToolUse Hook for Claude Code
8927
8927
  Blocks Grep/Glob/Search/Task(Explore)/EnterPlanMode and redirects to ContextStream.
8928
+
8929
+ Only blocks if the current project is indexed in ContextStream.
8930
+ If not indexed, allows local tools through with a suggestion to index.
8928
8931
  """
8929
8932
 
8930
8933
  import json
8931
8934
  import sys
8932
8935
  import os
8936
+ from pathlib import Path
8937
+ from datetime import datetime, timedelta
8933
8938
 
8934
8939
  ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
8940
+ INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
8941
+ # Consider index stale after 7 days
8942
+ STALE_THRESHOLD_DAYS = 7
8935
8943
 
8936
8944
  DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
8937
8945
 
@@ -8953,6 +8961,44 @@ def is_discovery_grep(file_path):
8953
8961
  return True
8954
8962
  return False
8955
8963
 
8964
+ def is_project_indexed(cwd: str) -> tuple[bool, bool]:
8965
+ """
8966
+ Check if the current directory is in an indexed project.
8967
+ Returns (is_indexed, is_stale).
8968
+ """
8969
+ if not INDEX_STATUS_FILE.exists():
8970
+ return False, False
8971
+
8972
+ try:
8973
+ with open(INDEX_STATUS_FILE, "r") as f:
8974
+ data = json.load(f)
8975
+ except:
8976
+ return False, False
8977
+
8978
+ projects = data.get("projects", {})
8979
+ cwd_path = Path(cwd).resolve()
8980
+
8981
+ # Check if cwd is within any indexed project
8982
+ for project_path, info in projects.items():
8983
+ try:
8984
+ indexed_path = Path(project_path).resolve()
8985
+ # Check if cwd is the project or a subdirectory
8986
+ if cwd_path == indexed_path or indexed_path in cwd_path.parents:
8987
+ # Check if stale
8988
+ indexed_at = info.get("indexed_at")
8989
+ if indexed_at:
8990
+ try:
8991
+ indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
8992
+ if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
8993
+ return True, True # Indexed but stale
8994
+ except:
8995
+ pass
8996
+ return True, False # Indexed and fresh
8997
+ except:
8998
+ continue
8999
+
9000
+ return False, False
9001
+
8956
9002
  def main():
8957
9003
  if not ENABLED:
8958
9004
  sys.exit(0)
@@ -8964,6 +9010,20 @@ def main():
8964
9010
 
8965
9011
  tool = data.get("tool_name", "")
8966
9012
  inp = data.get("tool_input", {})
9013
+ cwd = data.get("cwd", os.getcwd())
9014
+
9015
+ # Check if project is indexed
9016
+ is_indexed, is_stale = is_project_indexed(cwd)
9017
+
9018
+ if not is_indexed:
9019
+ # Project not indexed - allow local tools but suggest indexing
9020
+ # Don't block, just exit successfully
9021
+ sys.exit(0)
9022
+
9023
+ if is_stale:
9024
+ # Index is stale - allow with warning (printed but not blocking)
9025
+ # Still allow the tool but remind about re-indexing
9026
+ pass # Continue to blocking logic but could add warning
8967
9027
 
8968
9028
  if tool == "Glob":
8969
9029
  pattern = inp.get("pattern", "")
@@ -8971,18 +9031,16 @@ def main():
8971
9031
  print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of Glob.", file=sys.stderr)
8972
9032
  sys.exit(2)
8973
9033
 
8974
- elif tool == "Grep":
9034
+ elif tool == "Grep" or tool == "Search":
9035
+ # Block ALL Grep/Search operations - use ContextStream search or Read for specific files
8975
9036
  pattern = inp.get("pattern", "")
8976
9037
  path = inp.get("path", "")
8977
- if is_discovery_grep(path):
8978
- print(f"STOP: Use mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") instead of Grep.", file=sys.stderr)
8979
- sys.exit(2)
8980
-
8981
- elif tool == "Search":
8982
- # Block the Search tool which is Claude Code's default search
8983
- query = inp.get("pattern", "") or inp.get("query", "")
8984
- if query:
8985
- print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{query}\\") instead of Search.", file=sys.stderr)
9038
+ if pattern:
9039
+ if path and not is_discovery_grep(path):
9040
+ # Specific file - suggest Read instead
9041
+ print(f"STOP: Use Read(\\"{path}\\") to view file content, or mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") for codebase search.", file=sys.stderr)
9042
+ else:
9043
+ print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}.", file=sys.stderr)
8986
9044
  sys.exit(2)
8987
9045
 
8988
9046
  elif tool == "Task":
@@ -9148,6 +9206,34 @@ async function installClaudeCodeHooks(options) {
9148
9206
  }
9149
9207
  return result;
9150
9208
  }
9209
+ function getIndexStatusPath() {
9210
+ return path4.join(homedir2(), ".contextstream", "indexed-projects.json");
9211
+ }
9212
+ async function readIndexStatus() {
9213
+ const statusPath = getIndexStatusPath();
9214
+ try {
9215
+ const content = await fs3.readFile(statusPath, "utf-8");
9216
+ return JSON.parse(content);
9217
+ } catch {
9218
+ return { version: 1, projects: {} };
9219
+ }
9220
+ }
9221
+ async function writeIndexStatus(status) {
9222
+ const statusPath = getIndexStatusPath();
9223
+ const dir = path4.dirname(statusPath);
9224
+ await fs3.mkdir(dir, { recursive: true });
9225
+ await fs3.writeFile(statusPath, JSON.stringify(status, null, 2));
9226
+ }
9227
+ async function markProjectIndexed(projectPath, options) {
9228
+ const status = await readIndexStatus();
9229
+ const resolvedPath = path4.resolve(projectPath);
9230
+ status.projects[resolvedPath] = {
9231
+ indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
9232
+ project_id: options?.project_id,
9233
+ project_name: options?.project_name
9234
+ };
9235
+ await writeIndexStatus(status);
9236
+ }
9151
9237
 
9152
9238
  // src/tools.ts
9153
9239
  var LESSON_DEDUP_WINDOW_MS = 2 * 60 * 1e3;
@@ -11262,6 +11348,12 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
11262
11348
  console.error(
11263
11349
  `[ContextStream] Completed background ingestion: ${totalIndexed} files in ${batchCount} batches`
11264
11350
  );
11351
+ try {
11352
+ await markProjectIndexed(resolvedPath, { project_id: projectId });
11353
+ console.error(`[ContextStream] Marked project as indexed: ${resolvedPath}`);
11354
+ } catch (markError) {
11355
+ console.error(`[ContextStream] Failed to mark project as indexed:`, markError);
11356
+ }
11265
11357
  } catch (error) {
11266
11358
  console.error(`[ContextStream] Ingestion failed:`, error);
11267
11359
  }
@@ -13007,6 +13099,12 @@ ${benefitsList}` : "",
13007
13099
  noticeLines.push(
13008
13100
  `[INGEST_STATUS] Background indexing started. Codebase will be searchable shortly.`
13009
13101
  );
13102
+ } else if (folderPathForRules && !ingestRec?.recommended) {
13103
+ const projectId = typeof result.project_id === "string" ? result.project_id : void 0;
13104
+ const projectName = typeof result.project_name === "string" ? result.project_name : void 0;
13105
+ markProjectIndexed(folderPathForRules, { project_id: projectId, project_name: projectName }).catch(
13106
+ (err) => console.error("[ContextStream] Failed to mark project as indexed:", err)
13107
+ );
13010
13108
  }
13011
13109
  if (noticeLines.length > 0) {
13012
13110
  text = `${text}
@@ -19569,10 +19667,10 @@ Code: ${device.user_code}`);
19569
19667
  if (poll && poll.status === "pending") {
19570
19668
  const intervalSeconds = typeof poll.interval === "number" ? poll.interval : 5;
19571
19669
  const waitMs = Math.max(1, intervalSeconds) * 1e3;
19572
- await new Promise((resolve3) => setTimeout(resolve3, waitMs));
19670
+ await new Promise((resolve4) => setTimeout(resolve4, waitMs));
19573
19671
  continue;
19574
19672
  }
19575
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
19673
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
19576
19674
  }
19577
19675
  if (!accessToken) {
19578
19676
  throw new Error(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@contextstream/mcp-server",
3
3
  "mcpName": "io.github.contextstreamio/mcp-server",
4
- "version": "0.4.34",
4
+ "version": "0.4.35",
5
5
  "description": "ContextStream MCP server - v0.4.x with consolidated domain tools (~11 tools, ~75% token reduction). Code context, memory, search, and AI tools.",
6
6
  "type": "module",
7
7
  "license": "MIT",