@contextstream/mcp-server 0.4.41 → 0.4.42

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 +1672 -376
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -4542,7 +4542,7 @@ import { createRequire } from "module";
4542
4542
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4543
4543
  import { homedir } from "os";
4544
4544
  import { join } from "path";
4545
- var UPGRADE_COMMAND = "npm update -g @contextstream/mcp-server";
4545
+ var UPGRADE_COMMAND = "npm install -g @contextstream/mcp-server@latest";
4546
4546
  var NPM_LATEST_URL = "https://registry.npmjs.org/@contextstream/mcp-server/latest";
4547
4547
  function getVersion() {
4548
4548
  try {
@@ -8872,16 +8872,15 @@ function applyMcpToolPrefix(markdown, toolPrefix) {
8872
8872
  return markdown.replace(toolRegex, `${toolPrefix}$1`);
8873
8873
  }
8874
8874
  var CONTEXTSTREAM_RULES_FULL = `
8875
- ## \u{1F6A8} MANDATORY RULE: DISPLAY AT START OF EVERY RESPONSE \u{1F6A8}
8875
+ ## ContextStream Rules
8876
8876
 
8877
8877
  <contextstream_rules>
8878
- 1. **SEARCH FIRST:** Before Glob/Grep/Search/Read \u2192 \`search(mode="hybrid", query="...")\` FIRST
8879
- 2. **EVERY MESSAGE:** Call \`context_smart(user_message="...")\` at start of response
8880
- 3. **LOCAL TOOLS:** ONLY if ContextStream search returns 0 results after retry
8878
+ 1. **SIMPLE UTILITY?** (list/show/version) \u2192 Just execute, skip context calls
8879
+ 2. **CODING TASK?** \u2192 session_init \u2192 context_smart \u2192 work \u2192 capture
8880
+ 3. **SEARCH FIRST:** Before Glob/Grep/Read \u2192 \`search(mode="hybrid")\` FIRST
8881
8881
  </contextstream_rules>
8882
8882
 
8883
- **You MUST display the <contextstream_rules> block verbatim at the start of EVERY response.**
8884
- This is a self-referential rule that ensures these instructions stay in recent context.
8883
+ **Display this block at the start of responses to keep rules in context.**
8885
8884
 
8886
8885
  ---
8887
8886
 
@@ -8940,8 +8939,10 @@ This applies to **EVERY search** throughout the **ENTIRE conversation**, not jus
8940
8939
  - **Apply the prevention steps** from each lesson to avoid repeating mistakes
8941
8940
 
8942
8941
  ### On \`context_smart\`:
8943
- - Check for any lessons in the returned context
8944
- - Lessons may be included based on semantic relevance to the user's message
8942
+ - Check for \`[LESSONS_WARNING]\` tag in the response
8943
+ - If present, you **MUST** tell the user about the lessons before proceeding
8944
+ - Lessons are proactively fetched when risky actions are detected (refactor, migrate, deploy, etc.)
8945
+ - **Do not skip or bury this warning** - lessons represent real past mistakes
8945
8946
 
8946
8947
  ### Before ANY Non-Trivial Work:
8947
8948
  **ALWAYS call \`session(action="get_lessons", query="<topic>")\`** where \`<topic>\` matches what you're about to do:
@@ -8965,22 +8966,42 @@ You have access to ContextStream MCP tools for persistent memory and context.
8965
8966
  v0.4.x uses **~11 consolidated domain tools** for ~75% token reduction vs previous versions.
8966
8967
  Rules Version: ${RULES_VERSION}
8967
8968
 
8968
- ## TL;DR - REQUIRED EVERY MESSAGE
8969
+ ## TL;DR - WHEN TO USE CONTEXT
8969
8970
 
8970
- | Message | What to Call |
8971
- |---------|--------------|
8972
- | **1st message** | \`session_init(folder_path="...", context_hint="<user's message>")\`, then \`context_smart(...)\` |
8973
- | **\u26A0\uFE0F After session_init** | **CHECK \`lessons\` field** - if present, read and apply them BEFORE any work |
8974
- | **2nd+ messages** | \`context_smart(user_message="<user's message>", format="minified", max_tokens=400)\` |
8975
- | **\u{1F50D} ANY code search** | \`search(mode="hybrid", query="...")\` \u2014 ALWAYS before Glob/Grep/Search/Read |
8976
- | **\u26A0\uFE0F Before ANY risky work** | \`session(action="get_lessons", query="<topic>")\` \u2014 **MANDATORY, not optional** |
8977
- | **After completing task** | \`session(action="capture", event_type="decision", ...)\` - MUST capture |
8978
- | **User frustration/correction** | \`session(action="capture_lesson", ...)\` - MUST capture lessons |
8979
- | **Command/tool error + fix** | \`session(action="capture_lesson", ...)\` - MUST capture lessons |
8971
+ | Request Type | What to Do |
8972
+ |--------------|------------|
8973
+ | **\u{1F680} Simple utility** (list workspaces, show version) | **Just execute directly** - skip session_init, context_smart, capture |
8974
+ | **\u{1F4BB} Coding task** (edit, create, refactor) | Full context: session_init \u2192 context_smart \u2192 work \u2192 capture |
8975
+ | **\u{1F50D} Code search/discovery** | session_init \u2192 context_smart \u2192 search() |
8976
+ | **\u26A0\uFE0F Risky work** (deploy, migrate, refactor) | Check lessons first: \`session(action="get_lessons")\` |
8977
+ | **User frustration/correction** | Capture lesson: \`session(action="capture_lesson", ...)\` |
8980
8978
 
8981
- **NO EXCEPTIONS.** Do not skip even if you think you have enough context.
8979
+ ### Simple Utility Operations - FAST PATH
8982
8980
 
8983
- **First message rule:** After \`session_init\`:
8981
+ **For simple queries, just execute and respond:**
8982
+ - "list workspaces" \u2192 \`workspace(action="list")\` \u2192 done
8983
+ - "list projects" \u2192 \`project(action="list")\` \u2192 done
8984
+ - "show version" \u2192 \`help(action="version")\` \u2192 done
8985
+ - "what reminders do I have" \u2192 \`reminder(action="list")\` \u2192 done
8986
+
8987
+ **No session_init. No context_smart. No capture.** These add noise, not value.
8988
+
8989
+ ### Coding Tasks - FULL CONTEXT
8990
+
8991
+ | Step | What to Call |
8992
+ |------|--------------|
8993
+ | **1st message** | \`session_init(folder_path="...", context_hint="<msg>")\`, then \`context_smart(...)\` |
8994
+ | **2nd+ messages** | \`context_smart(user_message="<msg>", format="minified", max_tokens=400)\` |
8995
+ | **Code search** | \`search(mode="hybrid", query="...")\` \u2014 BEFORE Glob/Grep/Read |
8996
+ | **After significant work** | \`session(action="capture", event_type="decision", ...)\` |
8997
+ | **User correction** | \`session(action="capture_lesson", ...)\` |
8998
+
8999
+ **How to detect simple utility operations:**
9000
+ - Single-word commands: "list", "show", "version", "help"
9001
+ - Data retrieval with no context dependency: "list my workspaces", "what projects do I have"
9002
+ - Status checks: "am I authenticated?", "what's the server version?"
9003
+
9004
+ **First message rule (for coding tasks):** After \`session_init\`:
8984
9005
  1. Check for \`lessons\` in response - if present, READ and SUMMARIZE them to user
8985
9006
  2. Then call \`context_smart\` before any other tool or response
8986
9007
 
@@ -8994,9 +9015,9 @@ Rules Version: ${RULES_VERSION}
8994
9015
 
8995
9016
  v0.4.x consolidates ~58 individual tools into ~11 domain tools with action/mode dispatch:
8996
9017
 
8997
- ### Standalone Tools (Always Call)
8998
- - **\`session_init\`** - Initialize session with workspace detection + context
8999
- - **\`context_smart\`** - Semantic search for relevant context (CALL EVERY MESSAGE, including immediately after \`session_init\`)
9018
+ ### Standalone Tools
9019
+ - **\`session_init\`** - Initialize session with workspace detection + context (skip for simple utility operations)
9020
+ - **\`context_smart\`** - Semantic search for relevant context (skip for simple utility operations)
9000
9021
 
9001
9022
  ### Domain Tools (Use action/mode parameter)
9002
9023
 
@@ -9043,10 +9064,21 @@ If context still feels missing, use \`session(action="recall", query="...")\` fo
9043
9064
 
9044
9065
  ---
9045
9066
 
9046
- ### Rules Update Notices
9067
+ ### Rules, Version & Lessons Notices
9068
+
9069
+ **[RULES_NOTICE]** - Update rules via \`generate_rules()\` (or rerun setup).
9047
9070
 
9048
- - If you see **[RULES_NOTICE]**, update rules via \`generate_rules()\` (or rerun setup).
9049
- - If you see **[VERSION_NOTICE]**, tell the user to update MCP using the provided command.
9071
+ **[VERSION_NOTICE]** - You **MUST IMMEDIATELY** tell the user:
9072
+ 1. A new MCP server version is available
9073
+ 2. The exact update command to run
9074
+ 3. That they need to restart their AI tool after updating
9075
+ **Do not skip this** - users often miss stderr warnings.
9076
+
9077
+ **[LESSONS_WARNING]** - You **MUST** before proceeding:
9078
+ 1. Read all lessons listed
9079
+ 2. Tell the user about relevant lessons
9080
+ 3. Explain how you will avoid each past mistake
9081
+ **This is critical** - ignoring lessons leads to repeated failures.
9050
9082
 
9051
9083
  ---
9052
9084
 
@@ -9177,16 +9209,24 @@ If ContextStream returns results, stop and use them. NEVER use local Search/Expl
9177
9209
 
9178
9210
  ---
9179
9211
 
9180
- ### When to Capture (MANDATORY)
9212
+ ### When to Capture
9181
9213
 
9182
9214
  | When | Call | Example |
9183
9215
  |------|------|---------|
9184
9216
  | User makes decision | \`session(action="capture", event_type="decision", ...)\` | "Let's use PostgreSQL" |
9185
9217
  | User states preference | \`session(action="capture", event_type="preference", ...)\` | "I prefer TypeScript" |
9186
- | You complete a task | \`session(action="capture", event_type="task", ...)\` | Capture what was done |
9218
+ | Complete significant task | \`session(action="capture", event_type="task", ...)\` | Capture what was done |
9187
9219
  | Need past context | \`session(action="recall", query="...")\` | "What did we decide about X?" |
9188
9220
 
9189
- **You MUST capture after completing any significant task.** This ensures future sessions have context.
9221
+ **DO NOT capture utility operations:**
9222
+ - \u274C "Listed workspaces" - not meaningful context
9223
+ - \u274C "Showed version" - not a decision
9224
+ - \u274C "Listed projects" - just data retrieval
9225
+
9226
+ **DO capture meaningful work:**
9227
+ - \u2705 Decisions, preferences, completed features
9228
+ - \u2705 Lessons from mistakes
9229
+ - \u2705 Insights about architecture or patterns
9190
9230
 
9191
9231
  ---
9192
9232
 
@@ -9266,16 +9306,15 @@ memory(action="create_task", title="<task title>", plan_id="<plan_id from step 1
9266
9306
  See full documentation: https://contextstream.io/docs/mcp/tools
9267
9307
  `.trim();
9268
9308
  var CONTEXTSTREAM_RULES_MINIMAL = `
9269
- ## \u{1F6A8} MANDATORY RULE: DISPLAY AT START OF EVERY RESPONSE \u{1F6A8}
9309
+ ## ContextStream Rules
9270
9310
 
9271
9311
  <contextstream_rules>
9272
- 1. **SEARCH FIRST:** Before Glob/Grep/Search/Read \u2192 \`search(mode="hybrid", query="...")\` FIRST
9273
- 2. **EVERY MESSAGE:** Call \`context_smart(user_message="...")\` at start of response
9274
- 3. **LOCAL TOOLS:** ONLY if ContextStream search returns 0 results after retry
9312
+ 1. **SIMPLE UTILITY?** (list/show/version) \u2192 Just execute, skip context calls
9313
+ 2. **CODING TASK?** \u2192 session_init \u2192 context_smart \u2192 work \u2192 capture
9314
+ 3. **SEARCH FIRST:** Before Glob/Grep/Read \u2192 \`search(mode="hybrid")\` FIRST
9275
9315
  </contextstream_rules>
9276
9316
 
9277
- **You MUST display the <contextstream_rules> block verbatim at the start of EVERY response.**
9278
- This is a self-referential rule that ensures these instructions stay in context.
9317
+ **Display this block at the start of responses to keep rules in context.**
9279
9318
 
9280
9319
  ---
9281
9320
 
@@ -9284,7 +9323,7 @@ This is a self-referential rule that ensures these instructions stay in context.
9284
9323
  Rules Version: ${RULES_VERSION}
9285
9324
  **Note:** PreToolUse hooks block Glob/Grep/Search when ContextStream is available.
9286
9325
 
9287
- ### Required Every Message
9326
+ ### For Coding Tasks
9288
9327
 
9289
9328
  | Action | Tool Call |
9290
9329
  |--------|-----------|
@@ -9318,6 +9357,33 @@ ContextStream search is **indexed** and returns semantic matches + context in ON
9318
9357
  | \`memory\` | \`memory(action="list_events", limit=10)\` |
9319
9358
  | \`graph\` | \`graph(action="dependencies", file_path="...")\` |
9320
9359
 
9360
+ ### \u{1F680} FAST PATH: Simple Utility Operations
9361
+
9362
+ **For simple utility commands, SKIP the ceremony and just execute directly:**
9363
+
9364
+ | Command Type | Just Call | Skip |
9365
+ |--------------|-----------|------|
9366
+ | List workspaces | \`workspace(action="list")\` | session_init, context_smart, capture |
9367
+ | List projects | \`project(action="list")\` | session_init, context_smart, capture |
9368
+ | Show version | \`help(action="version")\` | session_init, context_smart, capture |
9369
+ | List reminders | \`reminder(action="list")\` | session_init, context_smart, capture |
9370
+ | Check auth | \`help(action="auth")\` | session_init, context_smart, capture |
9371
+
9372
+ **Detect simple operations by these patterns:**
9373
+ - "list ...", "show ...", "what are my ...", "get ..."
9374
+ - Single-action queries with no context dependency
9375
+ - User just wants data, not analysis or coding help
9376
+
9377
+ **DO NOT add overhead for utility operations:**
9378
+ - \u274C Don't call session_init just to list workspaces
9379
+ - \u274C Don't call context_smart for simple queries
9380
+ - \u274C Don't capture "listed workspaces" as an event (that's noise)
9381
+
9382
+ **Use full context ceremony ONLY for:**
9383
+ - Coding tasks (edit, create, refactor, debug)
9384
+ - Search/discovery (finding code, understanding architecture)
9385
+ - Tasks where past decisions or lessons matter
9386
+
9321
9387
  ### Lessons (Past Mistakes)
9322
9388
 
9323
9389
  - After \`session_init\`: Check for \`lessons\` field and apply before work
@@ -9326,8 +9392,27 @@ ContextStream search is **indexed** and returns semantic matches + context in ON
9326
9392
 
9327
9393
  ### Context Pressure & Compaction
9328
9394
 
9329
- - If \`context_smart\` returns high/critical \`context_pressure\`: call \`session_capture_smart(...)\` to save state
9330
- - After compaction (context lost): call \`session_init(..., is_post_compact=true)\` or \`session_restore_context()\`
9395
+ - If \`context_smart\` returns high/critical \`context_pressure\`: call \`session(action="capture", ...)\` to save state
9396
+ - PreCompact hooks automatically save snapshots before compaction (if installed)
9397
+
9398
+ ### Automatic Context Restoration
9399
+
9400
+ **Context restoration is now enabled by default.** Every \`session_init\` call automatically:
9401
+ - Restores context from recent snapshots (if available)
9402
+ - Returns \`restored_context\` field with snapshot data
9403
+ - Sets \`is_post_compact=true\` in response when restoration occurs
9404
+
9405
+ **No special handling needed after compaction** - just call \`session_init\` normally.
9406
+
9407
+ To disable automatic restoration:
9408
+ - Pass \`is_post_compact=false\` in the API call
9409
+ - Or set \`CONTEXTSTREAM_RESTORE_CONTEXT=false\` environment variable
9410
+
9411
+ ### Notices - MUST HANDLE IMMEDIATELY
9412
+
9413
+ - **[VERSION_NOTICE]**: Tell the user about the update and command to run
9414
+ - **[RULES_NOTICE]**: Run \`generate_rules(overwrite_existing=true)\` to update
9415
+ - **[LESSONS_WARNING]**: Read lessons, tell user about them, explain how you'll avoid past mistakes
9331
9416
 
9332
9417
  ### Plans & Tasks
9333
9418
 
@@ -9353,13 +9438,6 @@ var TEMPLATES = {
9353
9438
  description: "Codex CLI agent instructions",
9354
9439
  build: (rules) => `# Codex CLI Instructions
9355
9440
  ${rules}
9356
- `
9357
- },
9358
- windsurf: {
9359
- filename: ".windsurfrules",
9360
- description: "Windsurf AI rules",
9361
- build: (rules) => `# Windsurf Rules
9362
- ${rules}
9363
9441
  `
9364
9442
  },
9365
9443
  cursor: {
@@ -9805,7 +9883,7 @@ var PRECOMPACT_HOOK_SCRIPT = `#!/usr/bin/env python3
9805
9883
  ContextStream PreCompact Hook for Claude Code
9806
9884
 
9807
9885
  Runs BEFORE conversation context is compacted (manual via /compact or automatic).
9808
- Injects a reminder for the AI to save conversation state using session_capture_smart.
9886
+ Automatically saves conversation state to ContextStream by parsing the transcript.
9809
9887
 
9810
9888
  Input (via stdin):
9811
9889
  {
@@ -9821,7 +9899,7 @@ Output (to stdout):
9821
9899
  {
9822
9900
  "hookSpecificOutput": {
9823
9901
  "hookEventName": "PreCompact",
9824
- "additionalContext": "... instructions for AI ..."
9902
+ "additionalContext": "... status message ..."
9825
9903
  }
9826
9904
  }
9827
9905
  """
@@ -9829,8 +9907,149 @@ Output (to stdout):
9829
9907
  import json
9830
9908
  import sys
9831
9909
  import os
9910
+ import re
9911
+ import urllib.request
9912
+ import urllib.error
9832
9913
 
9833
9914
  ENABLED = os.environ.get("CONTEXTSTREAM_PRECOMPACT_ENABLED", "true").lower() == "true"
9915
+ AUTO_SAVE = os.environ.get("CONTEXTSTREAM_PRECOMPACT_AUTO_SAVE", "true").lower() == "true"
9916
+ API_URL = os.environ.get("CONTEXTSTREAM_API_URL", "https://api.contextstream.io")
9917
+ API_KEY = os.environ.get("CONTEXTSTREAM_API_KEY", "")
9918
+
9919
+ WORKSPACE_ID = None
9920
+
9921
+ def load_config_from_mcp_json(cwd):
9922
+ """Load API config from .mcp.json if env vars not set."""
9923
+ global API_URL, API_KEY, WORKSPACE_ID
9924
+
9925
+ # Try to find .mcp.json and .contextstream/config.json in cwd or parent directories
9926
+ search_dir = cwd
9927
+ for _ in range(5): # Search up to 5 levels
9928
+ # Load API config from .mcp.json
9929
+ if not API_KEY:
9930
+ mcp_path = os.path.join(search_dir, ".mcp.json")
9931
+ if os.path.exists(mcp_path):
9932
+ try:
9933
+ with open(mcp_path, 'r') as f:
9934
+ config = json.load(f)
9935
+ servers = config.get("mcpServers", {})
9936
+ cs_config = servers.get("contextstream", {})
9937
+ env = cs_config.get("env", {})
9938
+ if env.get("CONTEXTSTREAM_API_KEY"):
9939
+ API_KEY = env["CONTEXTSTREAM_API_KEY"]
9940
+ if env.get("CONTEXTSTREAM_API_URL"):
9941
+ API_URL = env["CONTEXTSTREAM_API_URL"]
9942
+ except:
9943
+ pass
9944
+
9945
+ # Load workspace_id from .contextstream/config.json
9946
+ if not WORKSPACE_ID:
9947
+ cs_config_path = os.path.join(search_dir, ".contextstream", "config.json")
9948
+ if os.path.exists(cs_config_path):
9949
+ try:
9950
+ with open(cs_config_path, 'r') as f:
9951
+ cs_config = json.load(f)
9952
+ if cs_config.get("workspace_id"):
9953
+ WORKSPACE_ID = cs_config["workspace_id"]
9954
+ except:
9955
+ pass
9956
+
9957
+ parent = os.path.dirname(search_dir)
9958
+ if parent == search_dir:
9959
+ break
9960
+ search_dir = parent
9961
+
9962
+ def parse_transcript(transcript_path):
9963
+ """Parse transcript to extract active files, decisions, and context."""
9964
+ active_files = set()
9965
+ recent_messages = []
9966
+ tool_calls = []
9967
+
9968
+ try:
9969
+ with open(transcript_path, 'r') as f:
9970
+ for line in f:
9971
+ try:
9972
+ entry = json.loads(line.strip())
9973
+ msg_type = entry.get("type", "")
9974
+
9975
+ # Extract files from tool calls
9976
+ if msg_type == "tool_use":
9977
+ tool_name = entry.get("name", "")
9978
+ tool_input = entry.get("input", {})
9979
+ tool_calls.append({"name": tool_name, "input": tool_input})
9980
+
9981
+ # Extract file paths from common tools
9982
+ if tool_name in ["Read", "Write", "Edit", "NotebookEdit"]:
9983
+ file_path = tool_input.get("file_path") or tool_input.get("notebook_path")
9984
+ if file_path:
9985
+ active_files.add(file_path)
9986
+ elif tool_name == "Glob":
9987
+ pattern = tool_input.get("pattern", "")
9988
+ if pattern:
9989
+ active_files.add(f"[glob:{pattern}]")
9990
+
9991
+ # Collect recent assistant messages for summary
9992
+ if msg_type == "assistant" and entry.get("content"):
9993
+ content = entry.get("content", "")
9994
+ if isinstance(content, str) and len(content) > 50:
9995
+ recent_messages.append(content[:500])
9996
+
9997
+ except json.JSONDecodeError:
9998
+ continue
9999
+ except Exception as e:
10000
+ pass
10001
+
10002
+ return {
10003
+ "active_files": list(active_files)[-20:], # Last 20 files
10004
+ "tool_call_count": len(tool_calls),
10005
+ "message_count": len(recent_messages),
10006
+ "last_tools": [t["name"] for t in tool_calls[-10:]], # Last 10 tool names
10007
+ }
10008
+
10009
+ def save_snapshot(session_id, transcript_data, trigger):
10010
+ """Save snapshot to ContextStream API."""
10011
+ if not API_KEY:
10012
+ return False, "No API key configured"
10013
+
10014
+ snapshot_content = {
10015
+ "session_id": session_id,
10016
+ "trigger": trigger,
10017
+ "captured_at": None, # API will set timestamp
10018
+ "active_files": transcript_data.get("active_files", []),
10019
+ "tool_call_count": transcript_data.get("tool_call_count", 0),
10020
+ "last_tools": transcript_data.get("last_tools", []),
10021
+ "auto_captured": True,
10022
+ }
10023
+
10024
+ payload = {
10025
+ "event_type": "session_snapshot",
10026
+ "title": f"Auto Pre-compaction Snapshot ({trigger})",
10027
+ "content": json.dumps(snapshot_content),
10028
+ "importance": "high",
10029
+ "tags": ["session_snapshot", "pre_compaction", "auto_captured"],
10030
+ "source_type": "hook",
10031
+ }
10032
+
10033
+ # Add workspace_id if available
10034
+ if WORKSPACE_ID:
10035
+ payload["workspace_id"] = WORKSPACE_ID
10036
+
10037
+ try:
10038
+ req = urllib.request.Request(
10039
+ f"{API_URL}/api/v1/memory/events",
10040
+ data=json.dumps(payload).encode('utf-8'),
10041
+ headers={
10042
+ "Content-Type": "application/json",
10043
+ "X-API-Key": API_KEY,
10044
+ },
10045
+ method="POST"
10046
+ )
10047
+ with urllib.request.urlopen(req, timeout=5) as resp:
10048
+ return True, "Snapshot saved"
10049
+ except urllib.error.URLError as e:
10050
+ return False, str(e)
10051
+ except Exception as e:
10052
+ return False, str(e)
9834
10053
 
9835
10054
  def main():
9836
10055
  if not ENABLED:
@@ -9841,26 +10060,38 @@ def main():
9841
10060
  except:
9842
10061
  sys.exit(0)
9843
10062
 
10063
+ # Load config from .mcp.json if env vars not set
10064
+ cwd = data.get("cwd", os.getcwd())
10065
+ load_config_from_mcp_json(cwd)
10066
+
10067
+ session_id = data.get("session_id", "unknown")
10068
+ transcript_path = data.get("transcript_path", "")
9844
10069
  trigger = data.get("trigger", "unknown")
9845
10070
  custom_instructions = data.get("custom_instructions", "")
9846
10071
 
9847
- # Build context injection for the AI
9848
- context = f"""[CONTEXT COMPACTION IMMINENT - {trigger.upper()}]
9849
- CRITICAL: Before context is compacted, you MUST save conversation state:
10072
+ # Parse transcript for context
10073
+ transcript_data = {}
10074
+ if transcript_path and os.path.exists(transcript_path):
10075
+ transcript_data = parse_transcript(transcript_path)
9850
10076
 
9851
- 1. IMMEDIATELY call: mcp__contextstream__session(action="capture", event_type="session_snapshot", title="Pre-compaction snapshot", content="<JSON with: conversation_summary, active_goals, recent_decisions, active_files, unfinished_work>")
10077
+ # Auto-save snapshot if enabled
10078
+ auto_save_status = ""
10079
+ if AUTO_SAVE and API_KEY:
10080
+ success, msg = save_snapshot(session_id, transcript_data, trigger)
10081
+ if success:
10082
+ auto_save_status = f"\\n[ContextStream: Auto-saved snapshot with {len(transcript_data.get('active_files', []))} active files]"
10083
+ else:
10084
+ auto_save_status = f"\\n[ContextStream: Auto-save failed - {msg}]"
9852
10085
 
9853
- 2. Include in the snapshot:
9854
- - conversation_summary: Brief summary of what was discussed
9855
- - active_goals: List of goals/tasks in progress
9856
- - recent_decisions: Key decisions made in this session
9857
- - active_files: Files currently being worked on
9858
- - unfinished_work: Any incomplete tasks
10086
+ # Build context injection for the AI (backup in case auto-save fails)
10087
+ files_list = ", ".join(transcript_data.get("active_files", [])[:5]) or "none detected"
10088
+ context = f"""[CONTEXT COMPACTION - {trigger.upper()}]{auto_save_status}
9859
10089
 
9860
- 3. After compaction, call session_init(is_post_compact=true) to restore context.
10090
+ Active files detected: {files_list}
10091
+ Tool calls in session: {transcript_data.get('tool_call_count', 0)}
9861
10092
 
9862
- {f"User instructions: {custom_instructions}" if custom_instructions else ""}
9863
- [END COMPACTION WARNING]"""
10093
+ After compaction, call session_init(is_post_compact=true) to restore context.
10094
+ {f"User instructions: {custom_instructions}" if custom_instructions else ""}"""
9864
10095
 
9865
10096
  output = {
9866
10097
  "hookSpecificOutput": {
@@ -9982,45 +10213,6 @@ function mergeHooksIntoSettings(existingSettings, newHooks) {
9982
10213
  settings.hooks = existingHooks;
9983
10214
  return settings;
9984
10215
  }
9985
- async function installClaudeCodeHooks(options) {
9986
- const result = { scripts: [], settings: [] };
9987
- if (!options.dryRun) {
9988
- const scripts = await installHookScripts({ includePreCompact: options.includePreCompact });
9989
- result.scripts.push(scripts.preToolUse, scripts.userPrompt);
9990
- if (scripts.preCompact) {
9991
- result.scripts.push(scripts.preCompact);
9992
- }
9993
- } else {
9994
- const hooksDir = getHooksDir();
9995
- result.scripts.push(
9996
- path5.join(hooksDir, "contextstream-redirect.py"),
9997
- path5.join(hooksDir, "contextstream-reminder.py")
9998
- );
9999
- if (options.includePreCompact) {
10000
- result.scripts.push(path5.join(hooksDir, "contextstream-precompact.py"));
10001
- }
10002
- }
10003
- const hooksConfig = buildHooksConfig({ includePreCompact: options.includePreCompact });
10004
- if (options.scope === "user" || options.scope === "both") {
10005
- const settingsPath = getClaudeSettingsPath("user");
10006
- if (!options.dryRun) {
10007
- const existing = await readClaudeSettings("user");
10008
- const merged = mergeHooksIntoSettings(existing, hooksConfig);
10009
- await writeClaudeSettings(merged, "user");
10010
- }
10011
- result.settings.push(settingsPath);
10012
- }
10013
- if ((options.scope === "project" || options.scope === "both") && options.projectPath) {
10014
- const settingsPath = getClaudeSettingsPath("project", options.projectPath);
10015
- if (!options.dryRun) {
10016
- const existing = await readClaudeSettings("project", options.projectPath);
10017
- const merged = mergeHooksIntoSettings(existing, hooksConfig);
10018
- await writeClaudeSettings(merged, "project", options.projectPath);
10019
- }
10020
- result.settings.push(settingsPath);
10021
- }
10022
- return result;
10023
- }
10024
10216
  function getIndexStatusPath() {
10025
10217
  return path5.join(homedir2(), ".contextstream", "indexed-projects.json");
10026
10218
  }
@@ -10049,6 +10241,547 @@ async function markProjectIndexed(projectPath, options) {
10049
10241
  };
10050
10242
  await writeIndexStatus(status);
10051
10243
  }
10244
+ var CLINE_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
10245
+ """
10246
+ ContextStream PreToolUse Hook for Cline
10247
+ Blocks discovery tools and redirects to ContextStream search.
10248
+
10249
+ Cline hooks use JSON output format:
10250
+ {
10251
+ "cancel": true/false,
10252
+ "errorMessage": "optional error description",
10253
+ "contextModification": "optional text to inject"
10254
+ }
10255
+ """
10256
+
10257
+ import json
10258
+ import sys
10259
+ import os
10260
+ from pathlib import Path
10261
+ from datetime import datetime, timedelta
10262
+
10263
+ ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
10264
+ INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
10265
+ STALE_THRESHOLD_DAYS = 7
10266
+
10267
+ DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
10268
+
10269
+ def is_discovery_glob(pattern):
10270
+ pattern_lower = pattern.lower()
10271
+ for p in DISCOVERY_PATTERNS:
10272
+ if p in pattern_lower:
10273
+ return True
10274
+ if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
10275
+ return True
10276
+ if "**" in pattern or "*/" in pattern:
10277
+ return True
10278
+ return False
10279
+
10280
+ def is_discovery_grep(file_path):
10281
+ if not file_path or file_path in [".", "./", "*", "**"]:
10282
+ return True
10283
+ if "*" in file_path or "**" in file_path:
10284
+ return True
10285
+ return False
10286
+
10287
+ def is_project_indexed(workspace_roots):
10288
+ """Check if any workspace root is in an indexed project."""
10289
+ if not INDEX_STATUS_FILE.exists():
10290
+ return False, False
10291
+
10292
+ try:
10293
+ with open(INDEX_STATUS_FILE, "r") as f:
10294
+ data = json.load(f)
10295
+ except:
10296
+ return False, False
10297
+
10298
+ projects = data.get("projects", {})
10299
+
10300
+ for workspace in workspace_roots:
10301
+ cwd_path = Path(workspace).resolve()
10302
+ for project_path, info in projects.items():
10303
+ try:
10304
+ indexed_path = Path(project_path).resolve()
10305
+ if cwd_path == indexed_path or indexed_path in cwd_path.parents:
10306
+ indexed_at = info.get("indexed_at")
10307
+ if indexed_at:
10308
+ try:
10309
+ indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
10310
+ if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
10311
+ return True, True
10312
+ except:
10313
+ pass
10314
+ return True, False
10315
+ except:
10316
+ continue
10317
+ return False, False
10318
+
10319
+ def output_allow(context_mod=None):
10320
+ result = {"cancel": False}
10321
+ if context_mod:
10322
+ result["contextModification"] = context_mod
10323
+ print(json.dumps(result))
10324
+ sys.exit(0)
10325
+
10326
+ def output_block(error_msg, context_mod=None):
10327
+ result = {"cancel": True, "errorMessage": error_msg}
10328
+ if context_mod:
10329
+ result["contextModification"] = context_mod
10330
+ print(json.dumps(result))
10331
+ sys.exit(0)
10332
+
10333
+ def main():
10334
+ if not ENABLED:
10335
+ output_allow()
10336
+
10337
+ try:
10338
+ data = json.load(sys.stdin)
10339
+ except:
10340
+ output_allow()
10341
+
10342
+ hook_name = data.get("hookName", "")
10343
+ if hook_name != "PreToolUse":
10344
+ output_allow()
10345
+
10346
+ tool = data.get("toolName", "")
10347
+ params = data.get("toolParameters", {})
10348
+ workspace_roots = data.get("workspaceRoots", [])
10349
+
10350
+ # Check if project is indexed
10351
+ is_indexed, is_stale = is_project_indexed(workspace_roots)
10352
+ if not is_indexed:
10353
+ output_allow()
10354
+
10355
+ # Check for discovery patterns
10356
+ if tool == "list_files" or tool == "search_files":
10357
+ pattern = params.get("path", "") or params.get("regex", "")
10358
+ if is_discovery_glob(pattern) or is_discovery_grep(pattern):
10359
+ output_block(
10360
+ f"Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}. "
10361
+ "ContextStream search is indexed and faster. Only use local tools if ContextStream returns 0 results.",
10362
+ "[CONTEXTSTREAM] Use ContextStream search for code discovery."
10363
+ )
10364
+
10365
+ elif tool == "read_file":
10366
+ # Allow read_file by default - blocking discovery at search level is enough
10367
+ pass
10368
+
10369
+ output_allow()
10370
+
10371
+ if __name__ == "__main__":
10372
+ main()
10373
+ `;
10374
+ var CLINE_USER_PROMPT_HOOK_SCRIPT = `#!/usr/bin/env python3
10375
+ """
10376
+ ContextStream UserPromptSubmit Hook for Cline
10377
+ Injects reminder about ContextStream rules on every message.
10378
+ """
10379
+
10380
+ import json
10381
+ import sys
10382
+ import os
10383
+
10384
+ ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
10385
+
10386
+ REMINDER = """[CONTEXTSTREAM RULES]
10387
+ 1. BEFORE list_files/search_files/read_file: mcp__contextstream__search(mode="hybrid") FIRST
10388
+ 2. Call context_smart at start of EVERY response
10389
+ 3. Local tools ONLY if ContextStream returns 0 results
10390
+ [END RULES]"""
10391
+
10392
+ def main():
10393
+ if not ENABLED:
10394
+ print(json.dumps({"cancel": False}))
10395
+ sys.exit(0)
10396
+
10397
+ try:
10398
+ json.load(sys.stdin)
10399
+ except:
10400
+ print(json.dumps({"cancel": False}))
10401
+ sys.exit(0)
10402
+
10403
+ print(json.dumps({
10404
+ "cancel": False,
10405
+ "contextModification": REMINDER
10406
+ }))
10407
+ sys.exit(0)
10408
+
10409
+ if __name__ == "__main__":
10410
+ main()
10411
+ `;
10412
+ function getClineHooksDir(scope, projectPath) {
10413
+ if (scope === "global") {
10414
+ return path5.join(homedir2(), "Documents", "Cline", "Rules", "Hooks");
10415
+ }
10416
+ if (!projectPath) {
10417
+ throw new Error("projectPath required for project scope");
10418
+ }
10419
+ return path5.join(projectPath, ".clinerules", "hooks");
10420
+ }
10421
+ async function installClineHookScripts(options) {
10422
+ const hooksDir = getClineHooksDir(options.scope, options.projectPath);
10423
+ await fs4.mkdir(hooksDir, { recursive: true });
10424
+ const preToolUsePath = path5.join(hooksDir, "PreToolUse");
10425
+ const userPromptPath = path5.join(hooksDir, "UserPromptSubmit");
10426
+ await fs4.writeFile(preToolUsePath, CLINE_PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
10427
+ await fs4.writeFile(userPromptPath, CLINE_USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
10428
+ return {
10429
+ preToolUse: preToolUsePath,
10430
+ userPromptSubmit: userPromptPath
10431
+ };
10432
+ }
10433
+ function getRooCodeHooksDir(scope, projectPath) {
10434
+ if (scope === "global") {
10435
+ return path5.join(homedir2(), ".roo", "hooks");
10436
+ }
10437
+ if (!projectPath) {
10438
+ throw new Error("projectPath required for project scope");
10439
+ }
10440
+ return path5.join(projectPath, ".roo", "hooks");
10441
+ }
10442
+ async function installRooCodeHookScripts(options) {
10443
+ const hooksDir = getRooCodeHooksDir(options.scope, options.projectPath);
10444
+ await fs4.mkdir(hooksDir, { recursive: true });
10445
+ const preToolUsePath = path5.join(hooksDir, "PreToolUse");
10446
+ const userPromptPath = path5.join(hooksDir, "UserPromptSubmit");
10447
+ await fs4.writeFile(preToolUsePath, CLINE_PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
10448
+ await fs4.writeFile(userPromptPath, CLINE_USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
10449
+ return {
10450
+ preToolUse: preToolUsePath,
10451
+ userPromptSubmit: userPromptPath
10452
+ };
10453
+ }
10454
+ function getKiloCodeHooksDir(scope, projectPath) {
10455
+ if (scope === "global") {
10456
+ return path5.join(homedir2(), ".kilocode", "hooks");
10457
+ }
10458
+ if (!projectPath) {
10459
+ throw new Error("projectPath required for project scope");
10460
+ }
10461
+ return path5.join(projectPath, ".kilocode", "hooks");
10462
+ }
10463
+ async function installKiloCodeHookScripts(options) {
10464
+ const hooksDir = getKiloCodeHooksDir(options.scope, options.projectPath);
10465
+ await fs4.mkdir(hooksDir, { recursive: true });
10466
+ const preToolUsePath = path5.join(hooksDir, "PreToolUse");
10467
+ const userPromptPath = path5.join(hooksDir, "UserPromptSubmit");
10468
+ await fs4.writeFile(preToolUsePath, CLINE_PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
10469
+ await fs4.writeFile(userPromptPath, CLINE_USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
10470
+ return {
10471
+ preToolUse: preToolUsePath,
10472
+ userPromptSubmit: userPromptPath
10473
+ };
10474
+ }
10475
+ var CURSOR_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
10476
+ """
10477
+ ContextStream PreToolUse Hook for Cursor
10478
+ Blocks discovery tools and redirects to ContextStream search.
10479
+
10480
+ Cursor hooks use JSON output format:
10481
+ {
10482
+ "decision": "allow" | "deny",
10483
+ "reason": "optional error description"
10484
+ }
10485
+ """
10486
+
10487
+ import json
10488
+ import sys
10489
+ import os
10490
+ from pathlib import Path
10491
+ from datetime import datetime, timedelta
10492
+
10493
+ ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
10494
+ INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
10495
+ STALE_THRESHOLD_DAYS = 7
10496
+
10497
+ DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
10498
+
10499
+ def is_discovery_glob(pattern):
10500
+ pattern_lower = pattern.lower()
10501
+ for p in DISCOVERY_PATTERNS:
10502
+ if p in pattern_lower:
10503
+ return True
10504
+ if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
10505
+ return True
10506
+ if "**" in pattern or "*/" in pattern:
10507
+ return True
10508
+ return False
10509
+
10510
+ def is_discovery_grep(file_path):
10511
+ if not file_path or file_path in [".", "./", "*", "**"]:
10512
+ return True
10513
+ if "*" in file_path or "**" in file_path:
10514
+ return True
10515
+ return False
10516
+
10517
+ def is_project_indexed(workspace_roots):
10518
+ """Check if any workspace root is in an indexed project."""
10519
+ if not INDEX_STATUS_FILE.exists():
10520
+ return False, False
10521
+
10522
+ try:
10523
+ with open(INDEX_STATUS_FILE, "r") as f:
10524
+ data = json.load(f)
10525
+ except:
10526
+ return False, False
10527
+
10528
+ projects = data.get("projects", {})
10529
+
10530
+ for workspace in workspace_roots:
10531
+ cwd_path = Path(workspace).resolve()
10532
+ for project_path, info in projects.items():
10533
+ try:
10534
+ indexed_path = Path(project_path).resolve()
10535
+ if cwd_path == indexed_path or indexed_path in cwd_path.parents:
10536
+ indexed_at = info.get("indexed_at")
10537
+ if indexed_at:
10538
+ try:
10539
+ indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
10540
+ if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
10541
+ return True, True
10542
+ except:
10543
+ pass
10544
+ return True, False
10545
+ except:
10546
+ continue
10547
+ return False, False
10548
+
10549
+ def output_allow():
10550
+ print(json.dumps({"decision": "allow"}))
10551
+ sys.exit(0)
10552
+
10553
+ def output_deny(reason):
10554
+ print(json.dumps({"decision": "deny", "reason": reason}))
10555
+ sys.exit(0)
10556
+
10557
+ def main():
10558
+ if not ENABLED:
10559
+ output_allow()
10560
+
10561
+ try:
10562
+ data = json.load(sys.stdin)
10563
+ except:
10564
+ output_allow()
10565
+
10566
+ hook_name = data.get("hook_event_name", "")
10567
+ if hook_name != "preToolUse":
10568
+ output_allow()
10569
+
10570
+ tool = data.get("tool_name", "")
10571
+ params = data.get("tool_input", {}) or data.get("parameters", {})
10572
+ workspace_roots = data.get("workspace_roots", [])
10573
+
10574
+ # Check if project is indexed
10575
+ is_indexed, _ = is_project_indexed(workspace_roots)
10576
+ if not is_indexed:
10577
+ output_allow()
10578
+
10579
+ # Check for Cursor tools
10580
+ if tool in ["Glob", "glob", "list_files"]:
10581
+ pattern = params.get("pattern", "") or params.get("path", "")
10582
+ if is_discovery_glob(pattern):
10583
+ output_deny(
10584
+ f"Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}. "
10585
+ "ContextStream search is indexed and faster."
10586
+ )
10587
+
10588
+ elif tool in ["Grep", "grep", "search_files", "ripgrep"]:
10589
+ pattern = params.get("pattern", "") or params.get("regex", "")
10590
+ file_path = params.get("path", "")
10591
+ if is_discovery_grep(file_path):
10592
+ output_deny(
10593
+ f"Use mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") instead of {tool}. "
10594
+ "ContextStream search is indexed and faster."
10595
+ )
10596
+
10597
+ output_allow()
10598
+
10599
+ if __name__ == "__main__":
10600
+ main()
10601
+ `;
10602
+ var CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT = `#!/usr/bin/env python3
10603
+ """
10604
+ ContextStream BeforeSubmitPrompt Hook for Cursor
10605
+ Injects reminder about ContextStream rules.
10606
+ """
10607
+
10608
+ import json
10609
+ import sys
10610
+ import os
10611
+
10612
+ ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
10613
+
10614
+ def main():
10615
+ if not ENABLED:
10616
+ print(json.dumps({"continue": True}))
10617
+ sys.exit(0)
10618
+
10619
+ try:
10620
+ json.load(sys.stdin)
10621
+ except:
10622
+ print(json.dumps({"continue": True}))
10623
+ sys.exit(0)
10624
+
10625
+ print(json.dumps({
10626
+ "continue": True,
10627
+ "user_message": "[CONTEXTSTREAM] Search with mcp__contextstream__search before using Glob/Grep/Read"
10628
+ }))
10629
+ sys.exit(0)
10630
+
10631
+ if __name__ == "__main__":
10632
+ main()
10633
+ `;
10634
+ function getCursorHooksConfigPath(scope, projectPath) {
10635
+ if (scope === "global") {
10636
+ return path5.join(homedir2(), ".cursor", "hooks.json");
10637
+ }
10638
+ if (!projectPath) {
10639
+ throw new Error("projectPath required for project scope");
10640
+ }
10641
+ return path5.join(projectPath, ".cursor", "hooks.json");
10642
+ }
10643
+ function getCursorHooksDir(scope, projectPath) {
10644
+ if (scope === "global") {
10645
+ return path5.join(homedir2(), ".cursor", "hooks");
10646
+ }
10647
+ if (!projectPath) {
10648
+ throw new Error("projectPath required for project scope");
10649
+ }
10650
+ return path5.join(projectPath, ".cursor", "hooks");
10651
+ }
10652
+ async function readCursorHooksConfig(scope, projectPath) {
10653
+ const configPath = getCursorHooksConfigPath(scope, projectPath);
10654
+ try {
10655
+ const content = await fs4.readFile(configPath, "utf-8");
10656
+ return JSON.parse(content);
10657
+ } catch {
10658
+ return { version: 1, hooks: {} };
10659
+ }
10660
+ }
10661
+ async function writeCursorHooksConfig(config, scope, projectPath) {
10662
+ const configPath = getCursorHooksConfigPath(scope, projectPath);
10663
+ const dir = path5.dirname(configPath);
10664
+ await fs4.mkdir(dir, { recursive: true });
10665
+ await fs4.writeFile(configPath, JSON.stringify(config, null, 2));
10666
+ }
10667
+ async function installCursorHookScripts(options) {
10668
+ const hooksDir = getCursorHooksDir(options.scope, options.projectPath);
10669
+ await fs4.mkdir(hooksDir, { recursive: true });
10670
+ const preToolUsePath = path5.join(hooksDir, "contextstream-pretooluse.py");
10671
+ const beforeSubmitPath = path5.join(hooksDir, "contextstream-beforesubmit.py");
10672
+ await fs4.writeFile(preToolUsePath, CURSOR_PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
10673
+ await fs4.writeFile(beforeSubmitPath, CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT, { mode: 493 });
10674
+ const existingConfig = await readCursorHooksConfig(options.scope, options.projectPath);
10675
+ const filterContextStreamHooks = (hooks) => {
10676
+ if (!hooks) return [];
10677
+ return hooks.filter((h) => !h.command?.includes("contextstream"));
10678
+ };
10679
+ const config = {
10680
+ version: 1,
10681
+ hooks: {
10682
+ ...existingConfig.hooks,
10683
+ preToolUse: [
10684
+ ...filterContextStreamHooks(existingConfig.hooks.preToolUse),
10685
+ {
10686
+ command: `python3 "${preToolUsePath}"`,
10687
+ type: "command",
10688
+ timeout: 5,
10689
+ matcher: { tool_name: "Glob|Grep|search_files|list_files|ripgrep" }
10690
+ }
10691
+ ],
10692
+ beforeSubmitPrompt: [
10693
+ ...filterContextStreamHooks(existingConfig.hooks.beforeSubmitPrompt),
10694
+ {
10695
+ command: `python3 "${beforeSubmitPath}"`,
10696
+ type: "command",
10697
+ timeout: 5
10698
+ }
10699
+ ]
10700
+ }
10701
+ };
10702
+ await writeCursorHooksConfig(config, options.scope, options.projectPath);
10703
+ const configPath = getCursorHooksConfigPath(options.scope, options.projectPath);
10704
+ return {
10705
+ preToolUse: preToolUsePath,
10706
+ beforeSubmitPrompt: beforeSubmitPath,
10707
+ config: configPath
10708
+ };
10709
+ }
10710
+ async function installEditorHooks(options) {
10711
+ const { editor, scope, projectPath, includePreCompact } = options;
10712
+ switch (editor) {
10713
+ case "claude": {
10714
+ if (scope === "project" && !projectPath) {
10715
+ throw new Error("projectPath required for project scope");
10716
+ }
10717
+ const scripts = await installHookScripts({ includePreCompact });
10718
+ const hooksConfig = buildHooksConfig({ includePreCompact });
10719
+ const settingsScope = scope === "global" ? "user" : "project";
10720
+ const existing = await readClaudeSettings(settingsScope, projectPath);
10721
+ const merged = mergeHooksIntoSettings(existing, hooksConfig);
10722
+ await writeClaudeSettings(merged, settingsScope, projectPath);
10723
+ const installed = [scripts.preToolUse, scripts.userPrompt];
10724
+ if (scripts.preCompact) installed.push(scripts.preCompact);
10725
+ return {
10726
+ editor: "claude",
10727
+ installed,
10728
+ hooksDir: getHooksDir()
10729
+ };
10730
+ }
10731
+ case "cline": {
10732
+ const scripts = await installClineHookScripts({ scope, projectPath });
10733
+ return {
10734
+ editor: "cline",
10735
+ installed: [scripts.preToolUse, scripts.userPromptSubmit],
10736
+ hooksDir: getClineHooksDir(scope, projectPath)
10737
+ };
10738
+ }
10739
+ case "roo": {
10740
+ const scripts = await installRooCodeHookScripts({ scope, projectPath });
10741
+ return {
10742
+ editor: "roo",
10743
+ installed: [scripts.preToolUse, scripts.userPromptSubmit],
10744
+ hooksDir: getRooCodeHooksDir(scope, projectPath)
10745
+ };
10746
+ }
10747
+ case "kilo": {
10748
+ const scripts = await installKiloCodeHookScripts({ scope, projectPath });
10749
+ return {
10750
+ editor: "kilo",
10751
+ installed: [scripts.preToolUse, scripts.userPromptSubmit],
10752
+ hooksDir: getKiloCodeHooksDir(scope, projectPath)
10753
+ };
10754
+ }
10755
+ case "cursor": {
10756
+ const scripts = await installCursorHookScripts();
10757
+ return {
10758
+ editor: "cursor",
10759
+ installed: [scripts.preToolUse, scripts.beforeSubmit],
10760
+ hooksDir: getCursorHooksDir()
10761
+ };
10762
+ }
10763
+ default:
10764
+ throw new Error(`Unsupported editor: ${editor}`);
10765
+ }
10766
+ }
10767
+ async function installAllEditorHooks(options) {
10768
+ const editors = options.editors || ["claude", "cline", "roo", "kilo", "cursor"];
10769
+ const results = [];
10770
+ for (const editor of editors) {
10771
+ try {
10772
+ const result = await installEditorHooks({
10773
+ editor,
10774
+ scope: options.scope,
10775
+ projectPath: options.projectPath,
10776
+ includePreCompact: options.includePreCompact
10777
+ });
10778
+ results.push(result);
10779
+ } catch (error) {
10780
+ console.error(`Failed to install hooks for ${editor}:`, error);
10781
+ }
10782
+ }
10783
+ return results;
10784
+ }
10052
10785
 
10053
10786
  // src/token-savings.ts
10054
10787
  var TOKEN_SAVINGS_FORMULA_VERSION = 1;
@@ -10114,6 +10847,54 @@ function trackToolTokenSavings(client, tool, contextText, params, extraMetadata)
10114
10847
  }
10115
10848
 
10116
10849
  // src/tools.ts
10850
+ var LOG_LEVEL = (process.env.CONTEXTSTREAM_LOG_LEVEL || "normal").toLowerCase();
10851
+ var LOG_QUIET = LOG_LEVEL === "quiet";
10852
+ var LOG_VERBOSE = LOG_LEVEL === "verbose";
10853
+ var TOOL_DISPLAY_NAMES = {
10854
+ session_init: "init",
10855
+ session: "session",
10856
+ context_smart: "context",
10857
+ search: "search",
10858
+ memory: "memory",
10859
+ graph: "graph",
10860
+ ai: "ai",
10861
+ generate_rules: "rules",
10862
+ generate_editor_rules: "rules",
10863
+ help: "help",
10864
+ workspace_bootstrap: "bootstrap",
10865
+ workspace: "workspace",
10866
+ github: "github",
10867
+ slack: "slack",
10868
+ notion: "notion",
10869
+ jira: "jira",
10870
+ ingest: "ingest",
10871
+ // Hide internal tools
10872
+ session_capture_smart: "",
10873
+ session_restore_context: ""
10874
+ };
10875
+ function getToolDisplayName(toolName) {
10876
+ if (toolName in TOOL_DISPLAY_NAMES) {
10877
+ return TOOL_DISPLAY_NAMES[toolName];
10878
+ }
10879
+ return toolName.replace(/_/g, " ").replace(/^mcp__contextstream__/, "");
10880
+ }
10881
+ function logTool(toolName, status, details) {
10882
+ if (LOG_QUIET && status !== "error") return;
10883
+ const displayName = getToolDisplayName(toolName);
10884
+ if (!displayName) return;
10885
+ const icon = status === "start" ? "\u25E6" : status === "done" ? "\u2713" : "\u2717";
10886
+ const message = details ? `${icon} ${displayName}: ${details}` : `${icon} ${displayName}`;
10887
+ console.error(message);
10888
+ }
10889
+ function logSystem(message, level = "info") {
10890
+ if (LOG_QUIET && level === "info") return;
10891
+ const prefix = level === "error" ? "\u2717" : level === "warn" ? "!" : "\u2022";
10892
+ console.error(`${prefix} ${message}`);
10893
+ }
10894
+ function logDebug(message) {
10895
+ if (!LOG_VERBOSE) return;
10896
+ console.error(`[DEBUG] ${message}`);
10897
+ }
10117
10898
  var LESSON_DEDUP_WINDOW_MS = 2 * 60 * 1e3;
10118
10899
  var recentLessonCaptures = /* @__PURE__ */ new Map();
10119
10900
  var SEARCH_RULES_REMINDER_ENABLED = process.env.CONTEXTSTREAM_SEARCH_REMINDER?.toLowerCase() !== "false";
@@ -10124,9 +10905,75 @@ BEFORE using EnterPlanMode/Task(Plan) \u2192 call mcp__contextstream__session(ac
10124
10905
  Local tools ONLY if ContextStream returns 0 results after retry.
10125
10906
  `.trim();
10126
10907
  var LESSONS_REMINDER_PREFIX = `
10127
- \u26A0\uFE0F [LESSONS - REVIEW BEFORE CHANGES]
10128
- Past mistakes found that may be relevant. STOP and review before proceeding:
10908
+ \u{1F6A8} [LESSONS_WARNING] Past Mistakes Found - READ BEFORE PROCEEDING!
10909
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
10910
+ \u26A0\uFE0F IMPORTANT: You MUST review these lessons and tell the user about relevant ones.
10911
+ These are mistakes from past sessions that you should NOT repeat.
10129
10912
  `.trim();
10913
+ var RISKY_ACTION_KEYWORDS = [
10914
+ // Code changes
10915
+ "refactor",
10916
+ "rewrite",
10917
+ "restructure",
10918
+ "reorganize",
10919
+ "migrate",
10920
+ "delete",
10921
+ "remove",
10922
+ "drop",
10923
+ "deprecate",
10924
+ // Database
10925
+ "database",
10926
+ "migration",
10927
+ "schema",
10928
+ "sql",
10929
+ // Deployment
10930
+ "deploy",
10931
+ "release",
10932
+ "production",
10933
+ "prod",
10934
+ // API changes
10935
+ "api",
10936
+ "endpoint",
10937
+ "breaking change",
10938
+ // Architecture
10939
+ "architecture",
10940
+ "design",
10941
+ "pattern",
10942
+ // Testing
10943
+ "test",
10944
+ "testing",
10945
+ // Security
10946
+ "auth",
10947
+ "security",
10948
+ "permission",
10949
+ "credential",
10950
+ "access",
10951
+ "token",
10952
+ "secret",
10953
+ // Version control
10954
+ "git",
10955
+ "commit",
10956
+ "merge",
10957
+ "rebase",
10958
+ "push",
10959
+ "force",
10960
+ // Infrastructure
10961
+ "config",
10962
+ "environment",
10963
+ "env",
10964
+ "docker",
10965
+ "kubernetes",
10966
+ "k8s",
10967
+ // Performance
10968
+ "performance",
10969
+ "optimize",
10970
+ "cache",
10971
+ "memory"
10972
+ ];
10973
+ function detectRiskyActions(userMessage) {
10974
+ const messageLower = userMessage.toLowerCase();
10975
+ return RISKY_ACTION_KEYWORDS.filter((keyword) => messageLower.includes(keyword));
10976
+ }
10130
10977
  function generateLessonsReminder(result) {
10131
10978
  const lessons = result.lessons;
10132
10979
  if (!lessons || lessons.length === 0) {
@@ -10141,7 +10988,8 @@ function generateLessonsReminder(result) {
10141
10988
  return `
10142
10989
 
10143
10990
  ${LESSONS_REMINDER_PREFIX}
10144
- ${lessonLines.join("\n")}`;
10991
+ ${lessonLines.join("\n")}
10992
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`;
10145
10993
  }
10146
10994
  function generateRulesUpdateWarning(rulesNotice) {
10147
10995
  if (!rulesNotice || rulesNotice.status !== "behind" && rulesNotice.status !== "missing") {
@@ -10164,12 +11012,16 @@ function generateVersionUpdateWarning(versionNotice) {
10164
11012
  return "";
10165
11013
  }
10166
11014
  return `
10167
- \u{1F6A8} [MCP SERVER OUTDATED - UPDATE RECOMMENDED]
10168
- Current: ${versionNotice.current} \u2192 Latest: ${versionNotice.latest}
10169
- New version may include critical bug fixes, performance improvements, and new features.
11015
+ \u{1F6A8} [VERSION_NOTICE] MCP Server Update Available!
11016
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
11017
+ Version: ${versionNotice.current} \u2192 ${versionNotice.latest}
11018
+
11019
+ \u26A0\uFE0F IMPORTANT: You MUST tell the user about this update IMMEDIATELY.
11020
+ Do not proceed with other work until you have informed the user.
10170
11021
 
10171
- **UPDATE NOW:** Run \`${versionNotice.upgrade_command || "npm update @contextstream/mcp-server"}\`
10172
- Then restart Claude Code to use the new version.
11022
+ Update command: \`${versionNotice.upgrade_command || "npm update -g @contextstream/mcp-server"}\`
11023
+ After updating, restart the AI tool to use the new version.
11024
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
10173
11025
  `.trim();
10174
11026
  }
10175
11027
  var DEFAULT_PARAM_DESCRIPTIONS = {
@@ -10225,7 +11077,6 @@ var RULES_PROJECT_FILES = {
10225
11077
  codex: "AGENTS.md",
10226
11078
  claude: "CLAUDE.md",
10227
11079
  cursor: ".cursorrules",
10228
- windsurf: ".windsurfrules",
10229
11080
  cline: ".clinerules",
10230
11081
  kilo: path6.join(".kilocode", "rules", "contextstream.md"),
10231
11082
  roo: path6.join(".roo", "rules", "contextstream.md"),
@@ -10233,7 +11084,6 @@ var RULES_PROJECT_FILES = {
10233
11084
  };
10234
11085
  var RULES_GLOBAL_FILES = {
10235
11086
  codex: [path6.join(homedir3(), ".codex", "AGENTS.md")],
10236
- windsurf: [path6.join(homedir3(), ".codeium", "windsurf", "memories", "global_rules.md")],
10237
11087
  kilo: [path6.join(homedir3(), ".kilocode", "rules", "contextstream.md")],
10238
11088
  roo: [path6.join(homedir3(), ".roo", "rules", "contextstream.md")]
10239
11089
  };
@@ -10273,7 +11123,6 @@ function detectEditorFromClientName(clientName) {
10273
11123
  if (!clientName) return null;
10274
11124
  const normalized = clientName.toLowerCase().trim();
10275
11125
  if (normalized.includes("cursor")) return "cursor";
10276
- if (normalized.includes("windsurf") || normalized.includes("codeium")) return "windsurf";
10277
11126
  if (normalized.includes("claude")) return "claude";
10278
11127
  if (normalized.includes("cline")) return "cline";
10279
11128
  if (normalized.includes("kilo")) return "kilo";
@@ -10435,7 +11284,6 @@ var CONTEXTSTREAM_PREAMBLE_PATTERNS = [
10435
11284
  /^#\s+codex cli instructions$/i,
10436
11285
  /^#\s+claude code instructions$/i,
10437
11286
  /^#\s+cursor rules$/i,
10438
- /^#\s+windsurf rules$/i,
10439
11287
  /^#\s+cline rules$/i,
10440
11288
  /^#\s+kilo code rules$/i,
10441
11289
  /^#\s+roo code rules$/i,
@@ -11030,6 +11878,7 @@ var ALL_INTEGRATION_TOOLS = /* @__PURE__ */ new Set([
11030
11878
  ...CROSS_INTEGRATION_TOOLS
11031
11879
  ]);
11032
11880
  var AUTO_HIDE_INTEGRATIONS = process.env.CONTEXTSTREAM_AUTO_HIDE_INTEGRATIONS !== "false";
11881
+ var RESTORE_CONTEXT_DEFAULT = process.env.CONTEXTSTREAM_RESTORE_CONTEXT !== "false";
11033
11882
  var TOKEN_SENSITIVE_CLIENTS = /* @__PURE__ */ new Set([
11034
11883
  "claude",
11035
11884
  "claude-code",
@@ -11419,9 +12268,7 @@ function resolveToolFilter() {
11419
12268
  if (allowlistRaw) {
11420
12269
  const allowlist = parseToolList(allowlistRaw);
11421
12270
  if (allowlist.size === 0) {
11422
- console.error(
11423
- "[ContextStream] CONTEXTSTREAM_TOOL_ALLOWLIST is empty; using standard toolset."
11424
- );
12271
+ logDebug("TOOL_ALLOWLIST is empty; using standard toolset");
11425
12272
  return { allowlist: STANDARD_TOOLSET, source: "standard", autoDetected: false };
11426
12273
  }
11427
12274
  return { allowlist, source: "allowlist", autoDetected: false };
@@ -11432,9 +12279,7 @@ function resolveToolFilter() {
11432
12279
  if (clientDetectedFromEnv && AUTO_TOOLSET_ENABLED) {
11433
12280
  const recommended = getRecommendedToolset(void 0, true);
11434
12281
  if (recommended) {
11435
- console.error(
11436
- "[ContextStream] Detected Claude Code via environment. Using light toolset for optimal token usage."
11437
- );
12282
+ logDebug("Detected Claude Code, using light toolset");
11438
12283
  return { allowlist: recommended, source: "auto-claude", autoDetected: true };
11439
12284
  }
11440
12285
  }
@@ -11443,12 +12288,10 @@ function resolveToolFilter() {
11443
12288
  if (toolsetRaw.trim().toLowerCase() === "auto") {
11444
12289
  clientDetectedFromEnv = detectClaudeCodeFromEnv();
11445
12290
  if (clientDetectedFromEnv) {
11446
- console.error("[ContextStream] TOOLSET=auto: Detected Claude Code, using light toolset.");
12291
+ logDebug("TOOLSET=auto: Detected Claude Code, using light toolset");
11447
12292
  return { allowlist: LIGHT_TOOLSET, source: "auto-claude", autoDetected: true };
11448
12293
  }
11449
- console.error(
11450
- "[ContextStream] TOOLSET=auto: Will adjust toolset based on MCP client (currently standard)."
11451
- );
12294
+ logDebug("TOOLSET=auto: Will adjust based on MCP client");
11452
12295
  return { allowlist: STANDARD_TOOLSET, source: "auto-pending", autoDetected: true };
11453
12296
  }
11454
12297
  const key = toolsetRaw.trim().toLowerCase();
@@ -11459,9 +12302,7 @@ function resolveToolFilter() {
11459
12302
  }
11460
12303
  return { allowlist: resolved, source: key, autoDetected: false };
11461
12304
  }
11462
- console.error(
11463
- `[ContextStream] Unknown CONTEXTSTREAM_TOOLSET "${toolsetRaw}". Using standard toolset.`
11464
- );
12305
+ logDebug(`Unknown TOOLSET "${toolsetRaw}"; using standard`);
11465
12306
  return { allowlist: STANDARD_TOOLSET, source: "standard", autoDetected: false };
11466
12307
  }
11467
12308
  function formatContent(data, forceFormat) {
@@ -11559,22 +12400,16 @@ function isDuplicateLessonCapture(signature) {
11559
12400
  }
11560
12401
  function setupClientDetection(server) {
11561
12402
  if (!AUTO_TOOLSET_ENABLED) {
11562
- console.error(
11563
- "[ContextStream] Auto-toolset: DISABLED (set CONTEXTSTREAM_AUTO_TOOLSET=true to enable)"
11564
- );
12403
+ logDebug("Auto-toolset: DISABLED");
11565
12404
  return;
11566
12405
  }
11567
12406
  if (clientDetectedFromEnv) {
11568
- console.error(
11569
- "[ContextStream] Client detection: Already detected Claude Code from environment"
11570
- );
12407
+ logDebug("Client detection: Already detected from environment");
11571
12408
  return;
11572
12409
  }
11573
12410
  const lowLevelServer = server.server;
11574
12411
  if (!lowLevelServer) {
11575
- console.error(
11576
- "[ContextStream] Warning: Could not access low-level MCP server for client detection"
11577
- );
12412
+ logDebug("Warning: Could not access low-level MCP server for client detection");
11578
12413
  return;
11579
12414
  }
11580
12415
  lowLevelServer.oninitialized = () => {
@@ -11584,23 +12419,21 @@ function setupClientDetection(server) {
11584
12419
  detectedClientInfo = clientVersion;
11585
12420
  const clientName = clientVersion.name || "unknown";
11586
12421
  const clientVer = clientVersion.version || "unknown";
11587
- console.error(`[ContextStream] MCP Client detected: ${clientName} v${clientVer}`);
12422
+ logDebug(`MCP Client detected: ${clientName} v${clientVer}`);
11588
12423
  if (isTokenSensitiveClient(clientName)) {
11589
- console.error(
11590
- "[ContextStream] Token-sensitive client detected. Consider using CONTEXTSTREAM_TOOLSET=light for optimal performance."
11591
- );
12424
+ logDebug("Token-sensitive client detected");
11592
12425
  try {
11593
12426
  lowLevelServer.sendToolsListChanged?.();
11594
- console.error("[ContextStream] Emitted tools/list_changed notification");
12427
+ logDebug("Emitted tools/list_changed notification");
11595
12428
  } catch (error) {
11596
12429
  }
11597
12430
  }
11598
12431
  }
11599
12432
  } catch (error) {
11600
- console.error("[ContextStream] Error in client detection callback:", error);
12433
+ logSystem(`Error in client detection: ${error}`, "error");
11601
12434
  }
11602
12435
  };
11603
- console.error("[ContextStream] Client detection: Callback registered for MCP initialize");
12436
+ logDebug("Client detection callback registered");
11604
12437
  }
11605
12438
  function registerTools(server, client, sessionManager) {
11606
12439
  const upgradeUrl2 = process.env.CONTEXTSTREAM_UPGRADE_URL || "https://contextstream.io/pricing";
@@ -11608,69 +12441,31 @@ function registerTools(server, client, sessionManager) {
11608
12441
  const toolAllowlist = toolFilter.allowlist;
11609
12442
  if (toolAllowlist) {
11610
12443
  const source = toolFilter.source;
11611
- const autoNote = toolFilter.autoDetected ? " (auto-detected)" : "";
11612
- const hint = source === "light" || source === "auto-claude" ? " Set CONTEXTSTREAM_TOOLSET=standard or complete for more tools." : source === "standard" ? " Set CONTEXTSTREAM_TOOLSET=complete for all tools." : source === "auto-pending" ? " Toolset may be adjusted when MCP client is detected." : "";
11613
- console.error(
11614
- `[ContextStream] Toolset: ${source} (${toolAllowlist.size} tools)${autoNote}.${hint}`
11615
- );
12444
+ logDebug(`Toolset: ${source} (${toolAllowlist.size} tools)`);
11616
12445
  } else {
11617
- console.error(`[ContextStream] Toolset: complete (all tools).`);
12446
+ logDebug("Toolset: complete (all tools)");
11618
12447
  }
11619
- if (AUTO_TOOLSET_ENABLED) {
11620
- if (clientDetectedFromEnv) {
11621
- console.error("[ContextStream] Auto-toolset: ACTIVE (Claude Code detected from environment)");
11622
- } else {
11623
- console.error("[ContextStream] Auto-toolset: ENABLED (will detect MCP client on initialize)");
11624
- }
11625
- }
11626
- if (AUTO_HIDE_INTEGRATIONS) {
11627
- console.error(
11628
- `[ContextStream] Integration auto-hide: ENABLED (${ALL_INTEGRATION_TOOLS.size} tools hidden until integrations connected)`
11629
- );
11630
- console.error("[ContextStream] Set CONTEXTSTREAM_AUTO_HIDE_INTEGRATIONS=false to disable.");
11631
- } else {
11632
- console.error("[ContextStream] Integration auto-hide: disabled");
11633
- }
11634
- if (COMPACT_SCHEMA_ENABLED) {
11635
- console.error("[ContextStream] Schema mode: COMPACT (shorter descriptions, minimal params)");
11636
- } else {
11637
- console.error(
11638
- "[ContextStream] Schema mode: full (set CONTEXTSTREAM_SCHEMA_MODE=compact to reduce token overhead)"
11639
- );
11640
- }
11641
- if (PROGRESSIVE_MODE) {
11642
- const coreBundle = TOOL_BUNDLES.core;
11643
- console.error(
11644
- `[ContextStream] Progressive mode: ENABLED (starting with ${coreBundle.size} core tools)`
11645
- );
11646
- console.error(
11647
- "[ContextStream] Use tools_enable_bundle to unlock additional tool bundles dynamically."
11648
- );
11649
- }
11650
- if (ROUTER_MODE) {
11651
- console.error(
11652
- "[ContextStream] Router mode: ENABLED (all operations accessed via contextstream/contextstream_help)"
11653
- );
11654
- console.error(
11655
- "[ContextStream] Only 2 tools registered. Use contextstream_help to see available operations."
11656
- );
12448
+ if (AUTO_TOOLSET_ENABLED) {
12449
+ logDebug(clientDetectedFromEnv ? "Auto-toolset: ACTIVE" : "Auto-toolset: ENABLED");
11657
12450
  }
11658
- if (COMPACT_OUTPUT) {
11659
- console.error(
11660
- "[ContextStream] Output format: COMPACT (minified JSON, ~30% fewer tokens per response)"
11661
- );
12451
+ if (AUTO_HIDE_INTEGRATIONS) {
12452
+ logDebug(`Integration auto-hide: ENABLED (${ALL_INTEGRATION_TOOLS.size} tools hidden)`);
12453
+ }
12454
+ if (COMPACT_SCHEMA_ENABLED) {
12455
+ logDebug("Schema mode: COMPACT");
11662
12456
  } else {
11663
- console.error(
11664
- "[ContextStream] Output format: pretty (set CONTEXTSTREAM_OUTPUT_FORMAT=compact for fewer tokens)"
11665
- );
12457
+ logDebug("Schema mode: full");
11666
12458
  }
12459
+ if (PROGRESSIVE_MODE) {
12460
+ const coreBundle = TOOL_BUNDLES.core;
12461
+ logDebug(`Progressive mode: ENABLED (${coreBundle.size} core tools)`);
12462
+ }
12463
+ if (ROUTER_MODE) {
12464
+ logDebug("Router mode: ENABLED");
12465
+ }
12466
+ logDebug(COMPACT_OUTPUT ? "Output: COMPACT" : "Output: pretty");
11667
12467
  if (CONSOLIDATED_MODE) {
11668
- console.error(
11669
- `[ContextStream] Consolidated mode: ENABLED (~${CONSOLIDATED_TOOLS.size} domain tools, ~75% token reduction)`
11670
- );
11671
- console.error("[ContextStream] Set CONTEXTSTREAM_CONSOLIDATED=false to use individual tools.");
11672
- } else {
11673
- console.error("[ContextStream] Consolidated mode: disabled (using individual tools)");
12468
+ logDebug(`Consolidated mode: ENABLED (~${CONSOLIDATED_TOOLS.size} domain tools)`);
11674
12469
  }
11675
12470
  const serverRef = server;
11676
12471
  const defaultProTools = /* @__PURE__ */ new Set([
@@ -11789,12 +12584,10 @@ function registerTools(server, client, sessionManager) {
11789
12584
  notion: notionConnected,
11790
12585
  workspaceId
11791
12586
  };
11792
- console.error(
11793
- `[ContextStream] Integration status: Slack=${slackConnected}, GitHub=${githubConnected}, Notion=${notionConnected}`
11794
- );
12587
+ logDebug(`Integrations: Slack=${slackConnected}, GitHub=${githubConnected}, Notion=${notionConnected}`);
11795
12588
  return { slack: slackConnected, github: githubConnected, notion: notionConnected };
11796
12589
  } catch (error) {
11797
- console.error("[ContextStream] Failed to check integration status:", error);
12590
+ logDebug(`Failed to check integration status: ${error}`);
11798
12591
  return { slack: false, github: false, notion: false };
11799
12592
  }
11800
12593
  }
@@ -11815,11 +12608,9 @@ function registerTools(server, client, sessionManager) {
11815
12608
  try {
11816
12609
  server.server?.sendToolsListChanged?.();
11817
12610
  toolsListChangedNotified = true;
11818
- console.error(
11819
- "[ContextStream] Emitted tools/list_changed notification (integrations detected)"
11820
- );
12611
+ logDebug("Emitted tools/list_changed notification (integrations detected)");
11821
12612
  } catch (error) {
11822
- console.error("[ContextStream] Failed to emit tools/list_changed:", error);
12613
+ logDebug(`Failed to emit tools/list_changed: ${error}`);
11823
12614
  }
11824
12615
  }
11825
12616
  }
@@ -12087,7 +12878,7 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
12087
12878
  lowLevelServer.sendToolsListChanged?.();
12088
12879
  } catch {
12089
12880
  }
12090
- console.error(`[ContextStream] Bundle '${bundleName}' enabled with ${toolsEnabled} tools.`);
12881
+ logSystem(`bundle '${bundleName}' enabled (${toolsEnabled} tools)`);
12091
12882
  return {
12092
12883
  success: true,
12093
12884
  message: `Enabled bundle '${bundleName}' with ${toolsEnabled} tools.`,
@@ -12196,33 +12987,27 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
12196
12987
  if (options.preflight) {
12197
12988
  const fileCheck = await countIndexableFiles(resolvedPath, { maxFiles: 1 });
12198
12989
  if (fileCheck.count === 0) {
12199
- console.error(
12200
- `[ContextStream] No indexable files found in ${resolvedPath}. Skipping ingest.`
12201
- );
12990
+ logDebug(`No indexable files in ${resolvedPath}`);
12202
12991
  return;
12203
12992
  }
12204
12993
  }
12205
12994
  let totalIndexed = 0;
12206
12995
  let batchCount = 0;
12207
- console.error(
12208
- `[ContextStream] Starting background ingestion for project ${projectId} from ${resolvedPath}`
12209
- );
12996
+ logTool("ingest", "start", `indexing ${resolvedPath}`);
12210
12997
  for await (const batch of readAllFilesInBatches(resolvedPath, { batchSize: 50 })) {
12211
12998
  const result = await client.ingestFiles(projectId, batch, ingestOptions);
12212
12999
  totalIndexed += result.data?.files_indexed ?? batch.length;
12213
13000
  batchCount++;
12214
13001
  }
12215
- console.error(
12216
- `[ContextStream] Completed background ingestion: ${totalIndexed} files in ${batchCount} batches`
12217
- );
13002
+ logTool("ingest", "done", `${totalIndexed} files`);
12218
13003
  try {
12219
13004
  await markProjectIndexed(resolvedPath, { project_id: projectId });
12220
- console.error(`[ContextStream] Marked project as indexed: ${resolvedPath}`);
13005
+ logDebug(`Marked project as indexed: ${resolvedPath}`);
12221
13006
  } catch (markError) {
12222
- console.error(`[ContextStream] Failed to mark project as indexed:`, markError);
13007
+ logDebug(`Failed to mark project as indexed: ${markError}`);
12223
13008
  }
12224
13009
  } catch (error) {
12225
- console.error(`[ContextStream] Ingestion failed:`, error);
13010
+ logTool("ingest", "error", `${error}`);
12226
13011
  }
12227
13012
  })();
12228
13013
  }
@@ -12467,9 +13252,7 @@ Use contextstream_help({}) to see available operations.`
12467
13252
  };
12468
13253
  }
12469
13254
  );
12470
- console.error(
12471
- `[ContextStream] Router mode: Registered 2 meta-tools, ${operationsRegistry.size} operations available via dispatcher.`
12472
- );
13255
+ logDebug(`Router mode: 2 meta-tools, ${operationsRegistry.size} operations`);
12473
13256
  }
12474
13257
  registerTool(
12475
13258
  "workspaces_list",
@@ -12629,7 +13412,7 @@ Access: Free`,
12629
13412
  rulesSkipped = ruleResults.filter((r) => r.status.startsWith("skipped")).map((r) => r.filename);
12630
13413
  }
12631
13414
  } catch (err) {
12632
- console.error("[ContextStream] Failed to write project config:", err);
13415
+ logDebug(`Failed to write project config: ${err}`);
12633
13416
  }
12634
13417
  }
12635
13418
  const response = {
@@ -13145,10 +13928,7 @@ Access: Free`,
13145
13928
  const stats = await client.projectStatistics(projectId);
13146
13929
  estimate = estimateGraphIngestMinutes(stats);
13147
13930
  } catch (error) {
13148
- console.error(
13149
- "[ContextStream] Failed to fetch project statistics for graph ingest estimate:",
13150
- error
13151
- );
13931
+ logDebug(`Failed to fetch project statistics for graph estimate: ${error}`);
13152
13932
  }
13153
13933
  const result = await client.graphIngest({ project_id: projectId, wait });
13154
13934
  const estimateText = estimate ? `Estimated time: ${estimate.min}-${estimate.max} min${estimate.basis ? ` (based on ${estimate.basis})` : ""}.` : "Estimated time varies with repo size.";
@@ -13824,7 +14604,7 @@ This does semantic search on the first message. You only need context_smart on s
13824
14604
  "If true, skip automatic project creation/matching. Use for parent folders containing multiple projects where you want workspace-level context but no project-specific context."
13825
14605
  ),
13826
14606
  is_post_compact: external_exports.boolean().optional().describe(
13827
- "Set to true when resuming after conversation compaction. This prioritizes session_snapshot restoration and recent decisions."
14607
+ "Controls context restoration from recent snapshots. Defaults to true (always restores). Set to false to skip restoration. Can also be controlled via CONTEXTSTREAM_RESTORE_CONTEXT environment variable."
13828
14608
  )
13829
14609
  })
13830
14610
  },
@@ -13845,19 +14625,22 @@ This does semantic search on the first message. You only need context_smart on s
13845
14625
  }
13846
14626
  const result = await client.initSession(input, ideRoots);
13847
14627
  result.tools_hint = getCoreToolsHint();
13848
- if (input.is_post_compact) {
14628
+ const shouldRestoreContext = input.is_post_compact ?? RESTORE_CONTEXT_DEFAULT;
14629
+ if (shouldRestoreContext) {
14630
+ result.is_post_compact = true;
13849
14631
  const workspaceIdForRestore = typeof result.workspace_id === "string" ? result.workspace_id : void 0;
13850
14632
  const projectIdForRestore = typeof result.project_id === "string" ? result.project_id : void 0;
13851
14633
  if (workspaceIdForRestore) {
13852
14634
  try {
13853
- const snapshotSearch = await client.searchEvents({
14635
+ const listResult = await client.listMemoryEvents({
13854
14636
  workspace_id: workspaceIdForRestore,
13855
14637
  project_id: projectIdForRestore,
13856
- query: "session_snapshot",
13857
- event_types: ["session_snapshot"],
13858
- limit: 1
14638
+ limit: 50
13859
14639
  });
13860
- const snapshots = snapshotSearch?.data?.results || snapshotSearch?.results || snapshotSearch?.data || [];
14640
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
14641
+ const snapshots = allEvents.filter(
14642
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
14643
+ );
13861
14644
  if (snapshots && snapshots.length > 0) {
13862
14645
  const latestSnapshot = snapshots[0];
13863
14646
  let snapshotData;
@@ -13866,19 +14649,58 @@ This does semantic search on the first message. You only need context_smart on s
13866
14649
  } catch {
13867
14650
  snapshotData = { conversation_summary: latestSnapshot.content };
13868
14651
  }
14652
+ const prevSessionId = snapshotData.session_id || latestSnapshot.session_id;
14653
+ const sessionLinking = {};
14654
+ if (prevSessionId) {
14655
+ sessionLinking.previous_session_id = prevSessionId;
14656
+ const workingOn = [];
14657
+ const activeFiles = snapshotData.active_files;
14658
+ const lastTools = snapshotData.last_tools;
14659
+ if (activeFiles && activeFiles.length > 0) {
14660
+ workingOn.push(`Files: ${activeFiles.slice(0, 5).join(", ")}${activeFiles.length > 5 ? ` (+${activeFiles.length - 5} more)` : ""}`);
14661
+ }
14662
+ if (lastTools && lastTools.length > 0) {
14663
+ const toolCounts = lastTools.reduce((acc, tool) => {
14664
+ acc[tool] = (acc[tool] || 0) + 1;
14665
+ return acc;
14666
+ }, {});
14667
+ const topTools = Object.entries(toolCounts).sort(([, a], [, b]) => b - a).slice(0, 3).map(([tool]) => tool);
14668
+ workingOn.push(`Recent tools: ${topTools.join(", ")}`);
14669
+ }
14670
+ if (workingOn.length > 0) {
14671
+ sessionLinking.previous_session_summary = workingOn.join("; ");
14672
+ }
14673
+ const relatedSessionIds = /* @__PURE__ */ new Set();
14674
+ snapshots.forEach((s) => {
14675
+ let sData;
14676
+ try {
14677
+ sData = JSON.parse(s.content || "{}");
14678
+ } catch {
14679
+ sData = {};
14680
+ }
14681
+ const sSessionId = sData.session_id || s.session_id;
14682
+ if (sSessionId && sSessionId !== prevSessionId) {
14683
+ relatedSessionIds.add(sSessionId);
14684
+ }
14685
+ });
14686
+ if (relatedSessionIds.size > 0) {
14687
+ sessionLinking.related_sessions = Array.from(relatedSessionIds);
14688
+ }
14689
+ }
13869
14690
  result.restored_context = {
13870
14691
  snapshot_id: latestSnapshot.id,
13871
14692
  captured_at: snapshotData.captured_at || latestSnapshot.created_at,
14693
+ session_linking: Object.keys(sessionLinking).length > 0 ? sessionLinking : void 0,
13872
14694
  ...snapshotData
13873
14695
  };
13874
14696
  result.is_post_compact = true;
13875
- result.post_compact_hint = "Session restored from pre-compaction snapshot. Review the 'restored_context' to continue where you left off.";
14697
+ result.post_compact_hint = prevSessionId ? `Session restored from session ${prevSessionId}. Review 'restored_context' to continue where you left off.` : "Session restored from pre-compaction snapshot. Review the 'restored_context' to continue where you left off.";
13876
14698
  } else {
13877
14699
  result.is_post_compact = true;
13878
14700
  result.post_compact_hint = "Post-compaction session started, but no snapshots found. Use context_smart to retrieve relevant context.";
13879
14701
  }
13880
14702
  } catch (err) {
13881
- console.error("[ContextStream] Failed to restore post-compact context:", err);
14703
+ logDebug(`Failed to restore post-compact context: ${err}`);
13882
14704
  result.is_post_compact = true;
13883
14705
  result.post_compact_hint = "Post-compaction session started. Snapshot restoration failed, use context_smart for context.";
13884
14706
  }
@@ -13919,10 +14741,7 @@ This does semantic search on the first message. You only need context_smart on s
13919
14741
  hint: intStatus.slack || intStatus.github ? "Integration tools are now available in the tool list." : "Connect integrations at https://contextstream.io/settings/integrations to enable Slack/GitHub tools."
13920
14742
  };
13921
14743
  } catch (error) {
13922
- console.error(
13923
- "[ContextStream] Failed to check integration status in session_init:",
13924
- error
13925
- );
14744
+ logDebug(`Failed to check integration status: ${error}`);
13926
14745
  }
13927
14746
  }
13928
14747
  result.modes = {
@@ -14017,7 +14836,7 @@ ${benefitsList}` : "",
14017
14836
  const projectId = typeof result.project_id === "string" ? result.project_id : void 0;
14018
14837
  const projectName = typeof result.project_name === "string" ? result.project_name : void 0;
14019
14838
  markProjectIndexed(folderPathForRules, { project_id: projectId, project_name: projectName }).catch(
14020
- (err) => console.error("[ContextStream] Failed to mark project as indexed:", err)
14839
+ (err) => logDebug(`Failed to mark project as indexed: ${err}`)
14021
14840
  );
14022
14841
  }
14023
14842
  if (noticeLines.length > 0) {
@@ -14133,7 +14952,7 @@ Optionally generates AI editor rules for automatic ContextStream usage.`,
14133
14952
  workspace_name: external_exports.string().optional().describe("Workspace name for reference"),
14134
14953
  create_parent_mapping: external_exports.boolean().optional().describe("Also create a parent folder mapping (e.g., /dev/maker/* -> workspace)"),
14135
14954
  generate_editor_rules: external_exports.boolean().optional().describe(
14136
- "Generate AI editor rules for Windsurf, Cursor, Cline, Kilo Code, Roo Code, Claude Code, and Aider"
14955
+ "Generate AI editor rules for Cursor, Cline, Kilo Code, Roo Code, Claude Code, and Aider"
14137
14956
  ),
14138
14957
  overwrite_existing: external_exports.boolean().optional().describe("Allow overwriting existing rule files when generating editor rules")
14139
14958
  })
@@ -14545,14 +15364,16 @@ Use this in combination with session_init(is_post_compact=true) for seamless con
14545
15364
  structuredContent: toStructured(response2)
14546
15365
  };
14547
15366
  }
14548
- const searchResult = await client.searchEvents({
15367
+ const listResult = await client.listMemoryEvents({
14549
15368
  workspace_id: workspaceId,
14550
15369
  project_id: projectId,
14551
- query: "session_snapshot",
14552
- event_types: ["session_snapshot"],
14553
- limit: input.max_snapshots || 1
15370
+ limit: 50
15371
+ // Fetch more to filter
14554
15372
  });
14555
- const events = searchResult?.data?.results || searchResult?.results || searchResult?.data || [];
15373
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
15374
+ const events = allEvents.filter(
15375
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
15376
+ ).slice(0, input.max_snapshots || 1);
14556
15377
  if (!events || events.length === 0) {
14557
15378
  return {
14558
15379
  content: [
@@ -14923,7 +15744,7 @@ Example: "What were the auth decisions?" or "What are my TypeScript preferences?
14923
15744
  "generate_rules",
14924
15745
  {
14925
15746
  title: "Generate ContextStream rules",
14926
- description: `Generate AI rule files for editors (Windsurf, Cursor, Cline, Kilo Code, Roo Code, Claude Code, Aider).
15747
+ description: `Generate AI rule files for editors (Cursor, Cline, Kilo Code, Roo Code, Claude Code, Aider).
14927
15748
  Defaults to the current project folder; no folder_path required when run from a project.
14928
15749
  Supported editors: ${getAvailableEditors().join(", ")}`,
14929
15750
  inputSchema: external_exports.object({
@@ -14931,7 +15752,6 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
14931
15752
  editors: external_exports.array(
14932
15753
  external_exports.enum([
14933
15754
  "codex",
14934
- "windsurf",
14935
15755
  "cursor",
14936
15756
  "cline",
14937
15757
  "kilo",
@@ -15020,34 +15840,70 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
15020
15840
  const globalPrompt = input.apply_global ? "Global rule update complete." : globalTargets.length > 0 ? "Apply rules globally too? Re-run with apply_global: true." : "No global rule locations are known for these editors.";
15021
15841
  let hooksResults;
15022
15842
  let hooksPrompt;
15023
- const hasClaude = editors.includes("claude");
15024
- const shouldInstallHooks = hasClaude && input.install_hooks !== false;
15843
+ const editorHookMap = {
15844
+ claude: "claude",
15845
+ cline: "cline",
15846
+ roo: "roo",
15847
+ kilo: "kilo",
15848
+ cursor: "cursor"
15849
+ };
15850
+ const hookSupportedEditors = editors.filter((e) => e in editorHookMap);
15851
+ const shouldInstallHooks = hookSupportedEditors.length > 0 && input.install_hooks !== false;
15025
15852
  if (shouldInstallHooks) {
15026
15853
  try {
15027
15854
  if (input.dry_run) {
15028
- hooksResults = [
15029
- { file: "~/.claude/hooks/contextstream-redirect.py", status: "dry run - would create" },
15030
- { file: "~/.claude/hooks/contextstream-reminder.py", status: "dry run - would create" },
15031
- { file: "~/.claude/settings.json", status: "dry run - would update" }
15032
- ];
15033
- if (input.include_pre_compact) {
15034
- hooksResults.push({ file: "~/.claude/hooks/contextstream-precompact.py", status: "dry run - would create" });
15855
+ hooksResults = [];
15856
+ for (const editor of hookSupportedEditors) {
15857
+ if (editor === "claude") {
15858
+ hooksResults.push(
15859
+ { editor, file: "~/.claude/hooks/contextstream-redirect.py", status: "dry run - would create" },
15860
+ { editor, file: "~/.claude/hooks/contextstream-reminder.py", status: "dry run - would create" },
15861
+ { editor, file: "~/.claude/settings.json", status: "dry run - would update" }
15862
+ );
15863
+ if (input.include_pre_compact) {
15864
+ hooksResults.push({ editor, file: "~/.claude/hooks/contextstream-precompact.py", status: "dry run - would create" });
15865
+ }
15866
+ } else if (editor === "cline") {
15867
+ hooksResults.push(
15868
+ { editor, file: "~/Documents/Cline/Rules/Hooks/PreToolUse", status: "dry run - would create" },
15869
+ { editor, file: "~/Documents/Cline/Rules/Hooks/UserPromptSubmit", status: "dry run - would create" }
15870
+ );
15871
+ } else if (editor === "roo") {
15872
+ hooksResults.push(
15873
+ { editor, file: "~/.roo/hooks/PreToolUse", status: "dry run - would create" },
15874
+ { editor, file: "~/.roo/hooks/UserPromptSubmit", status: "dry run - would create" }
15875
+ );
15876
+ } else if (editor === "kilo") {
15877
+ hooksResults.push(
15878
+ { editor, file: "~/.kilocode/hooks/PreToolUse", status: "dry run - would create" },
15879
+ { editor, file: "~/.kilocode/hooks/UserPromptSubmit", status: "dry run - would create" }
15880
+ );
15881
+ } else if (editor === "cursor") {
15882
+ hooksResults.push(
15883
+ { editor, file: "~/.cursor/hooks/contextstream-pretooluse.py", status: "dry run - would create" },
15884
+ { editor, file: "~/.cursor/hooks/contextstream-beforesubmit.py", status: "dry run - would create" },
15885
+ { editor, file: "~/.cursor/hooks.json", status: "dry run - would update" }
15886
+ );
15887
+ }
15035
15888
  }
15036
15889
  } else {
15037
- const hookResult = await installClaudeCodeHooks({
15038
- scope: "user",
15890
+ hooksResults = [];
15891
+ const allHookResults = await installAllEditorHooks({
15892
+ scope: "global",
15893
+ editors: hookSupportedEditors,
15039
15894
  includePreCompact: input.include_pre_compact
15040
15895
  });
15041
- hooksResults = [
15042
- ...hookResult.scripts.map((f) => ({ file: f, status: "created" })),
15043
- ...hookResult.settings.map((f) => ({ file: f, status: "updated" }))
15044
- ];
15896
+ for (const result of allHookResults) {
15897
+ for (const file of result.installed) {
15898
+ hooksResults.push({ editor: result.editor, file, status: "created" });
15899
+ }
15900
+ }
15045
15901
  }
15046
15902
  } catch (err) {
15047
15903
  hooksResults = [{ file: "hooks", status: `error: ${err.message}` }];
15048
15904
  }
15049
- } else if (hasClaude && input.install_hooks === false) {
15050
- hooksPrompt = "Hooks skipped. Claude may use default tools instead of ContextStream search.";
15905
+ } else if (hookSupportedEditors.length > 0 && input.install_hooks === false) {
15906
+ hooksPrompt = "Hooks skipped. AI may use default tools instead of ContextStream search.";
15051
15907
  }
15052
15908
  const summary = {
15053
15909
  folder: folderPath,
@@ -15069,7 +15925,7 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
15069
15925
  "generate_editor_rules",
15070
15926
  {
15071
15927
  title: "Generate editor AI rules",
15072
- description: `Generate AI rule files for editors (Windsurf, Cursor, Cline, Kilo Code, Roo Code, Claude Code, Aider).
15928
+ description: `Generate AI rule files for editors (Cursor, Cline, Kilo Code, Roo Code, Claude Code, Aider).
15073
15929
  These rules instruct the AI to automatically use ContextStream for memory and context.
15074
15930
  Supported editors: ${getAvailableEditors().join(", ")}`,
15075
15931
  inputSchema: external_exports.object({
@@ -15077,7 +15933,6 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
15077
15933
  editors: external_exports.array(
15078
15934
  external_exports.enum([
15079
15935
  "codex",
15080
- "windsurf",
15081
15936
  "cursor",
15082
15937
  "cline",
15083
15938
  "kilo",
@@ -15426,6 +16281,52 @@ This saves ~80% tokens compared to including full chat history.`,
15426
16281
  }
15427
16282
  sessionManager.addTokens(input.user_message);
15428
16283
  }
16284
+ let postCompactContext = "";
16285
+ let postCompactRestored = false;
16286
+ if (sessionManager && sessionManager.shouldRestorePostCompact() && workspaceId) {
16287
+ try {
16288
+ const listResult = await client.listMemoryEvents({
16289
+ workspace_id: workspaceId,
16290
+ project_id: projectId,
16291
+ limit: 20
16292
+ });
16293
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
16294
+ const snapshotEvent = allEvents.find(
16295
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.tags?.includes("session_snapshot")
16296
+ );
16297
+ if (snapshotEvent && snapshotEvent.content) {
16298
+ let snapshotData;
16299
+ try {
16300
+ snapshotData = JSON.parse(snapshotEvent.content);
16301
+ } catch {
16302
+ snapshotData = { conversation_summary: snapshotEvent.content };
16303
+ }
16304
+ const summary = snapshotData.conversation_summary || snapshotData.summary || "";
16305
+ const decisions = snapshotData.key_decisions || [];
16306
+ const unfinished = snapshotData.unfinished_work || snapshotData.pending_tasks || [];
16307
+ const files = snapshotData.active_files || [];
16308
+ const parts = [];
16309
+ parts.push("\u{1F4CB} [POST-COMPACTION CONTEXT RESTORED]");
16310
+ if (summary) parts.push(`Summary: ${summary}`);
16311
+ if (Array.isArray(decisions) && decisions.length > 0) {
16312
+ parts.push(`Decisions: ${decisions.slice(0, 5).join("; ")}`);
16313
+ }
16314
+ if (Array.isArray(unfinished) && unfinished.length > 0) {
16315
+ parts.push(`Unfinished: ${unfinished.slice(0, 3).join("; ")}`);
16316
+ }
16317
+ if (Array.isArray(files) && files.length > 0) {
16318
+ parts.push(`Active files: ${files.slice(0, 5).join(", ")}`);
16319
+ }
16320
+ parts.push("---");
16321
+ postCompactContext = parts.join("\n") + "\n\n";
16322
+ postCompactRestored = true;
16323
+ sessionManager.markPostCompactRestoreCompleted();
16324
+ logSystem("context restored");
16325
+ }
16326
+ } catch (err) {
16327
+ logDebug(`Failed to restore post-compact context: ${err}`);
16328
+ }
16329
+ }
15429
16330
  const result = await client.getSmartContext({
15430
16331
  user_message: input.user_message,
15431
16332
  workspace_id: workspaceId,
@@ -15466,8 +16367,44 @@ This saves ~80% tokens compared to including full chat history.`,
15466
16367
  project_id: projectId,
15467
16368
  max_tokens: input.max_tokens
15468
16369
  });
15469
- const hasLessons = result.context.includes("|L:") || result.context.includes("L:") || result.context.toLowerCase().includes("lesson");
15470
- const lessonsWarningLine = hasLessons ? "\n\n\u26A0\uFE0F [LESSONS DETECTED] Review the L: items above - these are past mistakes. STOP and review before making similar changes." : "";
16370
+ let lessonsWarningLine = "";
16371
+ const riskyKeywords = detectRiskyActions(input.user_message);
16372
+ if (riskyKeywords.length > 0 && workspaceId) {
16373
+ try {
16374
+ const lessons = await client.getHighPriorityLessons({
16375
+ workspace_id: workspaceId,
16376
+ project_id: projectId,
16377
+ context_hint: riskyKeywords.join(" "),
16378
+ limit: 5
16379
+ });
16380
+ if (lessons.length > 0) {
16381
+ const lessonLines = lessons.slice(0, 5).map((l, i) => {
16382
+ const severity = l.severity === "critical" ? "\u{1F6A8}" : l.severity === "high" ? "\u26A0\uFE0F" : "\u{1F4DD}";
16383
+ const title = l.title || "Untitled lesson";
16384
+ const prevention = l.prevention || "";
16385
+ return `${i + 1}. ${severity} ${title}${prevention ? `: ${prevention.slice(0, 100)}` : ""}`;
16386
+ });
16387
+ lessonsWarningLine = `
16388
+
16389
+ \u{1F6A8} [LESSONS_WARNING] Relevant Lessons for "${riskyKeywords.slice(0, 3).join(", ")}"
16390
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
16391
+ \u26A0\uFE0F IMPORTANT: You MUST tell the user about these lessons before proceeding.
16392
+ These are past mistakes that may be relevant to the current task.
16393
+
16394
+ ${lessonLines.join("\n")}
16395
+
16396
+ Action: Review each lesson and explain to the user how you will avoid these mistakes.
16397
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`;
16398
+ }
16399
+ } catch {
16400
+ }
16401
+ }
16402
+ if (!lessonsWarningLine) {
16403
+ const hasLessonsInContext = result.context.includes("|L:") || result.context.includes("L:") || result.context.toLowerCase().includes("lesson");
16404
+ if (hasLessonsInContext) {
16405
+ lessonsWarningLine = "\n\n\u26A0\uFE0F [LESSONS_WARNING] Lessons found in context - review the L: items above before making changes.";
16406
+ }
16407
+ }
15471
16408
  const searchRulesLine = SEARCH_RULES_REMINDER_ENABLED ? `
15472
16409
 
15473
16410
  ${SEARCH_RULES_REMINDER}` : "";
@@ -15475,12 +16412,18 @@ ${SEARCH_RULES_REMINDER}` : "";
15475
16412
  if (result.context_pressure) {
15476
16413
  const cp = result.context_pressure;
15477
16414
  if (cp.level === "critical") {
16415
+ if (sessionManager) {
16416
+ sessionManager.markHighContextPressure();
16417
+ }
15478
16418
  contextPressureWarning = `
15479
16419
 
15480
16420
  \u{1F6A8} [CONTEXT PRESSURE: CRITICAL] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
15481
16421
  Action: ${cp.suggested_action === "save_now" ? 'SAVE STATE NOW - Call session(action="capture") to preserve conversation state before compaction.' : cp.suggested_action}
15482
16422
  The conversation may compact soon. Save important decisions, insights, and progress immediately.`;
15483
16423
  } else if (cp.level === "high") {
16424
+ if (sessionManager) {
16425
+ sessionManager.markHighContextPressure();
16426
+ }
15484
16427
  contextPressureWarning = `
15485
16428
 
15486
16429
  \u26A0\uFE0F [CONTEXT PRESSURE: HIGH] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
@@ -15498,14 +16441,16 @@ ${versionWarningLine}` : "",
15498
16441
  contextPressureWarning,
15499
16442
  searchRulesLine
15500
16443
  ].filter(Boolean).join("");
16444
+ const finalContext = postCompactContext + result.context;
16445
+ const enrichedResultWithRestore = postCompactRestored ? { ...enrichedResult, post_compact_restored: true } : enrichedResult;
15501
16446
  return {
15502
16447
  content: [
15503
16448
  {
15504
16449
  type: "text",
15505
- text: result.context + footer + allWarnings
16450
+ text: finalContext + footer + allWarnings
15506
16451
  }
15507
16452
  ],
15508
- structuredContent: toStructured(enrichedResult)
16453
+ structuredContent: toStructured(enrichedResultWithRestore)
15509
16454
  };
15510
16455
  }
15511
16456
  );
@@ -16574,7 +17519,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16574
17519
  "session",
16575
17520
  {
16576
17521
  title: "Session",
16577
- description: `Session management operations. Actions: capture (save decision/insight), capture_lesson (save lesson from mistake), get_lessons (retrieve lessons), recall (natural language recall), remember (quick save), user_context (get preferences), summary (workspace summary), compress (compress chat), delta (changes since timestamp), smart_search (context-enriched search), decision_trace (trace decision provenance). Plan actions: capture_plan (save implementation plan), get_plan (retrieve plan with tasks), update_plan (modify plan), list_plans (list all plans).`,
17522
+ description: `Session management operations. Actions: capture (save decision/insight), capture_lesson (save lesson from mistake), get_lessons (retrieve lessons), recall (natural language recall), remember (quick save), user_context (get preferences), summary (workspace summary), compress (compress chat), delta (changes since timestamp), smart_search (context-enriched search), decision_trace (trace decision provenance), restore_context (restore state after compaction). Plan actions: capture_plan (save implementation plan), get_plan (retrieve plan with tasks), update_plan (modify plan), list_plans (list all plans).`,
16578
17523
  inputSchema: external_exports.object({
16579
17524
  action: external_exports.enum([
16580
17525
  "capture",
@@ -16592,7 +17537,9 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16592
17537
  "capture_plan",
16593
17538
  "get_plan",
16594
17539
  "update_plan",
16595
- "list_plans"
17540
+ "list_plans",
17541
+ // Context restore
17542
+ "restore_context"
16596
17543
  ]).describe("Action to perform"),
16597
17544
  workspace_id: external_exports.string().uuid().optional(),
16598
17545
  project_id: external_exports.string().uuid().optional(),
@@ -16614,7 +17561,8 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16614
17561
  "lesson",
16615
17562
  "warning",
16616
17563
  "frustration",
16617
- "conversation"
17564
+ "conversation",
17565
+ "session_snapshot"
16618
17566
  ]).optional().describe("Event type for capture"),
16619
17567
  importance: external_exports.enum(["low", "medium", "high", "critical"]).optional(),
16620
17568
  tags: external_exports.array(external_exports.string()).optional(),
@@ -16664,7 +17612,10 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16664
17612
  status: external_exports.enum(["draft", "active", "completed", "archived", "abandoned"]).optional().describe("Plan status"),
16665
17613
  due_at: external_exports.string().optional().describe("Due date for plan (ISO timestamp)"),
16666
17614
  source_tool: external_exports.string().optional().describe("Tool that generated this plan"),
16667
- include_tasks: external_exports.boolean().optional().describe("Include tasks when getting plan")
17615
+ include_tasks: external_exports.boolean().optional().describe("Include tasks when getting plan"),
17616
+ // Restore context params
17617
+ snapshot_id: external_exports.string().uuid().optional().describe("Specific snapshot ID to restore (defaults to most recent)"),
17618
+ max_snapshots: external_exports.number().optional().default(1).describe("Number of recent snapshots to consider (default: 1)")
16668
17619
  })
16669
17620
  },
16670
17621
  async (input) => {
@@ -16970,6 +17921,143 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16970
17921
  structuredContent: toStructured(result)
16971
17922
  };
16972
17923
  }
17924
+ case "restore_context": {
17925
+ if (!workspaceId) {
17926
+ return errorResult(
17927
+ "restore_context requires workspace_id. Call session_init first."
17928
+ );
17929
+ }
17930
+ if (input.snapshot_id) {
17931
+ const eventResult = await client.getEvent(input.snapshot_id);
17932
+ const event = eventResult?.data || eventResult;
17933
+ if (!event || !event.content) {
17934
+ return errorResult(
17935
+ `Snapshot not found: ${input.snapshot_id}. The snapshot may have been deleted or does not exist.`
17936
+ );
17937
+ }
17938
+ let snapshotData;
17939
+ try {
17940
+ snapshotData = JSON.parse(event.content);
17941
+ } catch {
17942
+ snapshotData = { conversation_summary: event.content };
17943
+ }
17944
+ const sessionId = snapshotData.session_id || event.session_id;
17945
+ const sessionLinking2 = {};
17946
+ if (sessionId) {
17947
+ sessionLinking2.previous_session_id = sessionId;
17948
+ const workingOn = [];
17949
+ const activeFiles = snapshotData.active_files;
17950
+ const lastTools = snapshotData.last_tools;
17951
+ if (activeFiles && activeFiles.length > 0) {
17952
+ workingOn.push(`Files: ${activeFiles.slice(0, 5).join(", ")}${activeFiles.length > 5 ? ` (+${activeFiles.length - 5} more)` : ""}`);
17953
+ }
17954
+ if (lastTools && lastTools.length > 0) {
17955
+ const toolCounts = lastTools.reduce((acc, tool) => {
17956
+ acc[tool] = (acc[tool] || 0) + 1;
17957
+ return acc;
17958
+ }, {});
17959
+ const topTools = Object.entries(toolCounts).sort(([, a], [, b]) => b - a).slice(0, 3).map(([tool]) => tool);
17960
+ workingOn.push(`Recent tools: ${topTools.join(", ")}`);
17961
+ }
17962
+ if (workingOn.length > 0) {
17963
+ sessionLinking2.previous_session_summary = workingOn.join("; ");
17964
+ }
17965
+ }
17966
+ const response2 = {
17967
+ restored: true,
17968
+ snapshot_id: event.id,
17969
+ captured_at: snapshotData.captured_at || event.created_at,
17970
+ session_linking: Object.keys(sessionLinking2).length > 0 ? sessionLinking2 : void 0,
17971
+ ...snapshotData,
17972
+ hint: sessionId ? `Context restored from session ${sessionId}. Continue the conversation with awareness of the above state.` : "Context restored. Continue the conversation with awareness of the above state."
17973
+ };
17974
+ return {
17975
+ content: [{ type: "text", text: formatContent(response2) }],
17976
+ structuredContent: toStructured(response2)
17977
+ };
17978
+ }
17979
+ const listResult = await client.listMemoryEvents({
17980
+ workspace_id: workspaceId,
17981
+ project_id: projectId,
17982
+ limit: 50
17983
+ // Fetch more to filter
17984
+ });
17985
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
17986
+ const snapshotEvents = allEvents.filter(
17987
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
17988
+ ).slice(0, input.max_snapshots || 1);
17989
+ if (!snapshotEvents || snapshotEvents.length === 0) {
17990
+ return {
17991
+ content: [
17992
+ {
17993
+ type: "text",
17994
+ text: formatContent({
17995
+ restored: false,
17996
+ message: "No session snapshots found. Use session_capture_smart to save state before compaction.",
17997
+ hint: "Start fresh or use session_init to get recent context."
17998
+ })
17999
+ }
18000
+ ]
18001
+ };
18002
+ }
18003
+ const snapshots = snapshotEvents.map((event) => {
18004
+ let snapshotData;
18005
+ try {
18006
+ snapshotData = JSON.parse(event.content || "{}");
18007
+ } catch {
18008
+ snapshotData = { conversation_summary: event.content };
18009
+ }
18010
+ return {
18011
+ snapshot_id: event.id,
18012
+ captured_at: snapshotData.captured_at || event.created_at,
18013
+ session_id: snapshotData.session_id || event.session_id,
18014
+ ...snapshotData
18015
+ };
18016
+ });
18017
+ const latestSnapshot = snapshots[0];
18018
+ const sessionLinking = {};
18019
+ if (latestSnapshot?.session_id) {
18020
+ sessionLinking.previous_session_id = latestSnapshot.session_id;
18021
+ const workingOn = [];
18022
+ const activeFiles = latestSnapshot.active_files;
18023
+ const lastTools = latestSnapshot.last_tools;
18024
+ if (activeFiles && activeFiles.length > 0) {
18025
+ workingOn.push(`Files: ${activeFiles.slice(0, 5).join(", ")}${activeFiles.length > 5 ? ` (+${activeFiles.length - 5} more)` : ""}`);
18026
+ }
18027
+ if (lastTools && lastTools.length > 0) {
18028
+ const toolCounts = lastTools.reduce((acc, tool) => {
18029
+ acc[tool] = (acc[tool] || 0) + 1;
18030
+ return acc;
18031
+ }, {});
18032
+ const topTools = Object.entries(toolCounts).sort(([, a], [, b]) => b - a).slice(0, 3).map(([tool]) => tool);
18033
+ workingOn.push(`Recent tools: ${topTools.join(", ")}`);
18034
+ }
18035
+ if (workingOn.length > 0) {
18036
+ sessionLinking.previous_session_summary = workingOn.join("; ");
18037
+ }
18038
+ const relatedSessionIds = /* @__PURE__ */ new Set();
18039
+ snapshots.forEach((s) => {
18040
+ if (s.session_id && s.session_id !== latestSnapshot.session_id) {
18041
+ relatedSessionIds.add(s.session_id);
18042
+ }
18043
+ });
18044
+ if (relatedSessionIds.size > 0) {
18045
+ sessionLinking.related_sessions = Array.from(relatedSessionIds);
18046
+ }
18047
+ }
18048
+ const response = {
18049
+ restored: true,
18050
+ snapshots_found: snapshots.length,
18051
+ latest: snapshots[0],
18052
+ all_snapshots: snapshots.length > 1 ? snapshots : void 0,
18053
+ session_linking: Object.keys(sessionLinking).length > 0 ? sessionLinking : void 0,
18054
+ hint: sessionLinking.previous_session_id ? `Context restored from session ${sessionLinking.previous_session_id}. Continue the conversation with awareness of the above state.` : "Context restored. Continue the conversation with awareness of the above state."
18055
+ };
18056
+ return {
18057
+ content: [{ type: "text", text: formatContent(response) }],
18058
+ structuredContent: toStructured(response)
18059
+ };
18060
+ }
16973
18061
  default:
16974
18062
  return errorResult(`Unknown action: ${input.action}`);
16975
18063
  }
@@ -18673,9 +19761,7 @@ Each domain tool has an 'action' parameter for specific operations.` : "";
18673
19761
  }
18674
19762
  }
18675
19763
  );
18676
- console.error(
18677
- `[ContextStream] Consolidated mode: Registered ${CONSOLIDATED_TOOLS.size} domain tools.`
18678
- );
19764
+ logDebug(`Consolidated mode: ${CONSOLIDATED_TOOLS.size} domain tools`);
18679
19765
  }
18680
19766
  }
18681
19767
  function registerLimitedTools(server) {
@@ -18710,7 +19796,7 @@ After setup, restart your editor to enable all ContextStream tools.`
18710
19796
  };
18711
19797
  }
18712
19798
  );
18713
- console.error("[ContextStream] Limited mode: Registered setup helper tool only.");
19799
+ logSystem("limited mode (run setup)");
18714
19800
  }
18715
19801
 
18716
19802
  // src/resources.ts
@@ -19492,7 +20578,7 @@ function registerPrompts(server) {
19492
20578
  "First:",
19493
20579
  "- If you can infer the project folder path from the environment/IDE roots, use it.",
19494
20580
  "- Otherwise ask me for an absolute folder path.",
19495
- "- Ask which editor(s) (codex,windsurf,cursor,cline,kilo,roo,claude,aider,antigravity) or default to all.",
20581
+ "- Ask which editor(s) (codex,cursor,cline,kilo,roo,claude,aider,antigravity) or default to all.",
19496
20582
  "",
19497
20583
  "Then call `generate_rules` and confirm which files were created/updated.",
19498
20584
  "Ask if the user also wants to apply rules globally (pass apply_global: true)."
@@ -19534,8 +20620,7 @@ function registerPrompts(server) {
19534
20620
  }
19535
20621
 
19536
20622
  // src/session-manager.ts
19537
- var SessionManager = class {
19538
- // Conservative default for 100k context window
20623
+ var SessionManager = class _SessionManager {
19539
20624
  constructor(server, client) {
19540
20625
  this.server = server;
19541
20626
  this.client = client;
@@ -19547,8 +20632,30 @@ var SessionManager = class {
19547
20632
  this.contextSmartCalled = false;
19548
20633
  this.warningShown = false;
19549
20634
  // Token tracking for context pressure calculation
20635
+ // Note: MCP servers cannot see actual token usage (AI responses, thinking, system prompts).
20636
+ // We use a heuristic: tracked tokens + (turns * estimated tokens per turn)
19550
20637
  this.sessionTokens = 0;
19551
20638
  this.contextThreshold = 7e4;
20639
+ // Conservative default for 100k context window
20640
+ this.conversationTurns = 0;
20641
+ // Continuous checkpointing
20642
+ this.toolCallCount = 0;
20643
+ this.checkpointInterval = 20;
20644
+ // Save checkpoint every N tool calls
20645
+ this.lastCheckpointAt = 0;
20646
+ this.activeFiles = /* @__PURE__ */ new Set();
20647
+ this.recentToolCalls = [];
20648
+ this.checkpointEnabled = process.env.CONTEXTSTREAM_CHECKPOINT_ENABLED?.toLowerCase() === "true";
20649
+ // Post-compaction restoration tracking
20650
+ // Tracks when context pressure was high/critical so we can detect post-compaction state
20651
+ this.lastHighPressureAt = null;
20652
+ this.lastHighPressureTokens = 0;
20653
+ this.postCompactRestoreCompleted = false;
20654
+ }
20655
+ static {
20656
+ // Each conversation turn typically includes: user message (~500), AI response (~1500),
20657
+ // system prompt overhead (~500), and reasoning (~1500). Conservative estimate: 3000/turn
20658
+ this.TOKENS_PER_TURN_ESTIMATE = 3e3;
19552
20659
  }
19553
20660
  /**
19554
20661
  * Check if session has been auto-initialized
@@ -19591,17 +20698,40 @@ var SessionManager = class {
19591
20698
  this.folderPath = path9;
19592
20699
  }
19593
20700
  /**
19594
- * Mark that context_smart has been called in this session
20701
+ * Mark that context_smart has been called in this session.
20702
+ * Also increments the conversation turn counter for token estimation.
19595
20703
  */
19596
20704
  markContextSmartCalled() {
19597
20705
  this.contextSmartCalled = true;
20706
+ this.conversationTurns++;
19598
20707
  }
19599
20708
  /**
19600
20709
  * Get current session token count for context pressure calculation.
20710
+ *
20711
+ * This returns an ESTIMATED count based on:
20712
+ * 1. Tokens tracked through ContextStream tools (actual)
20713
+ * 2. Estimated tokens per conversation turn (heuristic)
20714
+ *
20715
+ * Note: MCP servers cannot see actual AI token usage (responses, thinking,
20716
+ * system prompts). This estimate helps provide a more realistic context
20717
+ * pressure signal.
19601
20718
  */
19602
20719
  getSessionTokens() {
20720
+ const turnEstimate = this.conversationTurns * _SessionManager.TOKENS_PER_TURN_ESTIMATE;
20721
+ return this.sessionTokens + turnEstimate;
20722
+ }
20723
+ /**
20724
+ * Get the raw tracked tokens (without turn-based estimation).
20725
+ */
20726
+ getRawTrackedTokens() {
19603
20727
  return this.sessionTokens;
19604
20728
  }
20729
+ /**
20730
+ * Get the current conversation turn count.
20731
+ */
20732
+ getConversationTurns() {
20733
+ return this.conversationTurns;
20734
+ }
19605
20735
  /**
19606
20736
  * Get the context threshold (max tokens before compaction warning).
19607
20737
  */
@@ -19640,6 +20770,52 @@ var SessionManager = class {
19640
20770
  */
19641
20771
  resetTokenCount() {
19642
20772
  this.sessionTokens = 0;
20773
+ this.conversationTurns = 0;
20774
+ }
20775
+ /**
20776
+ * Record that context pressure is high/critical.
20777
+ * Called when context_smart returns high or critical pressure level.
20778
+ */
20779
+ markHighContextPressure() {
20780
+ this.lastHighPressureAt = Date.now();
20781
+ this.lastHighPressureTokens = this.getSessionTokens();
20782
+ }
20783
+ /**
20784
+ * Check if we should attempt post-compaction restoration.
20785
+ *
20786
+ * Detection heuristic:
20787
+ * 1. We recorded high/critical context pressure recently (within 10 minutes)
20788
+ * 2. Current token count is very low (< 5000) compared to when pressure was high
20789
+ * 3. We haven't already restored in this session
20790
+ *
20791
+ * This indicates compaction likely happened and we should restore context.
20792
+ */
20793
+ shouldRestorePostCompact() {
20794
+ if (this.postCompactRestoreCompleted) {
20795
+ return false;
20796
+ }
20797
+ if (!this.lastHighPressureAt) {
20798
+ return false;
20799
+ }
20800
+ const elapsed = Date.now() - this.lastHighPressureAt;
20801
+ if (elapsed > 10 * 60 * 1e3) {
20802
+ return false;
20803
+ }
20804
+ const currentTokens = this.getSessionTokens();
20805
+ const tokenDrop = this.lastHighPressureTokens - currentTokens;
20806
+ if (currentTokens > 1e4 || tokenDrop < this.lastHighPressureTokens * 0.5) {
20807
+ return false;
20808
+ }
20809
+ return true;
20810
+ }
20811
+ /**
20812
+ * Mark post-compaction restoration as completed.
20813
+ * Prevents multiple restoration attempts in the same session.
20814
+ */
20815
+ markPostCompactRestoreCompleted() {
20816
+ this.postCompactRestoreCompleted = true;
20817
+ this.lastHighPressureAt = null;
20818
+ this.lastHighPressureTokens = 0;
19643
20819
  }
19644
20820
  /**
19645
20821
  * Check if context_smart has been called and warn if not.
@@ -19905,6 +21081,112 @@ var SessionManager = class {
19905
21081
  parts.push("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
19906
21082
  return parts.join("\n");
19907
21083
  }
21084
+ // =========================================================================
21085
+ // Continuous Checkpointing
21086
+ // =========================================================================
21087
+ /**
21088
+ * Track a tool call for checkpointing purposes.
21089
+ * Call this after each tool execution to track files and trigger periodic checkpoints.
21090
+ */
21091
+ trackToolCall(toolName, input) {
21092
+ this.toolCallCount++;
21093
+ this.recentToolCalls.push({ name: toolName, timestamp: Date.now() });
21094
+ if (this.recentToolCalls.length > 50) {
21095
+ this.recentToolCalls = this.recentToolCalls.slice(-50);
21096
+ }
21097
+ if (input) {
21098
+ const filePath = input.file_path || input.notebook_path || input.path;
21099
+ if (filePath && typeof filePath === "string") {
21100
+ this.activeFiles.add(filePath);
21101
+ if (this.activeFiles.size > 30) {
21102
+ const arr = Array.from(this.activeFiles);
21103
+ this.activeFiles = new Set(arr.slice(-30));
21104
+ }
21105
+ }
21106
+ }
21107
+ this.maybeCheckpoint();
21108
+ }
21109
+ /**
21110
+ * Save a checkpoint if the interval has been reached.
21111
+ */
21112
+ async maybeCheckpoint() {
21113
+ if (!this.checkpointEnabled || !this.initialized || !this.context) {
21114
+ return;
21115
+ }
21116
+ const callsSinceLastCheckpoint = this.toolCallCount - this.lastCheckpointAt;
21117
+ if (callsSinceLastCheckpoint < this.checkpointInterval) {
21118
+ return;
21119
+ }
21120
+ this.lastCheckpointAt = this.toolCallCount;
21121
+ await this.saveCheckpoint("periodic");
21122
+ }
21123
+ /**
21124
+ * Get the list of active files being worked on.
21125
+ */
21126
+ getActiveFiles() {
21127
+ return Array.from(this.activeFiles);
21128
+ }
21129
+ /**
21130
+ * Get recent tool call names.
21131
+ */
21132
+ getRecentToolNames() {
21133
+ return this.recentToolCalls.map((t) => t.name);
21134
+ }
21135
+ /**
21136
+ * Get the current tool call count.
21137
+ */
21138
+ getToolCallCount() {
21139
+ return this.toolCallCount;
21140
+ }
21141
+ /**
21142
+ * Save a checkpoint snapshot to ContextStream.
21143
+ */
21144
+ async saveCheckpoint(trigger) {
21145
+ if (!this.initialized || !this.context) {
21146
+ return false;
21147
+ }
21148
+ const workspaceId = this.context.workspace_id;
21149
+ if (!workspaceId) {
21150
+ return false;
21151
+ }
21152
+ const checkpointData = {
21153
+ trigger,
21154
+ checkpoint_number: Math.floor(this.toolCallCount / this.checkpointInterval),
21155
+ tool_call_count: this.toolCallCount,
21156
+ session_tokens: this.sessionTokens,
21157
+ active_files: this.getActiveFiles(),
21158
+ recent_tools: this.getRecentToolNames().slice(-10),
21159
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
21160
+ auto_captured: true
21161
+ };
21162
+ try {
21163
+ await this.client.captureContext({
21164
+ workspace_id: workspaceId,
21165
+ project_id: this.context.project_id,
21166
+ event_type: "session_snapshot",
21167
+ title: `Checkpoint #${checkpointData.checkpoint_number} (${trigger})`,
21168
+ content: JSON.stringify(checkpointData),
21169
+ importance: trigger === "periodic" ? "low" : "medium",
21170
+ tags: ["session_snapshot", "checkpoint", trigger]
21171
+ });
21172
+ return true;
21173
+ } catch (err) {
21174
+ console.error("[ContextStream] Failed to save checkpoint:", err);
21175
+ return false;
21176
+ }
21177
+ }
21178
+ /**
21179
+ * Enable or disable continuous checkpointing.
21180
+ */
21181
+ setCheckpointEnabled(enabled) {
21182
+ this.checkpointEnabled = enabled;
21183
+ }
21184
+ /**
21185
+ * Set the checkpoint interval (tool calls between checkpoints).
21186
+ */
21187
+ setCheckpointInterval(interval) {
21188
+ this.checkpointInterval = Math.max(5, interval);
21189
+ }
19908
21190
  };
19909
21191
 
19910
21192
  // src/http-gateway.ts
@@ -20268,7 +21550,6 @@ var EDITOR_LABELS = {
20268
21550
  codex: "Codex CLI",
20269
21551
  claude: "Claude Code",
20270
21552
  cursor: "Cursor / VS Code",
20271
- windsurf: "Windsurf",
20272
21553
  cline: "Cline",
20273
21554
  kilo: "Kilo Code",
20274
21555
  roo: "Roo Code",
@@ -20347,7 +21628,6 @@ var CONTEXTSTREAM_PREAMBLE_PATTERNS2 = [
20347
21628
  /^#\s+codex cli instructions$/i,
20348
21629
  /^#\s+claude code instructions$/i,
20349
21630
  /^#\s+cursor rules$/i,
20350
- /^#\s+windsurf rules$/i,
20351
21631
  /^#\s+cline rules$/i,
20352
21632
  /^#\s+kilo code rules$/i,
20353
21633
  /^#\s+roo code rules$/i,
@@ -20489,8 +21769,6 @@ function globalRulesPathForEditor(editor) {
20489
21769
  return path8.join(home, ".codex", "AGENTS.md");
20490
21770
  case "claude":
20491
21771
  return path8.join(home, ".claude", "CLAUDE.md");
20492
- case "windsurf":
20493
- return path8.join(home, ".codeium", "windsurf", "memories", "global_rules.md");
20494
21772
  case "cline":
20495
21773
  return path8.join(home, "Documents", "Cline", "Rules", "contextstream.md");
20496
21774
  case "kilo":
@@ -20537,25 +21815,6 @@ async function isClaudeInstalled() {
20537
21815
  }
20538
21816
  return anyPathExists(candidates);
20539
21817
  }
20540
- async function isWindsurfInstalled() {
20541
- const home = homedir5();
20542
- const candidates = [
20543
- path8.join(home, ".codeium"),
20544
- path8.join(home, ".codeium", "windsurf"),
20545
- path8.join(home, ".config", "codeium")
20546
- ];
20547
- if (process.platform === "darwin") {
20548
- candidates.push(path8.join(home, "Library", "Application Support", "Windsurf"));
20549
- candidates.push(path8.join(home, "Library", "Application Support", "Codeium"));
20550
- } else if (process.platform === "win32") {
20551
- const appData = process.env.APPDATA;
20552
- if (appData) {
20553
- candidates.push(path8.join(appData, "Windsurf"));
20554
- candidates.push(path8.join(appData, "Codeium"));
20555
- }
20556
- }
20557
- return anyPathExists(candidates);
20558
- }
20559
21818
  async function isClineInstalled() {
20560
21819
  const home = homedir5();
20561
21820
  const candidates = [
@@ -20634,8 +21893,6 @@ async function isEditorInstalled(editor) {
20634
21893
  return isClaudeInstalled();
20635
21894
  case "cursor":
20636
21895
  return isCursorInstalled();
20637
- case "windsurf":
20638
- return isWindsurfInstalled();
20639
21896
  case "cline":
20640
21897
  return isClineInstalled();
20641
21898
  case "kilo":
@@ -20660,6 +21917,9 @@ function buildContextStreamMcpServer(params) {
20660
21917
  env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
20661
21918
  }
20662
21919
  env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
21920
+ if (params.restoreContextEnabled === false) {
21921
+ env.CONTEXTSTREAM_RESTORE_CONTEXT = "false";
21922
+ }
20663
21923
  if (params.showTiming) {
20664
21924
  env.CONTEXTSTREAM_SHOW_TIMING = "true";
20665
21925
  }
@@ -20685,6 +21945,9 @@ function buildContextStreamVsCodeServer(params) {
20685
21945
  env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
20686
21946
  }
20687
21947
  env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
21948
+ if (params.restoreContextEnabled === false) {
21949
+ env.CONTEXTSTREAM_RESTORE_CONTEXT = "false";
21950
+ }
20688
21951
  if (params.showTiming) {
20689
21952
  env.CONTEXTSTREAM_SHOW_TIMING = "true";
20690
21953
  }
@@ -20788,6 +22051,8 @@ async function upsertCodexTomlConfig(filePath, params) {
20788
22051
  ` : "";
20789
22052
  const contextPackLine = `CONTEXTSTREAM_CONTEXT_PACK = "${params.contextPackEnabled === false ? "false" : "true"}"
20790
22053
  `;
22054
+ const restoreContextLine = params.restoreContextEnabled === false ? `CONTEXTSTREAM_RESTORE_CONTEXT = "false"
22055
+ ` : "";
20791
22056
  const showTimingLine = params.showTiming ? `CONTEXTSTREAM_SHOW_TIMING = "true"
20792
22057
  ` : "";
20793
22058
  const commandLine = IS_WINDOWS ? `command = "cmd"
@@ -20803,7 +22068,7 @@ args = ["-y", "@contextstream/mcp-server"]
20803
22068
  [mcp_servers.contextstream.env]
20804
22069
  CONTEXTSTREAM_API_URL = "${params.apiUrl}"
20805
22070
  CONTEXTSTREAM_API_KEY = "${params.apiKey}"
20806
- ` + toolsetLine + contextPackLine + showTimingLine;
22071
+ ` + toolsetLine + contextPackLine + restoreContextLine + showTimingLine;
20807
22072
  if (!exists) {
20808
22073
  await fs7.writeFile(filePath, block.trimStart(), "utf8");
20809
22074
  return "created";
@@ -20933,6 +22198,26 @@ async function runSetupWizard(args) {
20933
22198
  console.log("This configures ContextStream MCP + rules for your AI editor(s).");
20934
22199
  if (dryRun) console.log("DRY RUN: no files will be written.\n");
20935
22200
  else console.log("");
22201
+ const versionNotice = await getUpdateNotice();
22202
+ if (versionNotice?.behind) {
22203
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
22204
+ console.log(`\u26A0\uFE0F You're running an outdated version (v${versionNotice.current})`);
22205
+ console.log(` Latest version is v${versionNotice.latest}`);
22206
+ console.log("");
22207
+ console.log(" To use the latest version, exit and run:");
22208
+ console.log(" npx -y @contextstream/mcp-server@latest setup");
22209
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
22210
+ console.log("");
22211
+ const continueAnyway = normalizeInput(
22212
+ await rl.question("Continue with current version anyway? [y/N]: ")
22213
+ ).toLowerCase();
22214
+ if (continueAnyway !== "y" && continueAnyway !== "yes") {
22215
+ console.log("\nExiting. Run the command above to use the latest version.");
22216
+ rl.close();
22217
+ return;
22218
+ }
22219
+ console.log("");
22220
+ }
20936
22221
  const savedCreds = await readSavedCredentials();
20937
22222
  const apiUrlDefault = normalizeApiUrl(
20938
22223
  process.env.CONTEXTSTREAM_API_URL || savedCreds?.api_url || "https://api.contextstream.io"
@@ -21125,11 +22410,7 @@ Code: ${device.user_code}`);
21125
22410
  }
21126
22411
  }
21127
22412
  }
21128
- console.log("Rules detail level (in the generated rules file):\n");
21129
- console.log(" 1) Standard (recommended) \u2014 concise, high-signal (lower token overhead)");
21130
- console.log(" 2) Enhanced \u2014 more guidance + examples (higher token overhead)");
21131
- const modeChoice = normalizeInput(await rl.question("Choose [1/2] (default 1): ")) || "1";
21132
- const mode = modeChoice === "2" ? "full" : "minimal";
22413
+ const mode = "full";
21133
22414
  const detectedPlanName = await client.getPlanName();
21134
22415
  const detectedGraphTier = await client.getGraphTier();
21135
22416
  const graphTierLabel = detectedGraphTier === "full" ? "full graph" : detectedGraphTier === "lite" ? "graph-lite" : "none";
@@ -21165,11 +22446,16 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21165
22446
  console.log(" Useful for debugging performance; disabled by default.");
21166
22447
  const showTimingChoice = normalizeInput(await rl.question("Show response timing? [y/N]: "));
21167
22448
  const showTiming = showTimingChoice.toLowerCase() === "y" || showTimingChoice.toLowerCase() === "yes";
22449
+ console.log("\nAutomatic Context Restoration:");
22450
+ console.log(" Automatically restore context from recent snapshots on every session_init.");
22451
+ console.log(" This enables seamless continuation across conversations and after compaction.");
22452
+ console.log(" Enabled by default; disable if you prefer explicit control.");
22453
+ const restoreContextChoice = normalizeInput(await rl.question("Enable automatic context restoration? [Y/n]: "));
22454
+ const restoreContextEnabled = !(restoreContextChoice.toLowerCase() === "n" || restoreContextChoice.toLowerCase() === "no");
21168
22455
  const editors = [
21169
22456
  "codex",
21170
22457
  "claude",
21171
22458
  "cursor",
21172
- "windsurf",
21173
22459
  "cline",
21174
22460
  "kilo",
21175
22461
  "roo",
@@ -21215,7 +22501,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21215
22501
  console.log(" 1) Global");
21216
22502
  console.log(" 2) Project");
21217
22503
  console.log(" 3) Both");
21218
- const scopeChoice = normalizeInput(await rl.question("Choose [1/2/3] (default 3): ")) || "3";
22504
+ const scopeChoice = normalizeInput(await rl.question("Choose [1/2/3] (default 2): ")) || "2";
21219
22505
  const scope = scopeChoice === "1" ? "global" : scopeChoice === "2" ? "project" : "both";
21220
22506
  console.log("\nInstall MCP server config as:");
21221
22507
  if (hasCodex && !hasProjectMcpEditors) {
@@ -21232,27 +22518,29 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21232
22518
  );
21233
22519
  }
21234
22520
  }
21235
- const mcpChoiceDefault = hasCodex && !hasProjectMcpEditors ? "1" : "3";
22521
+ const mcpChoiceDefault = hasCodex && !hasProjectMcpEditors ? "1" : "2";
21236
22522
  const mcpChoice = normalizeInput(
21237
22523
  await rl.question(
21238
22524
  `Choose [${hasCodex && !hasProjectMcpEditors ? "1/2" : "1/2/3/4"}] (default ${mcpChoiceDefault}): `
21239
22525
  )
21240
22526
  ) || mcpChoiceDefault;
21241
22527
  const mcpScope = mcpChoice === "2" && hasCodex && !hasProjectMcpEditors ? "skip" : mcpChoice === "4" ? "skip" : mcpChoice === "1" ? "global" : mcpChoice === "2" ? "project" : "both";
21242
- const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, toolset, contextPackEnabled, showTiming });
22528
+ const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, toolset, contextPackEnabled, showTiming, restoreContextEnabled });
21243
22529
  const mcpServerClaude = buildContextStreamMcpServer({
21244
22530
  apiUrl,
21245
22531
  apiKey,
21246
22532
  toolset,
21247
22533
  contextPackEnabled,
21248
- showTiming
22534
+ showTiming,
22535
+ restoreContextEnabled
21249
22536
  });
21250
22537
  const vsCodeServer = buildContextStreamVsCodeServer({
21251
22538
  apiUrl,
21252
22539
  apiKey,
21253
22540
  toolset,
21254
22541
  contextPackEnabled,
21255
- showTiming
22542
+ showTiming,
22543
+ restoreContextEnabled
21256
22544
  });
21257
22545
  const needsGlobalMcpConfig = mcpScope === "global" || mcpScope === "both" || mcpScope === "project" && hasCodex;
21258
22546
  if (needsGlobalMcpConfig) {
@@ -21272,24 +22560,13 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21272
22560
  apiKey,
21273
22561
  toolset,
21274
22562
  contextPackEnabled,
21275
- showTiming
22563
+ showTiming,
22564
+ restoreContextEnabled
21276
22565
  });
21277
22566
  writeActions.push({ kind: "mcp-config", target: filePath, status });
21278
22567
  console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
21279
22568
  continue;
21280
22569
  }
21281
- if (editor === "windsurf") {
21282
- const filePath = path8.join(homedir5(), ".codeium", "windsurf", "mcp_config.json");
21283
- if (dryRun) {
21284
- writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
21285
- console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
21286
- continue;
21287
- }
21288
- const status = await upsertJsonMcpConfig(filePath, mcpServer);
21289
- writeActions.push({ kind: "mcp-config", target: filePath, status });
21290
- console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
21291
- continue;
21292
- }
21293
22570
  if (editor === "claude") {
21294
22571
  const desktopPath = claudeDesktopConfigPath();
21295
22572
  if (desktopPath) {
@@ -21354,53 +22631,68 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21354
22631
  }
21355
22632
  }
21356
22633
  }
21357
- if (configuredEditors.includes("claude")) {
22634
+ const HOOKS_SUPPORTED_EDITORS = {
22635
+ claude: "claude",
22636
+ cursor: "cursor",
22637
+ cline: "cline",
22638
+ roo: "roo",
22639
+ kilo: "kilo",
22640
+ codex: null,
22641
+ // No hooks API
22642
+ aider: null,
22643
+ // No hooks API
22644
+ antigravity: null
22645
+ // No hooks API
22646
+ };
22647
+ const hookEligibleEditors = configuredEditors.filter(
22648
+ (e) => HOOKS_SUPPORTED_EDITORS[e] !== null
22649
+ );
22650
+ if (hookEligibleEditors.length > 0) {
21358
22651
  console.log("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
21359
- console.log("\u2502 Claude Code Hooks (Recommended) \u2502");
22652
+ console.log("\u2502 AI Editor Hooks (Recommended) \u2502");
21360
22653
  console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
21361
22654
  console.log("");
21362
- console.log(" Problem: Claude Code often ignores CLAUDE.md instructions and uses");
21363
- console.log(" its default tools (Grep/Glob/Search) instead of ContextStream search.");
21364
- console.log(" This happens because instructions decay over long conversations.");
22655
+ console.log(" Problem: AI editors often use their default tools (Grep/Glob/Search)");
22656
+ console.log(" instead of ContextStream smart search. Instructions decay over long chats.");
21365
22657
  console.log("");
21366
22658
  console.log(" Solution: Install hooks that:");
21367
- console.log(" \u2713 Block default search tools (Grep/Glob/Search) \u2192 redirect to ContextStream");
21368
- console.log(" \u2713 Block built-in plan mode \u2192 redirect to ContextStream plans (persistent)");
21369
- console.log(" \u2713 Inject reminders on every message to keep rules in context");
21370
- console.log(" \u2713 Result: Faster searches, persistent plans across sessions");
22659
+ console.log(" \u2713 Use ContextStream (indexed, faster) with default tool use");
22660
+ console.log(" \u2713 Use ContextStream plans (persistent) with default tool use");
22661
+ console.log(" \u2713 Inject reminders to keep rules in context");
22662
+ console.log("");
22663
+ console.log(` Hooks available for: ${hookEligibleEditors.map((e) => EDITOR_LABELS[e]).join(", ")}`);
21371
22664
  console.log("");
21372
22665
  console.log(" You can disable hooks anytime with CONTEXTSTREAM_HOOK_ENABLED=false");
21373
22666
  console.log("");
21374
22667
  const installHooks = normalizeInput(
21375
- await rl.question("Install Claude Code hooks? [Y/n] (recommended): ")
22668
+ await rl.question("Install editor hooks? [Y/n] (recommended): ")
21376
22669
  ).toLowerCase();
21377
22670
  if (installHooks !== "n" && installHooks !== "no") {
21378
- try {
21379
- if (dryRun) {
21380
- console.log("- Would install hooks to ~/.claude/hooks/");
21381
- console.log("- Would update ~/.claude/settings.json");
21382
- writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "hooks", "contextstream-redirect.py"), status: "dry-run" });
21383
- writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "hooks", "contextstream-reminder.py"), status: "dry-run" });
21384
- writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "settings.json"), status: "dry-run" });
21385
- } else {
21386
- const result = await installClaudeCodeHooks({ scope: "user" });
21387
- result.scripts.forEach((script) => {
21388
- writeActions.push({ kind: "mcp-config", target: script, status: "created" });
21389
- console.log(`- Created hook: ${script}`);
21390
- });
21391
- result.settings.forEach((settings) => {
21392
- writeActions.push({ kind: "mcp-config", target: settings, status: "updated" });
21393
- console.log(`- Updated settings: ${settings}`);
22671
+ for (const editor of hookEligibleEditors) {
22672
+ const hookEditor = HOOKS_SUPPORTED_EDITORS[editor];
22673
+ if (!hookEditor) continue;
22674
+ try {
22675
+ if (dryRun) {
22676
+ console.log(`- ${EDITOR_LABELS[editor]}: would install hooks`);
22677
+ continue;
22678
+ }
22679
+ const result = await installEditorHooks({
22680
+ editor: hookEditor,
22681
+ scope: "global"
21394
22682
  });
22683
+ for (const script of result.installed) {
22684
+ writeActions.push({ kind: "hooks", target: script, status: "created" });
22685
+ console.log(`- ${EDITOR_LABELS[editor]}: installed ${path8.basename(script)}`);
22686
+ }
22687
+ } catch (err) {
22688
+ const message = err instanceof Error ? err.message : String(err);
22689
+ console.log(`- ${EDITOR_LABELS[editor]}: failed to install hooks: ${message}`);
21395
22690
  }
21396
- console.log(" Hooks installed. Disable with CONTEXTSTREAM_HOOK_ENABLED=false");
21397
- } catch (err) {
21398
- const message = err instanceof Error ? err.message : String(err);
21399
- console.log(`- Failed to install hooks: ${message}`);
21400
22691
  }
22692
+ console.log(" Hooks installed. Disable with CONTEXTSTREAM_HOOK_ENABLED=false");
21401
22693
  } else {
21402
22694
  console.log("- Skipped hooks installation.");
21403
- console.log(" Note: Without hooks, Claude may still use default tools instead of ContextStream.");
22695
+ console.log(" Note: Without hooks, AI may still use default tools instead of ContextStream.");
21404
22696
  }
21405
22697
  }
21406
22698
  console.log("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
@@ -21729,11 +23021,7 @@ Applying to ${projects.length} project(s)...`);
21729
23021
  "- Toggle Response Timing with CONTEXTSTREAM_SHOW_TIMING=true|false."
21730
23022
  );
21731
23023
  console.log("");
21732
- console.log("You're set up! Now try these prompts in your AI tool:");
21733
- console.log(' 1) "session summary"');
21734
- console.log(` 2) "remember we're using PostgreSQL"`);
21735
- console.log(' 3) "what did we decide about auth?"');
21736
- console.log("");
23024
+ console.log("You're all set! ContextStream gives your AI persistent memory, semantic code search, and cross-session context.");
21737
23025
  console.log("More at: https://contextstream.io/docs/mcp");
21738
23026
  } finally {
21739
23027
  rl.close();
@@ -21883,13 +23171,21 @@ async function main() {
21883
23171
  if (ENABLE_PROMPTS2) {
21884
23172
  registerPrompts(server);
21885
23173
  }
21886
- console.error(`ContextStream MCP server v${VERSION} starting...`);
21887
- console.error(`API URL: ${config.apiUrl}`);
21888
- console.error(`Auth: ${config.apiKey ? "API Key" : config.jwt ? "JWT" : "None"}`);
21889
- console.error(`Auto-Context: ENABLED (context loads on first tool call)`);
23174
+ const logLevel = (process.env.CONTEXTSTREAM_LOG_LEVEL || "normal").toLowerCase();
23175
+ const logQuiet = logLevel === "quiet";
23176
+ const logVerbose = logLevel === "verbose";
23177
+ if (!logQuiet) {
23178
+ console.error(`\u2501\u2501\u2501 ContextStream v${VERSION} \u2501\u2501\u2501`);
23179
+ }
23180
+ if (logVerbose) {
23181
+ console.error(` API: ${config.apiUrl}`);
23182
+ console.error(` Auth: ${config.apiKey ? "API Key" : config.jwt ? "JWT" : "None"}`);
23183
+ }
21890
23184
  const transport = new StdioServerTransport();
21891
23185
  await server.connect(transport);
21892
- console.error("ContextStream MCP server connected and ready");
23186
+ if (!logQuiet) {
23187
+ console.error("\u2713 ready");
23188
+ }
21893
23189
  showFirstRunMessage();
21894
23190
  checkForUpdates().catch(() => {
21895
23191
  });