@contextstream/mcp-server 0.4.38 → 0.4.39

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 +1662 -212
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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
 
@@ -9805,7 +9890,7 @@ var PRECOMPACT_HOOK_SCRIPT = `#!/usr/bin/env python3
9805
9890
  ContextStream PreCompact Hook for Claude Code
9806
9891
 
9807
9892
  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.
9893
+ Automatically saves conversation state to ContextStream by parsing the transcript.
9809
9894
 
9810
9895
  Input (via stdin):
9811
9896
  {
@@ -9821,7 +9906,7 @@ Output (to stdout):
9821
9906
  {
9822
9907
  "hookSpecificOutput": {
9823
9908
  "hookEventName": "PreCompact",
9824
- "additionalContext": "... instructions for AI ..."
9909
+ "additionalContext": "... status message ..."
9825
9910
  }
9826
9911
  }
9827
9912
  """
@@ -9829,8 +9914,149 @@ Output (to stdout):
9829
9914
  import json
9830
9915
  import sys
9831
9916
  import os
9917
+ import re
9918
+ import urllib.request
9919
+ import urllib.error
9832
9920
 
9833
9921
  ENABLED = os.environ.get("CONTEXTSTREAM_PRECOMPACT_ENABLED", "true").lower() == "true"
9922
+ AUTO_SAVE = os.environ.get("CONTEXTSTREAM_PRECOMPACT_AUTO_SAVE", "true").lower() == "true"
9923
+ API_URL = os.environ.get("CONTEXTSTREAM_API_URL", "https://api.contextstream.io")
9924
+ API_KEY = os.environ.get("CONTEXTSTREAM_API_KEY", "")
9925
+
9926
+ WORKSPACE_ID = None
9927
+
9928
+ def load_config_from_mcp_json(cwd):
9929
+ """Load API config from .mcp.json if env vars not set."""
9930
+ global API_URL, API_KEY, WORKSPACE_ID
9931
+
9932
+ # Try to find .mcp.json and .contextstream/config.json in cwd or parent directories
9933
+ search_dir = cwd
9934
+ for _ in range(5): # Search up to 5 levels
9935
+ # Load API config from .mcp.json
9936
+ if not API_KEY:
9937
+ mcp_path = os.path.join(search_dir, ".mcp.json")
9938
+ if os.path.exists(mcp_path):
9939
+ try:
9940
+ with open(mcp_path, 'r') as f:
9941
+ config = json.load(f)
9942
+ servers = config.get("mcpServers", {})
9943
+ cs_config = servers.get("contextstream", {})
9944
+ env = cs_config.get("env", {})
9945
+ if env.get("CONTEXTSTREAM_API_KEY"):
9946
+ API_KEY = env["CONTEXTSTREAM_API_KEY"]
9947
+ if env.get("CONTEXTSTREAM_API_URL"):
9948
+ API_URL = env["CONTEXTSTREAM_API_URL"]
9949
+ except:
9950
+ pass
9951
+
9952
+ # Load workspace_id from .contextstream/config.json
9953
+ if not WORKSPACE_ID:
9954
+ cs_config_path = os.path.join(search_dir, ".contextstream", "config.json")
9955
+ if os.path.exists(cs_config_path):
9956
+ try:
9957
+ with open(cs_config_path, 'r') as f:
9958
+ cs_config = json.load(f)
9959
+ if cs_config.get("workspace_id"):
9960
+ WORKSPACE_ID = cs_config["workspace_id"]
9961
+ except:
9962
+ pass
9963
+
9964
+ parent = os.path.dirname(search_dir)
9965
+ if parent == search_dir:
9966
+ break
9967
+ search_dir = parent
9968
+
9969
+ def parse_transcript(transcript_path):
9970
+ """Parse transcript to extract active files, decisions, and context."""
9971
+ active_files = set()
9972
+ recent_messages = []
9973
+ tool_calls = []
9974
+
9975
+ try:
9976
+ with open(transcript_path, 'r') as f:
9977
+ for line in f:
9978
+ try:
9979
+ entry = json.loads(line.strip())
9980
+ msg_type = entry.get("type", "")
9981
+
9982
+ # Extract files from tool calls
9983
+ if msg_type == "tool_use":
9984
+ tool_name = entry.get("name", "")
9985
+ tool_input = entry.get("input", {})
9986
+ tool_calls.append({"name": tool_name, "input": tool_input})
9987
+
9988
+ # Extract file paths from common tools
9989
+ if tool_name in ["Read", "Write", "Edit", "NotebookEdit"]:
9990
+ file_path = tool_input.get("file_path") or tool_input.get("notebook_path")
9991
+ if file_path:
9992
+ active_files.add(file_path)
9993
+ elif tool_name == "Glob":
9994
+ pattern = tool_input.get("pattern", "")
9995
+ if pattern:
9996
+ active_files.add(f"[glob:{pattern}]")
9997
+
9998
+ # Collect recent assistant messages for summary
9999
+ if msg_type == "assistant" and entry.get("content"):
10000
+ content = entry.get("content", "")
10001
+ if isinstance(content, str) and len(content) > 50:
10002
+ recent_messages.append(content[:500])
10003
+
10004
+ except json.JSONDecodeError:
10005
+ continue
10006
+ except Exception as e:
10007
+ pass
10008
+
10009
+ return {
10010
+ "active_files": list(active_files)[-20:], # Last 20 files
10011
+ "tool_call_count": len(tool_calls),
10012
+ "message_count": len(recent_messages),
10013
+ "last_tools": [t["name"] for t in tool_calls[-10:]], # Last 10 tool names
10014
+ }
10015
+
10016
+ def save_snapshot(session_id, transcript_data, trigger):
10017
+ """Save snapshot to ContextStream API."""
10018
+ if not API_KEY:
10019
+ return False, "No API key configured"
10020
+
10021
+ snapshot_content = {
10022
+ "session_id": session_id,
10023
+ "trigger": trigger,
10024
+ "captured_at": None, # API will set timestamp
10025
+ "active_files": transcript_data.get("active_files", []),
10026
+ "tool_call_count": transcript_data.get("tool_call_count", 0),
10027
+ "last_tools": transcript_data.get("last_tools", []),
10028
+ "auto_captured": True,
10029
+ }
10030
+
10031
+ payload = {
10032
+ "event_type": "session_snapshot",
10033
+ "title": f"Auto Pre-compaction Snapshot ({trigger})",
10034
+ "content": json.dumps(snapshot_content),
10035
+ "importance": "high",
10036
+ "tags": ["session_snapshot", "pre_compaction", "auto_captured"],
10037
+ "source_type": "hook",
10038
+ }
10039
+
10040
+ # Add workspace_id if available
10041
+ if WORKSPACE_ID:
10042
+ payload["workspace_id"] = WORKSPACE_ID
10043
+
10044
+ try:
10045
+ req = urllib.request.Request(
10046
+ f"{API_URL}/api/v1/memory/events",
10047
+ data=json.dumps(payload).encode('utf-8'),
10048
+ headers={
10049
+ "Content-Type": "application/json",
10050
+ "X-API-Key": API_KEY,
10051
+ },
10052
+ method="POST"
10053
+ )
10054
+ with urllib.request.urlopen(req, timeout=5) as resp:
10055
+ return True, "Snapshot saved"
10056
+ except urllib.error.URLError as e:
10057
+ return False, str(e)
10058
+ except Exception as e:
10059
+ return False, str(e)
9834
10060
 
9835
10061
  def main():
9836
10062
  if not ENABLED:
@@ -9841,26 +10067,38 @@ def main():
9841
10067
  except:
9842
10068
  sys.exit(0)
9843
10069
 
10070
+ # Load config from .mcp.json if env vars not set
10071
+ cwd = data.get("cwd", os.getcwd())
10072
+ load_config_from_mcp_json(cwd)
10073
+
10074
+ session_id = data.get("session_id", "unknown")
10075
+ transcript_path = data.get("transcript_path", "")
9844
10076
  trigger = data.get("trigger", "unknown")
9845
10077
  custom_instructions = data.get("custom_instructions", "")
9846
10078
 
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:
10079
+ # Parse transcript for context
10080
+ transcript_data = {}
10081
+ if transcript_path and os.path.exists(transcript_path):
10082
+ transcript_data = parse_transcript(transcript_path)
9850
10083
 
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>")
10084
+ # Auto-save snapshot if enabled
10085
+ auto_save_status = ""
10086
+ if AUTO_SAVE and API_KEY:
10087
+ success, msg = save_snapshot(session_id, transcript_data, trigger)
10088
+ if success:
10089
+ auto_save_status = f"\\n[ContextStream: Auto-saved snapshot with {len(transcript_data.get('active_files', []))} active files]"
10090
+ else:
10091
+ auto_save_status = f"\\n[ContextStream: Auto-save failed - {msg}]"
9852
10092
 
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
10093
+ # Build context injection for the AI (backup in case auto-save fails)
10094
+ files_list = ", ".join(transcript_data.get("active_files", [])[:5]) or "none detected"
10095
+ context = f"""[CONTEXT COMPACTION - {trigger.upper()}]{auto_save_status}
9859
10096
 
9860
- 3. After compaction, call session_init(is_post_compact=true) to restore context.
10097
+ Active files detected: {files_list}
10098
+ Tool calls in session: {transcript_data.get('tool_call_count', 0)}
9861
10099
 
9862
- {f"User instructions: {custom_instructions}" if custom_instructions else ""}
9863
- [END COMPACTION WARNING]"""
10100
+ After compaction, call session_init(is_post_compact=true) to restore context.
10101
+ {f"User instructions: {custom_instructions}" if custom_instructions else ""}"""
9864
10102
 
9865
10103
  output = {
9866
10104
  "hookSpecificOutput": {
@@ -9956,98 +10194,778 @@ async function installHookScripts(options) {
9956
10194
  async function readClaudeSettings(scope, projectPath) {
9957
10195
  const settingsPath = getClaudeSettingsPath(scope, projectPath);
9958
10196
  try {
9959
- const content = await fs4.readFile(settingsPath, "utf-8");
10197
+ const content = await fs4.readFile(settingsPath, "utf-8");
10198
+ return JSON.parse(content);
10199
+ } catch {
10200
+ return {};
10201
+ }
10202
+ }
10203
+ async function writeClaudeSettings(settings, scope, projectPath) {
10204
+ const settingsPath = getClaudeSettingsPath(scope, projectPath);
10205
+ const dir = path5.dirname(settingsPath);
10206
+ await fs4.mkdir(dir, { recursive: true });
10207
+ await fs4.writeFile(settingsPath, JSON.stringify(settings, null, 2));
10208
+ }
10209
+ function mergeHooksIntoSettings(existingSettings, newHooks) {
10210
+ const settings = { ...existingSettings };
10211
+ const existingHooks = settings.hooks || {};
10212
+ for (const [hookType, matchers] of Object.entries(newHooks || {})) {
10213
+ if (!matchers) continue;
10214
+ const existing = existingHooks?.[hookType] || [];
10215
+ const filtered = existing.filter((m) => {
10216
+ return !m.hooks?.some((h) => h.command?.includes("contextstream"));
10217
+ });
10218
+ existingHooks[hookType] = [...filtered, ...matchers];
10219
+ }
10220
+ settings.hooks = existingHooks;
10221
+ return settings;
10222
+ }
10223
+ function getIndexStatusPath() {
10224
+ return path5.join(homedir2(), ".contextstream", "indexed-projects.json");
10225
+ }
10226
+ async function readIndexStatus() {
10227
+ const statusPath = getIndexStatusPath();
10228
+ try {
10229
+ const content = await fs4.readFile(statusPath, "utf-8");
10230
+ return JSON.parse(content);
10231
+ } catch {
10232
+ return { version: 1, projects: {} };
10233
+ }
10234
+ }
10235
+ async function writeIndexStatus(status) {
10236
+ const statusPath = getIndexStatusPath();
10237
+ const dir = path5.dirname(statusPath);
10238
+ await fs4.mkdir(dir, { recursive: true });
10239
+ await fs4.writeFile(statusPath, JSON.stringify(status, null, 2));
10240
+ }
10241
+ async function markProjectIndexed(projectPath, options) {
10242
+ const status = await readIndexStatus();
10243
+ const resolvedPath = path5.resolve(projectPath);
10244
+ status.projects[resolvedPath] = {
10245
+ indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
10246
+ project_id: options?.project_id,
10247
+ project_name: options?.project_name
10248
+ };
10249
+ await writeIndexStatus(status);
10250
+ }
10251
+ var CLINE_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
10252
+ """
10253
+ ContextStream PreToolUse Hook for Cline
10254
+ Blocks discovery tools and redirects to ContextStream search.
10255
+
10256
+ Cline hooks use JSON output format:
10257
+ {
10258
+ "cancel": true/false,
10259
+ "errorMessage": "optional error description",
10260
+ "contextModification": "optional text to inject"
10261
+ }
10262
+ """
10263
+
10264
+ import json
10265
+ import sys
10266
+ import os
10267
+ from pathlib import Path
10268
+ from datetime import datetime, timedelta
10269
+
10270
+ ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
10271
+ INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
10272
+ STALE_THRESHOLD_DAYS = 7
10273
+
10274
+ DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
10275
+
10276
+ def is_discovery_glob(pattern):
10277
+ pattern_lower = pattern.lower()
10278
+ for p in DISCOVERY_PATTERNS:
10279
+ if p in pattern_lower:
10280
+ return True
10281
+ if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
10282
+ return True
10283
+ if "**" in pattern or "*/" in pattern:
10284
+ return True
10285
+ return False
10286
+
10287
+ def is_discovery_grep(file_path):
10288
+ if not file_path or file_path in [".", "./", "*", "**"]:
10289
+ return True
10290
+ if "*" in file_path or "**" in file_path:
10291
+ return True
10292
+ return False
10293
+
10294
+ def is_project_indexed(workspace_roots):
10295
+ """Check if any workspace root is in an indexed project."""
10296
+ if not INDEX_STATUS_FILE.exists():
10297
+ return False, False
10298
+
10299
+ try:
10300
+ with open(INDEX_STATUS_FILE, "r") as f:
10301
+ data = json.load(f)
10302
+ except:
10303
+ return False, False
10304
+
10305
+ projects = data.get("projects", {})
10306
+
10307
+ for workspace in workspace_roots:
10308
+ cwd_path = Path(workspace).resolve()
10309
+ for project_path, info in projects.items():
10310
+ try:
10311
+ indexed_path = Path(project_path).resolve()
10312
+ if cwd_path == indexed_path or indexed_path in cwd_path.parents:
10313
+ indexed_at = info.get("indexed_at")
10314
+ if indexed_at:
10315
+ try:
10316
+ indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
10317
+ if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
10318
+ return True, True
10319
+ except:
10320
+ pass
10321
+ return True, False
10322
+ except:
10323
+ continue
10324
+ return False, False
10325
+
10326
+ def output_allow(context_mod=None):
10327
+ result = {"cancel": False}
10328
+ if context_mod:
10329
+ result["contextModification"] = context_mod
10330
+ print(json.dumps(result))
10331
+ sys.exit(0)
10332
+
10333
+ def output_block(error_msg, context_mod=None):
10334
+ result = {"cancel": True, "errorMessage": error_msg}
10335
+ if context_mod:
10336
+ result["contextModification"] = context_mod
10337
+ print(json.dumps(result))
10338
+ sys.exit(0)
10339
+
10340
+ def main():
10341
+ if not ENABLED:
10342
+ output_allow()
10343
+
10344
+ try:
10345
+ data = json.load(sys.stdin)
10346
+ except:
10347
+ output_allow()
10348
+
10349
+ hook_name = data.get("hookName", "")
10350
+ if hook_name != "PreToolUse":
10351
+ output_allow()
10352
+
10353
+ tool = data.get("toolName", "")
10354
+ params = data.get("toolParameters", {})
10355
+ workspace_roots = data.get("workspaceRoots", [])
10356
+
10357
+ # Check if project is indexed
10358
+ is_indexed, is_stale = is_project_indexed(workspace_roots)
10359
+ if not is_indexed:
10360
+ output_allow()
10361
+
10362
+ # Check for discovery patterns
10363
+ if tool == "list_files" or tool == "search_files":
10364
+ pattern = params.get("path", "") or params.get("regex", "")
10365
+ if is_discovery_glob(pattern) or is_discovery_grep(pattern):
10366
+ output_block(
10367
+ f"Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}. "
10368
+ "ContextStream search is indexed and faster. Only use local tools if ContextStream returns 0 results.",
10369
+ "[CONTEXTSTREAM] Use ContextStream search for code discovery."
10370
+ )
10371
+
10372
+ elif tool == "read_file":
10373
+ # Allow read_file by default - blocking discovery at search level is enough
10374
+ pass
10375
+
10376
+ output_allow()
10377
+
10378
+ if __name__ == "__main__":
10379
+ main()
10380
+ `;
10381
+ var CLINE_USER_PROMPT_HOOK_SCRIPT = `#!/usr/bin/env python3
10382
+ """
10383
+ ContextStream UserPromptSubmit Hook for Cline
10384
+ Injects reminder about ContextStream rules on every message.
10385
+ """
10386
+
10387
+ import json
10388
+ import sys
10389
+ import os
10390
+
10391
+ ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
10392
+
10393
+ REMINDER = """[CONTEXTSTREAM RULES]
10394
+ 1. BEFORE list_files/search_files/read_file: mcp__contextstream__search(mode="hybrid") FIRST
10395
+ 2. Call context_smart at start of EVERY response
10396
+ 3. Local tools ONLY if ContextStream returns 0 results
10397
+ [END RULES]"""
10398
+
10399
+ def main():
10400
+ if not ENABLED:
10401
+ print(json.dumps({"cancel": False}))
10402
+ sys.exit(0)
10403
+
10404
+ try:
10405
+ json.load(sys.stdin)
10406
+ except:
10407
+ print(json.dumps({"cancel": False}))
10408
+ sys.exit(0)
10409
+
10410
+ print(json.dumps({
10411
+ "cancel": False,
10412
+ "contextModification": REMINDER
10413
+ }))
10414
+ sys.exit(0)
10415
+
10416
+ if __name__ == "__main__":
10417
+ main()
10418
+ `;
10419
+ function getClineHooksDir(scope, projectPath) {
10420
+ if (scope === "global") {
10421
+ return path5.join(homedir2(), "Documents", "Cline", "Rules", "Hooks");
10422
+ }
10423
+ if (!projectPath) {
10424
+ throw new Error("projectPath required for project scope");
10425
+ }
10426
+ return path5.join(projectPath, ".clinerules", "hooks");
10427
+ }
10428
+ async function installClineHookScripts(options) {
10429
+ const hooksDir = getClineHooksDir(options.scope, options.projectPath);
10430
+ await fs4.mkdir(hooksDir, { recursive: true });
10431
+ const preToolUsePath = path5.join(hooksDir, "PreToolUse");
10432
+ const userPromptPath = path5.join(hooksDir, "UserPromptSubmit");
10433
+ await fs4.writeFile(preToolUsePath, CLINE_PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
10434
+ await fs4.writeFile(userPromptPath, CLINE_USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
10435
+ return {
10436
+ preToolUse: preToolUsePath,
10437
+ userPromptSubmit: userPromptPath
10438
+ };
10439
+ }
10440
+ function getRooCodeHooksDir(scope, projectPath) {
10441
+ if (scope === "global") {
10442
+ return path5.join(homedir2(), ".roo", "hooks");
10443
+ }
10444
+ if (!projectPath) {
10445
+ throw new Error("projectPath required for project scope");
10446
+ }
10447
+ return path5.join(projectPath, ".roo", "hooks");
10448
+ }
10449
+ async function installRooCodeHookScripts(options) {
10450
+ const hooksDir = getRooCodeHooksDir(options.scope, options.projectPath);
10451
+ await fs4.mkdir(hooksDir, { recursive: true });
10452
+ const preToolUsePath = path5.join(hooksDir, "PreToolUse");
10453
+ const userPromptPath = path5.join(hooksDir, "UserPromptSubmit");
10454
+ await fs4.writeFile(preToolUsePath, CLINE_PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
10455
+ await fs4.writeFile(userPromptPath, CLINE_USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
10456
+ return {
10457
+ preToolUse: preToolUsePath,
10458
+ userPromptSubmit: userPromptPath
10459
+ };
10460
+ }
10461
+ function getKiloCodeHooksDir(scope, projectPath) {
10462
+ if (scope === "global") {
10463
+ return path5.join(homedir2(), ".kilocode", "hooks");
10464
+ }
10465
+ if (!projectPath) {
10466
+ throw new Error("projectPath required for project scope");
10467
+ }
10468
+ return path5.join(projectPath, ".kilocode", "hooks");
10469
+ }
10470
+ async function installKiloCodeHookScripts(options) {
10471
+ const hooksDir = getKiloCodeHooksDir(options.scope, options.projectPath);
10472
+ await fs4.mkdir(hooksDir, { recursive: true });
10473
+ const preToolUsePath = path5.join(hooksDir, "PreToolUse");
10474
+ const userPromptPath = path5.join(hooksDir, "UserPromptSubmit");
10475
+ await fs4.writeFile(preToolUsePath, CLINE_PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
10476
+ await fs4.writeFile(userPromptPath, CLINE_USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
10477
+ return {
10478
+ preToolUse: preToolUsePath,
10479
+ userPromptSubmit: userPromptPath
10480
+ };
10481
+ }
10482
+ var CURSOR_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
10483
+ """
10484
+ ContextStream PreToolUse Hook for Cursor
10485
+ Blocks discovery tools and redirects to ContextStream search.
10486
+
10487
+ Cursor hooks use JSON output format:
10488
+ {
10489
+ "decision": "allow" | "deny",
10490
+ "reason": "optional error description"
10491
+ }
10492
+ """
10493
+
10494
+ import json
10495
+ import sys
10496
+ import os
10497
+ from pathlib import Path
10498
+ from datetime import datetime, timedelta
10499
+
10500
+ ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
10501
+ INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
10502
+ STALE_THRESHOLD_DAYS = 7
10503
+
10504
+ DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
10505
+
10506
+ def is_discovery_glob(pattern):
10507
+ pattern_lower = pattern.lower()
10508
+ for p in DISCOVERY_PATTERNS:
10509
+ if p in pattern_lower:
10510
+ return True
10511
+ if pattern_lower.startswith("**/*.") or pattern_lower.startswith("**/"):
10512
+ return True
10513
+ if "**" in pattern or "*/" in pattern:
10514
+ return True
10515
+ return False
10516
+
10517
+ def is_discovery_grep(file_path):
10518
+ if not file_path or file_path in [".", "./", "*", "**"]:
10519
+ return True
10520
+ if "*" in file_path or "**" in file_path:
10521
+ return True
10522
+ return False
10523
+
10524
+ def is_project_indexed(workspace_roots):
10525
+ """Check if any workspace root is in an indexed project."""
10526
+ if not INDEX_STATUS_FILE.exists():
10527
+ return False, False
10528
+
10529
+ try:
10530
+ with open(INDEX_STATUS_FILE, "r") as f:
10531
+ data = json.load(f)
10532
+ except:
10533
+ return False, False
10534
+
10535
+ projects = data.get("projects", {})
10536
+
10537
+ for workspace in workspace_roots:
10538
+ cwd_path = Path(workspace).resolve()
10539
+ for project_path, info in projects.items():
10540
+ try:
10541
+ indexed_path = Path(project_path).resolve()
10542
+ if cwd_path == indexed_path or indexed_path in cwd_path.parents:
10543
+ indexed_at = info.get("indexed_at")
10544
+ if indexed_at:
10545
+ try:
10546
+ indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
10547
+ if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
10548
+ return True, True
10549
+ except:
10550
+ pass
10551
+ return True, False
10552
+ except:
10553
+ continue
10554
+ return False, False
10555
+
10556
+ def output_allow():
10557
+ print(json.dumps({"decision": "allow"}))
10558
+ sys.exit(0)
10559
+
10560
+ def output_deny(reason):
10561
+ print(json.dumps({"decision": "deny", "reason": reason}))
10562
+ sys.exit(0)
10563
+
10564
+ def main():
10565
+ if not ENABLED:
10566
+ output_allow()
10567
+
10568
+ try:
10569
+ data = json.load(sys.stdin)
10570
+ except:
10571
+ output_allow()
10572
+
10573
+ hook_name = data.get("hook_event_name", "")
10574
+ if hook_name != "preToolUse":
10575
+ output_allow()
10576
+
10577
+ tool = data.get("tool_name", "")
10578
+ params = data.get("tool_input", {}) or data.get("parameters", {})
10579
+ workspace_roots = data.get("workspace_roots", [])
10580
+
10581
+ # Check if project is indexed
10582
+ is_indexed, _ = is_project_indexed(workspace_roots)
10583
+ if not is_indexed:
10584
+ output_allow()
10585
+
10586
+ # Check for Cursor tools
10587
+ if tool in ["Glob", "glob", "list_files"]:
10588
+ pattern = params.get("pattern", "") or params.get("path", "")
10589
+ if is_discovery_glob(pattern):
10590
+ output_deny(
10591
+ f"Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}. "
10592
+ "ContextStream search is indexed and faster."
10593
+ )
10594
+
10595
+ elif tool in ["Grep", "grep", "search_files", "ripgrep"]:
10596
+ pattern = params.get("pattern", "") or params.get("regex", "")
10597
+ file_path = params.get("path", "")
10598
+ if is_discovery_grep(file_path):
10599
+ output_deny(
10600
+ f"Use mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") instead of {tool}. "
10601
+ "ContextStream search is indexed and faster."
10602
+ )
10603
+
10604
+ output_allow()
10605
+
10606
+ if __name__ == "__main__":
10607
+ main()
10608
+ `;
10609
+ var CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT = `#!/usr/bin/env python3
10610
+ """
10611
+ ContextStream BeforeSubmitPrompt Hook for Cursor
10612
+ Injects reminder about ContextStream rules.
10613
+ """
10614
+
10615
+ import json
10616
+ import sys
10617
+ import os
10618
+
10619
+ ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
10620
+
10621
+ def main():
10622
+ if not ENABLED:
10623
+ print(json.dumps({"continue": True}))
10624
+ sys.exit(0)
10625
+
10626
+ try:
10627
+ json.load(sys.stdin)
10628
+ except:
10629
+ print(json.dumps({"continue": True}))
10630
+ sys.exit(0)
10631
+
10632
+ print(json.dumps({
10633
+ "continue": True,
10634
+ "user_message": "[CONTEXTSTREAM] Search with mcp__contextstream__search before using Glob/Grep/Read"
10635
+ }))
10636
+ sys.exit(0)
10637
+
10638
+ if __name__ == "__main__":
10639
+ main()
10640
+ `;
10641
+ function getCursorHooksConfigPath(scope, projectPath) {
10642
+ if (scope === "global") {
10643
+ return path5.join(homedir2(), ".cursor", "hooks.json");
10644
+ }
10645
+ if (!projectPath) {
10646
+ throw new Error("projectPath required for project scope");
10647
+ }
10648
+ return path5.join(projectPath, ".cursor", "hooks.json");
10649
+ }
10650
+ function getCursorHooksDir(scope, projectPath) {
10651
+ if (scope === "global") {
10652
+ return path5.join(homedir2(), ".cursor", "hooks");
10653
+ }
10654
+ if (!projectPath) {
10655
+ throw new Error("projectPath required for project scope");
10656
+ }
10657
+ return path5.join(projectPath, ".cursor", "hooks");
10658
+ }
10659
+ async function readCursorHooksConfig(scope, projectPath) {
10660
+ const configPath = getCursorHooksConfigPath(scope, projectPath);
10661
+ try {
10662
+ const content = await fs4.readFile(configPath, "utf-8");
10663
+ return JSON.parse(content);
10664
+ } catch {
10665
+ return { version: 1, hooks: {} };
10666
+ }
10667
+ }
10668
+ async function writeCursorHooksConfig(config, scope, projectPath) {
10669
+ const configPath = getCursorHooksConfigPath(scope, projectPath);
10670
+ const dir = path5.dirname(configPath);
10671
+ await fs4.mkdir(dir, { recursive: true });
10672
+ await fs4.writeFile(configPath, JSON.stringify(config, null, 2));
10673
+ }
10674
+ async function installCursorHookScripts(options) {
10675
+ const hooksDir = getCursorHooksDir(options.scope, options.projectPath);
10676
+ await fs4.mkdir(hooksDir, { recursive: true });
10677
+ const preToolUsePath = path5.join(hooksDir, "contextstream-pretooluse.py");
10678
+ const beforeSubmitPath = path5.join(hooksDir, "contextstream-beforesubmit.py");
10679
+ await fs4.writeFile(preToolUsePath, CURSOR_PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
10680
+ await fs4.writeFile(beforeSubmitPath, CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT, { mode: 493 });
10681
+ const existingConfig = await readCursorHooksConfig(options.scope, options.projectPath);
10682
+ const filterContextStreamHooks = (hooks) => {
10683
+ if (!hooks) return [];
10684
+ return hooks.filter((h) => !h.command?.includes("contextstream"));
10685
+ };
10686
+ const config = {
10687
+ version: 1,
10688
+ hooks: {
10689
+ ...existingConfig.hooks,
10690
+ preToolUse: [
10691
+ ...filterContextStreamHooks(existingConfig.hooks.preToolUse),
10692
+ {
10693
+ command: `python3 "${preToolUsePath}"`,
10694
+ type: "command",
10695
+ timeout: 5,
10696
+ matcher: { tool_name: "Glob|Grep|search_files|list_files|ripgrep" }
10697
+ }
10698
+ ],
10699
+ beforeSubmitPrompt: [
10700
+ ...filterContextStreamHooks(existingConfig.hooks.beforeSubmitPrompt),
10701
+ {
10702
+ command: `python3 "${beforeSubmitPath}"`,
10703
+ type: "command",
10704
+ timeout: 5
10705
+ }
10706
+ ]
10707
+ }
10708
+ };
10709
+ await writeCursorHooksConfig(config, options.scope, options.projectPath);
10710
+ const configPath = getCursorHooksConfigPath(options.scope, options.projectPath);
10711
+ return {
10712
+ preToolUse: preToolUsePath,
10713
+ beforeSubmitPrompt: beforeSubmitPath,
10714
+ config: configPath
10715
+ };
10716
+ }
10717
+ var WINDSURF_PRE_MCP_TOOL_USE_SCRIPT = `#!/usr/bin/env python3
10718
+ """
10719
+ ContextStream pre_mcp_tool_use Hook for Windsurf
10720
+ Blocks discovery tools and redirects to ContextStream search.
10721
+
10722
+ Exit codes:
10723
+ - 0: Allow action to proceed
10724
+ - 2: Block action (message to stderr)
10725
+ """
10726
+
10727
+ import json
10728
+ import sys
10729
+ import os
10730
+
10731
+ ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
10732
+
10733
+ # Tools to redirect
10734
+ DISCOVERY_TOOLS = {
10735
+ "read_file": "Use mcp__contextstream__search(mode=\\"hybrid\\") for discovery",
10736
+ "search_files": "Use mcp__contextstream__search(mode=\\"hybrid\\")",
10737
+ "list_files": "Use mcp__contextstream__search(mode=\\"pattern\\")",
10738
+ "codebase_search": "Use mcp__contextstream__search(mode=\\"hybrid\\")",
10739
+ "grep_search": "Use mcp__contextstream__search(mode=\\"keyword\\")",
10740
+ }
10741
+
10742
+ def is_project_indexed(workspace_roots):
10743
+ """Check if any workspace has a .contextstream/index marker."""
10744
+ for root in workspace_roots:
10745
+ marker = os.path.join(root, ".contextstream", "index.json")
10746
+ if os.path.exists(marker):
10747
+ return True, root
10748
+ return False, None
10749
+
10750
+ def main():
10751
+ if not ENABLED:
10752
+ sys.exit(0)
10753
+
10754
+ try:
10755
+ data = json.load(sys.stdin)
10756
+ except:
10757
+ sys.exit(0)
10758
+
10759
+ tool_info = data.get("tool_info", {})
10760
+ tool_name = tool_info.get("tool_name", "")
10761
+
10762
+ # For MCP tools, check the server and tool
10763
+ mcp_server = tool_info.get("mcp_server", "")
10764
+
10765
+ # Get workspace roots from the data
10766
+ workspace_roots = []
10767
+ if "working_directory" in data:
10768
+ workspace_roots.append(data["working_directory"])
10769
+
10770
+ # Check if project is indexed
10771
+ is_indexed, _ = is_project_indexed(workspace_roots)
10772
+ if not is_indexed:
10773
+ sys.exit(0)
10774
+
10775
+ # Check if this is a discovery tool we should redirect
10776
+ if tool_name in DISCOVERY_TOOLS:
10777
+ message = DISCOVERY_TOOLS[tool_name]
10778
+ print(message, file=sys.stderr)
10779
+ sys.exit(2)
10780
+
10781
+ sys.exit(0)
10782
+
10783
+ if __name__ == "__main__":
10784
+ main()
10785
+ `;
10786
+ var WINDSURF_PRE_USER_PROMPT_SCRIPT = `#!/usr/bin/env python3
10787
+ """
10788
+ ContextStream pre_user_prompt Hook for Windsurf
10789
+ Injects reminder about ContextStream rules.
10790
+
10791
+ Note: This hook runs before prompt processing but cannot modify the prompt.
10792
+ It primarily serves for logging and validation purposes.
10793
+ """
10794
+
10795
+ import json
10796
+ import sys
10797
+ import os
10798
+
10799
+ ENABLED = os.environ.get("CONTEXTSTREAM_REMINDER_ENABLED", "true").lower() == "true"
10800
+
10801
+ def main():
10802
+ if not ENABLED:
10803
+ sys.exit(0)
10804
+
10805
+ try:
10806
+ json.load(sys.stdin)
10807
+ except:
10808
+ sys.exit(0)
10809
+
10810
+ # Allow the prompt to proceed
10811
+ sys.exit(0)
10812
+
10813
+ if __name__ == "__main__":
10814
+ main()
10815
+ `;
10816
+ function getWindsurfHooksConfigPath(scope, projectPath) {
10817
+ if (scope === "project" && projectPath) {
10818
+ return path5.join(projectPath, ".windsurf", "hooks.json");
10819
+ }
10820
+ return path5.join(homedir2(), ".codeium", "windsurf", "hooks.json");
10821
+ }
10822
+ function getWindsurfHooksDir(scope, projectPath) {
10823
+ if (scope === "project" && projectPath) {
10824
+ return path5.join(projectPath, ".windsurf", "hooks");
10825
+ }
10826
+ return path5.join(homedir2(), ".codeium", "windsurf", "hooks");
10827
+ }
10828
+ async function readWindsurfHooksConfig(scope, projectPath) {
10829
+ const configPath = getWindsurfHooksConfigPath(scope, projectPath);
10830
+ try {
10831
+ const content = await fs4.promises.readFile(configPath, "utf-8");
9960
10832
  return JSON.parse(content);
9961
10833
  } catch {
9962
- return {};
10834
+ return { hooks: {} };
9963
10835
  }
9964
10836
  }
9965
- async function writeClaudeSettings(settings, scope, projectPath) {
9966
- const settingsPath = getClaudeSettingsPath(scope, projectPath);
9967
- const dir = path5.dirname(settingsPath);
9968
- await fs4.mkdir(dir, { recursive: true });
9969
- await fs4.writeFile(settingsPath, JSON.stringify(settings, null, 2));
10837
+ async function writeWindsurfHooksConfig(config, scope, projectPath) {
10838
+ const configPath = getWindsurfHooksConfigPath(scope, projectPath);
10839
+ const configDir = path5.dirname(configPath);
10840
+ await fs4.promises.mkdir(configDir, { recursive: true });
10841
+ await fs4.promises.writeFile(configPath, JSON.stringify(config, null, 2));
9970
10842
  }
9971
- function mergeHooksIntoSettings(existingSettings, newHooks) {
9972
- const settings = { ...existingSettings };
9973
- const existingHooks = settings.hooks || {};
9974
- for (const [hookType, matchers] of Object.entries(newHooks || {})) {
9975
- if (!matchers) continue;
9976
- const existing = existingHooks?.[hookType] || [];
9977
- const filtered = existing.filter((m) => {
9978
- return !m.hooks?.some((h) => h.command?.includes("contextstream"));
9979
- });
9980
- existingHooks[hookType] = [...filtered, ...matchers];
9981
- }
9982
- settings.hooks = existingHooks;
9983
- return settings;
10843
+ function filterWindsurfContextStreamHooks(hooks) {
10844
+ if (!hooks) return [];
10845
+ return hooks.filter((h) => !h.command?.includes("contextstream"));
9984
10846
  }
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"));
10847
+ async function installWindsurfHookScripts(options) {
10848
+ const scope = options.scope || "global";
10849
+ const hooksDir = getWindsurfHooksDir(scope, options.projectPath);
10850
+ await fs4.promises.mkdir(hooksDir, { recursive: true });
10851
+ const preMcpToolUsePath = path5.join(hooksDir, "contextstream-pretooluse.py");
10852
+ const preUserPromptPath = path5.join(hooksDir, "contextstream-reminder.py");
10853
+ await fs4.promises.writeFile(preMcpToolUsePath, WINDSURF_PRE_MCP_TOOL_USE_SCRIPT);
10854
+ await fs4.promises.writeFile(preUserPromptPath, WINDSURF_PRE_USER_PROMPT_SCRIPT);
10855
+ if (process.platform !== "win32") {
10856
+ await fs4.promises.chmod(preMcpToolUsePath, 493);
10857
+ await fs4.promises.chmod(preUserPromptPath, 493);
10858
+ }
10859
+ const existingConfig = await readWindsurfHooksConfig(scope, options.projectPath);
10860
+ const config = {
10861
+ hooks: {
10862
+ ...existingConfig.hooks,
10863
+ pre_mcp_tool_use: [
10864
+ ...filterWindsurfContextStreamHooks(existingConfig.hooks.pre_mcp_tool_use),
10865
+ {
10866
+ command: `python3 "${preMcpToolUsePath}"`,
10867
+ show_output: true
10868
+ }
10869
+ ],
10870
+ pre_user_prompt: [
10871
+ ...filterWindsurfContextStreamHooks(existingConfig.hooks.pre_user_prompt),
10872
+ {
10873
+ command: `python3 "${preUserPromptPath}"`,
10874
+ show_output: false
10875
+ }
10876
+ ]
10001
10877
  }
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");
10878
+ };
10879
+ await writeWindsurfHooksConfig(config, scope, options.projectPath);
10880
+ const configPath = getWindsurfHooksConfigPath(scope, options.projectPath);
10881
+ return {
10882
+ preMcpToolUse: preMcpToolUsePath,
10883
+ preUserPrompt: preUserPromptPath,
10884
+ config: configPath
10885
+ };
10886
+ }
10887
+ async function installEditorHooks(options) {
10888
+ const { editor, scope, projectPath, includePreCompact } = options;
10889
+ switch (editor) {
10890
+ case "claude": {
10891
+ if (scope === "project" && !projectPath) {
10892
+ throw new Error("projectPath required for project scope");
10893
+ }
10894
+ const scripts = await installHookScripts({ includePreCompact });
10895
+ const hooksConfig = buildHooksConfig({ includePreCompact });
10896
+ const settingsScope = scope === "global" ? "user" : "project";
10897
+ const existing = await readClaudeSettings(settingsScope, projectPath);
10008
10898
  const merged = mergeHooksIntoSettings(existing, hooksConfig);
10009
- await writeClaudeSettings(merged, "user");
10899
+ await writeClaudeSettings(merged, settingsScope, projectPath);
10900
+ const installed = [scripts.preToolUse, scripts.userPrompt];
10901
+ if (scripts.preCompact) installed.push(scripts.preCompact);
10902
+ return {
10903
+ editor: "claude",
10904
+ installed,
10905
+ hooksDir: getHooksDir()
10906
+ };
10010
10907
  }
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);
10908
+ case "cline": {
10909
+ const scripts = await installClineHookScripts({ scope, projectPath });
10910
+ return {
10911
+ editor: "cline",
10912
+ installed: [scripts.preToolUse, scripts.userPromptSubmit],
10913
+ hooksDir: getClineHooksDir(scope, projectPath)
10914
+ };
10915
+ }
10916
+ case "roo": {
10917
+ const scripts = await installRooCodeHookScripts({ scope, projectPath });
10918
+ return {
10919
+ editor: "roo",
10920
+ installed: [scripts.preToolUse, scripts.userPromptSubmit],
10921
+ hooksDir: getRooCodeHooksDir(scope, projectPath)
10922
+ };
10923
+ }
10924
+ case "kilo": {
10925
+ const scripts = await installKiloCodeHookScripts({ scope, projectPath });
10926
+ return {
10927
+ editor: "kilo",
10928
+ installed: [scripts.preToolUse, scripts.userPromptSubmit],
10929
+ hooksDir: getKiloCodeHooksDir(scope, projectPath)
10930
+ };
10931
+ }
10932
+ case "cursor": {
10933
+ const scripts = await installCursorHookScripts();
10934
+ return {
10935
+ editor: "cursor",
10936
+ installed: [scripts.preToolUse, scripts.beforeSubmit],
10937
+ hooksDir: getCursorHooksDir()
10938
+ };
10019
10939
  }
10020
- result.settings.push(settingsPath);
10940
+ case "windsurf": {
10941
+ const scripts = await installWindsurfHookScripts({ scope, projectPath });
10942
+ return {
10943
+ editor: "windsurf",
10944
+ installed: [scripts.preMcpToolUse, scripts.preUserPrompt],
10945
+ hooksDir: getWindsurfHooksDir(scope, projectPath)
10946
+ };
10947
+ }
10948
+ default:
10949
+ throw new Error(`Unsupported editor: ${editor}`);
10021
10950
  }
10022
- return result;
10023
- }
10024
- function getIndexStatusPath() {
10025
- return path5.join(homedir2(), ".contextstream", "indexed-projects.json");
10026
10951
  }
10027
- async function readIndexStatus() {
10028
- const statusPath = getIndexStatusPath();
10029
- try {
10030
- const content = await fs4.readFile(statusPath, "utf-8");
10031
- return JSON.parse(content);
10032
- } catch {
10033
- return { version: 1, projects: {} };
10952
+ async function installAllEditorHooks(options) {
10953
+ const editors = options.editors || ["claude", "cline", "roo", "kilo", "cursor", "windsurf"];
10954
+ const results = [];
10955
+ for (const editor of editors) {
10956
+ try {
10957
+ const result = await installEditorHooks({
10958
+ editor,
10959
+ scope: options.scope,
10960
+ projectPath: options.projectPath,
10961
+ includePreCompact: options.includePreCompact
10962
+ });
10963
+ results.push(result);
10964
+ } catch (error) {
10965
+ console.error(`Failed to install hooks for ${editor}:`, error);
10966
+ }
10034
10967
  }
10035
- }
10036
- async function writeIndexStatus(status) {
10037
- const statusPath = getIndexStatusPath();
10038
- const dir = path5.dirname(statusPath);
10039
- await fs4.mkdir(dir, { recursive: true });
10040
- await fs4.writeFile(statusPath, JSON.stringify(status, null, 2));
10041
- }
10042
- async function markProjectIndexed(projectPath, options) {
10043
- const status = await readIndexStatus();
10044
- const resolvedPath = path5.resolve(projectPath);
10045
- status.projects[resolvedPath] = {
10046
- indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
10047
- project_id: options?.project_id,
10048
- project_name: options?.project_name
10049
- };
10050
- await writeIndexStatus(status);
10968
+ return results;
10051
10969
  }
10052
10970
 
10053
10971
  // src/token-savings.ts
@@ -10124,9 +11042,75 @@ BEFORE using EnterPlanMode/Task(Plan) \u2192 call mcp__contextstream__session(ac
10124
11042
  Local tools ONLY if ContextStream returns 0 results after retry.
10125
11043
  `.trim();
10126
11044
  var LESSONS_REMINDER_PREFIX = `
10127
- \u26A0\uFE0F [LESSONS - REVIEW BEFORE CHANGES]
10128
- Past mistakes found that may be relevant. STOP and review before proceeding:
11045
+ \u{1F6A8} [LESSONS_WARNING] Past Mistakes Found - READ BEFORE PROCEEDING!
11046
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
11047
+ \u26A0\uFE0F IMPORTANT: You MUST review these lessons and tell the user about relevant ones.
11048
+ These are mistakes from past sessions that you should NOT repeat.
10129
11049
  `.trim();
11050
+ var RISKY_ACTION_KEYWORDS = [
11051
+ // Code changes
11052
+ "refactor",
11053
+ "rewrite",
11054
+ "restructure",
11055
+ "reorganize",
11056
+ "migrate",
11057
+ "delete",
11058
+ "remove",
11059
+ "drop",
11060
+ "deprecate",
11061
+ // Database
11062
+ "database",
11063
+ "migration",
11064
+ "schema",
11065
+ "sql",
11066
+ // Deployment
11067
+ "deploy",
11068
+ "release",
11069
+ "production",
11070
+ "prod",
11071
+ // API changes
11072
+ "api",
11073
+ "endpoint",
11074
+ "breaking change",
11075
+ // Architecture
11076
+ "architecture",
11077
+ "design",
11078
+ "pattern",
11079
+ // Testing
11080
+ "test",
11081
+ "testing",
11082
+ // Security
11083
+ "auth",
11084
+ "security",
11085
+ "permission",
11086
+ "credential",
11087
+ "access",
11088
+ "token",
11089
+ "secret",
11090
+ // Version control
11091
+ "git",
11092
+ "commit",
11093
+ "merge",
11094
+ "rebase",
11095
+ "push",
11096
+ "force",
11097
+ // Infrastructure
11098
+ "config",
11099
+ "environment",
11100
+ "env",
11101
+ "docker",
11102
+ "kubernetes",
11103
+ "k8s",
11104
+ // Performance
11105
+ "performance",
11106
+ "optimize",
11107
+ "cache",
11108
+ "memory"
11109
+ ];
11110
+ function detectRiskyActions(userMessage) {
11111
+ const messageLower = userMessage.toLowerCase();
11112
+ return RISKY_ACTION_KEYWORDS.filter((keyword) => messageLower.includes(keyword));
11113
+ }
10130
11114
  function generateLessonsReminder(result) {
10131
11115
  const lessons = result.lessons;
10132
11116
  if (!lessons || lessons.length === 0) {
@@ -10141,7 +11125,8 @@ function generateLessonsReminder(result) {
10141
11125
  return `
10142
11126
 
10143
11127
  ${LESSONS_REMINDER_PREFIX}
10144
- ${lessonLines.join("\n")}`;
11128
+ ${lessonLines.join("\n")}
11129
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\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
11130
  }
10146
11131
  function generateRulesUpdateWarning(rulesNotice) {
10147
11132
  if (!rulesNotice || rulesNotice.status !== "behind" && rulesNotice.status !== "missing") {
@@ -10164,12 +11149,16 @@ function generateVersionUpdateWarning(versionNotice) {
10164
11149
  return "";
10165
11150
  }
10166
11151
  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.
11152
+ \u{1F6A8} [VERSION_NOTICE] MCP Server Update Available!
11153
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
11154
+ Version: ${versionNotice.current} \u2192 ${versionNotice.latest}
10170
11155
 
10171
- **UPDATE NOW:** Run \`${versionNotice.upgrade_command || "npm update @contextstream/mcp-server"}\`
10172
- Then restart Claude Code to use the new version.
11156
+ \u26A0\uFE0F IMPORTANT: You MUST tell the user about this update IMMEDIATELY.
11157
+ Do not proceed with other work until you have informed the user.
11158
+
11159
+ Update command: \`${versionNotice.upgrade_command || "npm update -g @contextstream/mcp-server"}\`
11160
+ After updating, restart the AI tool to use the new version.
11161
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\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
11162
  `.trim();
10174
11163
  }
10175
11164
  var DEFAULT_PARAM_DESCRIPTIONS = {
@@ -11030,6 +12019,7 @@ var ALL_INTEGRATION_TOOLS = /* @__PURE__ */ new Set([
11030
12019
  ...CROSS_INTEGRATION_TOOLS
11031
12020
  ]);
11032
12021
  var AUTO_HIDE_INTEGRATIONS = process.env.CONTEXTSTREAM_AUTO_HIDE_INTEGRATIONS !== "false";
12022
+ var RESTORE_CONTEXT_DEFAULT = process.env.CONTEXTSTREAM_RESTORE_CONTEXT !== "false";
11033
12023
  var TOKEN_SENSITIVE_CLIENTS = /* @__PURE__ */ new Set([
11034
12024
  "claude",
11035
12025
  "claude-code",
@@ -13824,7 +14814,7 @@ This does semantic search on the first message. You only need context_smart on s
13824
14814
  "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
14815
  ),
13826
14816
  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."
14817
+ "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
14818
  )
13829
14819
  })
13830
14820
  },
@@ -13845,19 +14835,22 @@ This does semantic search on the first message. You only need context_smart on s
13845
14835
  }
13846
14836
  const result = await client.initSession(input, ideRoots);
13847
14837
  result.tools_hint = getCoreToolsHint();
13848
- if (input.is_post_compact) {
14838
+ const shouldRestoreContext = input.is_post_compact ?? RESTORE_CONTEXT_DEFAULT;
14839
+ if (shouldRestoreContext) {
14840
+ result.is_post_compact = true;
13849
14841
  const workspaceIdForRestore = typeof result.workspace_id === "string" ? result.workspace_id : void 0;
13850
14842
  const projectIdForRestore = typeof result.project_id === "string" ? result.project_id : void 0;
13851
14843
  if (workspaceIdForRestore) {
13852
14844
  try {
13853
- const snapshotSearch = await client.searchEvents({
14845
+ const listResult = await client.listMemoryEvents({
13854
14846
  workspace_id: workspaceIdForRestore,
13855
14847
  project_id: projectIdForRestore,
13856
- query: "session_snapshot",
13857
- event_types: ["session_snapshot"],
13858
- limit: 1
14848
+ limit: 50
13859
14849
  });
13860
- const snapshots = snapshotSearch?.data?.results || snapshotSearch?.results || snapshotSearch?.data || [];
14850
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
14851
+ const snapshots = allEvents.filter(
14852
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
14853
+ );
13861
14854
  if (snapshots && snapshots.length > 0) {
13862
14855
  const latestSnapshot = snapshots[0];
13863
14856
  let snapshotData;
@@ -13866,13 +14859,52 @@ This does semantic search on the first message. You only need context_smart on s
13866
14859
  } catch {
13867
14860
  snapshotData = { conversation_summary: latestSnapshot.content };
13868
14861
  }
14862
+ const prevSessionId = snapshotData.session_id || latestSnapshot.session_id;
14863
+ const sessionLinking = {};
14864
+ if (prevSessionId) {
14865
+ sessionLinking.previous_session_id = prevSessionId;
14866
+ const workingOn = [];
14867
+ const activeFiles = snapshotData.active_files;
14868
+ const lastTools = snapshotData.last_tools;
14869
+ if (activeFiles && activeFiles.length > 0) {
14870
+ workingOn.push(`Files: ${activeFiles.slice(0, 5).join(", ")}${activeFiles.length > 5 ? ` (+${activeFiles.length - 5} more)` : ""}`);
14871
+ }
14872
+ if (lastTools && lastTools.length > 0) {
14873
+ const toolCounts = lastTools.reduce((acc, tool) => {
14874
+ acc[tool] = (acc[tool] || 0) + 1;
14875
+ return acc;
14876
+ }, {});
14877
+ const topTools = Object.entries(toolCounts).sort(([, a], [, b]) => b - a).slice(0, 3).map(([tool]) => tool);
14878
+ workingOn.push(`Recent tools: ${topTools.join(", ")}`);
14879
+ }
14880
+ if (workingOn.length > 0) {
14881
+ sessionLinking.previous_session_summary = workingOn.join("; ");
14882
+ }
14883
+ const relatedSessionIds = /* @__PURE__ */ new Set();
14884
+ snapshots.forEach((s) => {
14885
+ let sData;
14886
+ try {
14887
+ sData = JSON.parse(s.content || "{}");
14888
+ } catch {
14889
+ sData = {};
14890
+ }
14891
+ const sSessionId = sData.session_id || s.session_id;
14892
+ if (sSessionId && sSessionId !== prevSessionId) {
14893
+ relatedSessionIds.add(sSessionId);
14894
+ }
14895
+ });
14896
+ if (relatedSessionIds.size > 0) {
14897
+ sessionLinking.related_sessions = Array.from(relatedSessionIds);
14898
+ }
14899
+ }
13869
14900
  result.restored_context = {
13870
14901
  snapshot_id: latestSnapshot.id,
13871
14902
  captured_at: snapshotData.captured_at || latestSnapshot.created_at,
14903
+ session_linking: Object.keys(sessionLinking).length > 0 ? sessionLinking : void 0,
13872
14904
  ...snapshotData
13873
14905
  };
13874
14906
  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.";
14907
+ 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
14908
  } else {
13877
14909
  result.is_post_compact = true;
13878
14910
  result.post_compact_hint = "Post-compaction session started, but no snapshots found. Use context_smart to retrieve relevant context.";
@@ -15022,34 +16054,77 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
15022
16054
  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.";
15023
16055
  let hooksResults;
15024
16056
  let hooksPrompt;
15025
- const hasClaude = editors.includes("claude");
15026
- const shouldInstallHooks = hasClaude && input.install_hooks !== false;
16057
+ const editorHookMap = {
16058
+ claude: "claude",
16059
+ cline: "cline",
16060
+ roo: "roo",
16061
+ kilo: "kilo",
16062
+ cursor: "cursor",
16063
+ windsurf: "windsurf"
16064
+ };
16065
+ const hookSupportedEditors = editors.filter((e) => e in editorHookMap);
16066
+ const shouldInstallHooks = hookSupportedEditors.length > 0 && input.install_hooks !== false;
15027
16067
  if (shouldInstallHooks) {
15028
16068
  try {
15029
16069
  if (input.dry_run) {
15030
- hooksResults = [
15031
- { file: "~/.claude/hooks/contextstream-redirect.py", status: "dry run - would create" },
15032
- { file: "~/.claude/hooks/contextstream-reminder.py", status: "dry run - would create" },
15033
- { file: "~/.claude/settings.json", status: "dry run - would update" }
15034
- ];
15035
- if (input.include_pre_compact) {
15036
- hooksResults.push({ file: "~/.claude/hooks/contextstream-precompact.py", status: "dry run - would create" });
16070
+ hooksResults = [];
16071
+ for (const editor of hookSupportedEditors) {
16072
+ if (editor === "claude") {
16073
+ hooksResults.push(
16074
+ { editor, file: "~/.claude/hooks/contextstream-redirect.py", status: "dry run - would create" },
16075
+ { editor, file: "~/.claude/hooks/contextstream-reminder.py", status: "dry run - would create" },
16076
+ { editor, file: "~/.claude/settings.json", status: "dry run - would update" }
16077
+ );
16078
+ if (input.include_pre_compact) {
16079
+ hooksResults.push({ editor, file: "~/.claude/hooks/contextstream-precompact.py", status: "dry run - would create" });
16080
+ }
16081
+ } else if (editor === "cline") {
16082
+ hooksResults.push(
16083
+ { editor, file: "~/Documents/Cline/Rules/Hooks/PreToolUse", status: "dry run - would create" },
16084
+ { editor, file: "~/Documents/Cline/Rules/Hooks/UserPromptSubmit", status: "dry run - would create" }
16085
+ );
16086
+ } else if (editor === "roo") {
16087
+ hooksResults.push(
16088
+ { editor, file: "~/.roo/hooks/PreToolUse", status: "dry run - would create" },
16089
+ { editor, file: "~/.roo/hooks/UserPromptSubmit", status: "dry run - would create" }
16090
+ );
16091
+ } else if (editor === "kilo") {
16092
+ hooksResults.push(
16093
+ { editor, file: "~/.kilocode/hooks/PreToolUse", status: "dry run - would create" },
16094
+ { editor, file: "~/.kilocode/hooks/UserPromptSubmit", status: "dry run - would create" }
16095
+ );
16096
+ } else if (editor === "cursor") {
16097
+ hooksResults.push(
16098
+ { editor, file: "~/.cursor/hooks/contextstream-pretooluse.py", status: "dry run - would create" },
16099
+ { editor, file: "~/.cursor/hooks/contextstream-beforesubmit.py", status: "dry run - would create" },
16100
+ { editor, file: "~/.cursor/hooks.json", status: "dry run - would update" }
16101
+ );
16102
+ } else if (editor === "windsurf") {
16103
+ hooksResults.push(
16104
+ { editor, file: "~/.codeium/windsurf/hooks/contextstream-pretooluse.py", status: "dry run - would create" },
16105
+ { editor, file: "~/.codeium/windsurf/hooks/contextstream-reminder.py", status: "dry run - would create" },
16106
+ { editor, file: "~/.codeium/windsurf/hooks.json", status: "dry run - would update" }
16107
+ );
16108
+ }
15037
16109
  }
15038
16110
  } else {
15039
- const hookResult = await installClaudeCodeHooks({
15040
- scope: "user",
16111
+ hooksResults = [];
16112
+ const allHookResults = await installAllEditorHooks({
16113
+ scope: "global",
16114
+ editors: hookSupportedEditors,
15041
16115
  includePreCompact: input.include_pre_compact
15042
16116
  });
15043
- hooksResults = [
15044
- ...hookResult.scripts.map((f) => ({ file: f, status: "created" })),
15045
- ...hookResult.settings.map((f) => ({ file: f, status: "updated" }))
15046
- ];
16117
+ for (const result of allHookResults) {
16118
+ for (const file of result.installed) {
16119
+ hooksResults.push({ editor: result.editor, file, status: "created" });
16120
+ }
16121
+ }
15047
16122
  }
15048
16123
  } catch (err) {
15049
16124
  hooksResults = [{ file: "hooks", status: `error: ${err.message}` }];
15050
16125
  }
15051
- } else if (hasClaude && input.install_hooks === false) {
15052
- hooksPrompt = "Hooks skipped. Claude may use default tools instead of ContextStream search.";
16126
+ } else if (hookSupportedEditors.length > 0 && input.install_hooks === false) {
16127
+ hooksPrompt = "Hooks skipped. AI may use default tools instead of ContextStream search.";
15053
16128
  }
15054
16129
  const summary = {
15055
16130
  folder: folderPath,
@@ -15428,6 +16503,52 @@ This saves ~80% tokens compared to including full chat history.`,
15428
16503
  }
15429
16504
  sessionManager.addTokens(input.user_message);
15430
16505
  }
16506
+ let postCompactContext = "";
16507
+ let postCompactRestored = false;
16508
+ if (sessionManager && sessionManager.shouldRestorePostCompact() && workspaceId) {
16509
+ try {
16510
+ const listResult = await client.listMemoryEvents({
16511
+ workspace_id: workspaceId,
16512
+ project_id: projectId,
16513
+ limit: 20
16514
+ });
16515
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
16516
+ const snapshotEvent = allEvents.find(
16517
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.tags?.includes("session_snapshot")
16518
+ );
16519
+ if (snapshotEvent && snapshotEvent.content) {
16520
+ let snapshotData;
16521
+ try {
16522
+ snapshotData = JSON.parse(snapshotEvent.content);
16523
+ } catch {
16524
+ snapshotData = { conversation_summary: snapshotEvent.content };
16525
+ }
16526
+ const summary = snapshotData.conversation_summary || snapshotData.summary || "";
16527
+ const decisions = snapshotData.key_decisions || [];
16528
+ const unfinished = snapshotData.unfinished_work || snapshotData.pending_tasks || [];
16529
+ const files = snapshotData.active_files || [];
16530
+ const parts = [];
16531
+ parts.push("\u{1F4CB} [POST-COMPACTION CONTEXT RESTORED]");
16532
+ if (summary) parts.push(`Summary: ${summary}`);
16533
+ if (Array.isArray(decisions) && decisions.length > 0) {
16534
+ parts.push(`Decisions: ${decisions.slice(0, 5).join("; ")}`);
16535
+ }
16536
+ if (Array.isArray(unfinished) && unfinished.length > 0) {
16537
+ parts.push(`Unfinished: ${unfinished.slice(0, 3).join("; ")}`);
16538
+ }
16539
+ if (Array.isArray(files) && files.length > 0) {
16540
+ parts.push(`Active files: ${files.slice(0, 5).join(", ")}`);
16541
+ }
16542
+ parts.push("---");
16543
+ postCompactContext = parts.join("\n") + "\n\n";
16544
+ postCompactRestored = true;
16545
+ sessionManager.markPostCompactRestoreCompleted();
16546
+ console.error("[ContextStream] Post-compaction context restored automatically");
16547
+ }
16548
+ } catch (err) {
16549
+ console.error("[ContextStream] Failed to restore post-compact context:", err);
16550
+ }
16551
+ }
15431
16552
  const result = await client.getSmartContext({
15432
16553
  user_message: input.user_message,
15433
16554
  workspace_id: workspaceId,
@@ -15468,8 +16589,44 @@ This saves ~80% tokens compared to including full chat history.`,
15468
16589
  project_id: projectId,
15469
16590
  max_tokens: input.max_tokens
15470
16591
  });
15471
- const hasLessons = result.context.includes("|L:") || result.context.includes("L:") || result.context.toLowerCase().includes("lesson");
15472
- 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." : "";
16592
+ let lessonsWarningLine = "";
16593
+ const riskyKeywords = detectRiskyActions(input.user_message);
16594
+ if (riskyKeywords.length > 0 && workspaceId) {
16595
+ try {
16596
+ const lessons = await client.getHighPriorityLessons({
16597
+ workspace_id: workspaceId,
16598
+ project_id: projectId,
16599
+ context_hint: riskyKeywords.join(" "),
16600
+ limit: 5
16601
+ });
16602
+ if (lessons.length > 0) {
16603
+ const lessonLines = lessons.slice(0, 5).map((l, i) => {
16604
+ const severity = l.severity === "critical" ? "\u{1F6A8}" : l.severity === "high" ? "\u26A0\uFE0F" : "\u{1F4DD}";
16605
+ const title = l.title || "Untitled lesson";
16606
+ const prevention = l.prevention || "";
16607
+ return `${i + 1}. ${severity} ${title}${prevention ? `: ${prevention.slice(0, 100)}` : ""}`;
16608
+ });
16609
+ lessonsWarningLine = `
16610
+
16611
+ \u{1F6A8} [LESSONS_WARNING] Relevant Lessons for "${riskyKeywords.slice(0, 3).join(", ")}"
16612
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
16613
+ \u26A0\uFE0F IMPORTANT: You MUST tell the user about these lessons before proceeding.
16614
+ These are past mistakes that may be relevant to the current task.
16615
+
16616
+ ${lessonLines.join("\n")}
16617
+
16618
+ Action: Review each lesson and explain to the user how you will avoid these mistakes.
16619
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`;
16620
+ }
16621
+ } catch {
16622
+ }
16623
+ }
16624
+ if (!lessonsWarningLine) {
16625
+ const hasLessonsInContext = result.context.includes("|L:") || result.context.includes("L:") || result.context.toLowerCase().includes("lesson");
16626
+ if (hasLessonsInContext) {
16627
+ lessonsWarningLine = "\n\n\u26A0\uFE0F [LESSONS_WARNING] Lessons found in context - review the L: items above before making changes.";
16628
+ }
16629
+ }
15473
16630
  const searchRulesLine = SEARCH_RULES_REMINDER_ENABLED ? `
15474
16631
 
15475
16632
  ${SEARCH_RULES_REMINDER}` : "";
@@ -15477,12 +16634,18 @@ ${SEARCH_RULES_REMINDER}` : "";
15477
16634
  if (result.context_pressure) {
15478
16635
  const cp = result.context_pressure;
15479
16636
  if (cp.level === "critical") {
16637
+ if (sessionManager) {
16638
+ sessionManager.markHighContextPressure();
16639
+ }
15480
16640
  contextPressureWarning = `
15481
16641
 
15482
16642
  \u{1F6A8} [CONTEXT PRESSURE: CRITICAL] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
15483
16643
  Action: ${cp.suggested_action === "save_now" ? 'SAVE STATE NOW - Call session(action="capture") to preserve conversation state before compaction.' : cp.suggested_action}
15484
16644
  The conversation may compact soon. Save important decisions, insights, and progress immediately.`;
15485
16645
  } else if (cp.level === "high") {
16646
+ if (sessionManager) {
16647
+ sessionManager.markHighContextPressure();
16648
+ }
15486
16649
  contextPressureWarning = `
15487
16650
 
15488
16651
  \u26A0\uFE0F [CONTEXT PRESSURE: HIGH] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
@@ -15500,14 +16663,16 @@ ${versionWarningLine}` : "",
15500
16663
  contextPressureWarning,
15501
16664
  searchRulesLine
15502
16665
  ].filter(Boolean).join("");
16666
+ const finalContext = postCompactContext + result.context;
16667
+ const enrichedResultWithRestore = postCompactRestored ? { ...enrichedResult, post_compact_restored: true } : enrichedResult;
15503
16668
  return {
15504
16669
  content: [
15505
16670
  {
15506
16671
  type: "text",
15507
- text: result.context + footer + allWarnings
16672
+ text: finalContext + footer + allWarnings
15508
16673
  }
15509
16674
  ],
15510
- structuredContent: toStructured(enrichedResult)
16675
+ structuredContent: toStructured(enrichedResultWithRestore)
15511
16676
  };
15512
16677
  }
15513
16678
  );
@@ -16998,12 +18163,35 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16998
18163
  } catch {
16999
18164
  snapshotData = { conversation_summary: event.content };
17000
18165
  }
18166
+ const sessionId = snapshotData.session_id || event.session_id;
18167
+ const sessionLinking2 = {};
18168
+ if (sessionId) {
18169
+ sessionLinking2.previous_session_id = sessionId;
18170
+ const workingOn = [];
18171
+ const activeFiles = snapshotData.active_files;
18172
+ const lastTools = snapshotData.last_tools;
18173
+ if (activeFiles && activeFiles.length > 0) {
18174
+ workingOn.push(`Files: ${activeFiles.slice(0, 5).join(", ")}${activeFiles.length > 5 ? ` (+${activeFiles.length - 5} more)` : ""}`);
18175
+ }
18176
+ if (lastTools && lastTools.length > 0) {
18177
+ const toolCounts = lastTools.reduce((acc, tool) => {
18178
+ acc[tool] = (acc[tool] || 0) + 1;
18179
+ return acc;
18180
+ }, {});
18181
+ const topTools = Object.entries(toolCounts).sort(([, a], [, b]) => b - a).slice(0, 3).map(([tool]) => tool);
18182
+ workingOn.push(`Recent tools: ${topTools.join(", ")}`);
18183
+ }
18184
+ if (workingOn.length > 0) {
18185
+ sessionLinking2.previous_session_summary = workingOn.join("; ");
18186
+ }
18187
+ }
17001
18188
  const response2 = {
17002
18189
  restored: true,
17003
18190
  snapshot_id: event.id,
17004
18191
  captured_at: snapshotData.captured_at || event.created_at,
18192
+ session_linking: Object.keys(sessionLinking2).length > 0 ? sessionLinking2 : void 0,
17005
18193
  ...snapshotData,
17006
- hint: "Context restored. Continue the conversation with awareness of the above state."
18194
+ 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."
17007
18195
  };
17008
18196
  return {
17009
18197
  content: [{ type: "text", text: formatContent(response2) }],
@@ -17044,15 +18232,48 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
17044
18232
  return {
17045
18233
  snapshot_id: event.id,
17046
18234
  captured_at: snapshotData.captured_at || event.created_at,
18235
+ session_id: snapshotData.session_id || event.session_id,
17047
18236
  ...snapshotData
17048
18237
  };
17049
18238
  });
18239
+ const latestSnapshot = snapshots[0];
18240
+ const sessionLinking = {};
18241
+ if (latestSnapshot?.session_id) {
18242
+ sessionLinking.previous_session_id = latestSnapshot.session_id;
18243
+ const workingOn = [];
18244
+ const activeFiles = latestSnapshot.active_files;
18245
+ const lastTools = latestSnapshot.last_tools;
18246
+ if (activeFiles && activeFiles.length > 0) {
18247
+ workingOn.push(`Files: ${activeFiles.slice(0, 5).join(", ")}${activeFiles.length > 5 ? ` (+${activeFiles.length - 5} more)` : ""}`);
18248
+ }
18249
+ if (lastTools && lastTools.length > 0) {
18250
+ const toolCounts = lastTools.reduce((acc, tool) => {
18251
+ acc[tool] = (acc[tool] || 0) + 1;
18252
+ return acc;
18253
+ }, {});
18254
+ const topTools = Object.entries(toolCounts).sort(([, a], [, b]) => b - a).slice(0, 3).map(([tool]) => tool);
18255
+ workingOn.push(`Recent tools: ${topTools.join(", ")}`);
18256
+ }
18257
+ if (workingOn.length > 0) {
18258
+ sessionLinking.previous_session_summary = workingOn.join("; ");
18259
+ }
18260
+ const relatedSessionIds = /* @__PURE__ */ new Set();
18261
+ snapshots.forEach((s) => {
18262
+ if (s.session_id && s.session_id !== latestSnapshot.session_id) {
18263
+ relatedSessionIds.add(s.session_id);
18264
+ }
18265
+ });
18266
+ if (relatedSessionIds.size > 0) {
18267
+ sessionLinking.related_sessions = Array.from(relatedSessionIds);
18268
+ }
18269
+ }
17050
18270
  const response = {
17051
18271
  restored: true,
17052
18272
  snapshots_found: snapshots.length,
17053
18273
  latest: snapshots[0],
17054
18274
  all_snapshots: snapshots.length > 1 ? snapshots : void 0,
17055
- hint: "Context restored. Continue the conversation with awareness of the above state."
18275
+ session_linking: Object.keys(sessionLinking).length > 0 ? sessionLinking : void 0,
18276
+ 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."
17056
18277
  };
17057
18278
  return {
17058
18279
  content: [{ type: "text", text: formatContent(response) }],
@@ -19623,8 +20844,7 @@ function registerPrompts(server) {
19623
20844
  }
19624
20845
 
19625
20846
  // src/session-manager.ts
19626
- var SessionManager = class {
19627
- // Conservative default for 100k context window
20847
+ var SessionManager = class _SessionManager {
19628
20848
  constructor(server, client) {
19629
20849
  this.server = server;
19630
20850
  this.client = client;
@@ -19636,8 +20856,30 @@ var SessionManager = class {
19636
20856
  this.contextSmartCalled = false;
19637
20857
  this.warningShown = false;
19638
20858
  // Token tracking for context pressure calculation
20859
+ // Note: MCP servers cannot see actual token usage (AI responses, thinking, system prompts).
20860
+ // We use a heuristic: tracked tokens + (turns * estimated tokens per turn)
19639
20861
  this.sessionTokens = 0;
19640
20862
  this.contextThreshold = 7e4;
20863
+ // Conservative default for 100k context window
20864
+ this.conversationTurns = 0;
20865
+ // Continuous checkpointing
20866
+ this.toolCallCount = 0;
20867
+ this.checkpointInterval = 20;
20868
+ // Save checkpoint every N tool calls
20869
+ this.lastCheckpointAt = 0;
20870
+ this.activeFiles = /* @__PURE__ */ new Set();
20871
+ this.recentToolCalls = [];
20872
+ this.checkpointEnabled = process.env.CONTEXTSTREAM_CHECKPOINT_ENABLED?.toLowerCase() === "true";
20873
+ // Post-compaction restoration tracking
20874
+ // Tracks when context pressure was high/critical so we can detect post-compaction state
20875
+ this.lastHighPressureAt = null;
20876
+ this.lastHighPressureTokens = 0;
20877
+ this.postCompactRestoreCompleted = false;
20878
+ }
20879
+ static {
20880
+ // Each conversation turn typically includes: user message (~500), AI response (~1500),
20881
+ // system prompt overhead (~500), and reasoning (~1500). Conservative estimate: 3000/turn
20882
+ this.TOKENS_PER_TURN_ESTIMATE = 3e3;
19641
20883
  }
19642
20884
  /**
19643
20885
  * Check if session has been auto-initialized
@@ -19680,17 +20922,40 @@ var SessionManager = class {
19680
20922
  this.folderPath = path9;
19681
20923
  }
19682
20924
  /**
19683
- * Mark that context_smart has been called in this session
20925
+ * Mark that context_smart has been called in this session.
20926
+ * Also increments the conversation turn counter for token estimation.
19684
20927
  */
19685
20928
  markContextSmartCalled() {
19686
20929
  this.contextSmartCalled = true;
20930
+ this.conversationTurns++;
19687
20931
  }
19688
20932
  /**
19689
20933
  * Get current session token count for context pressure calculation.
20934
+ *
20935
+ * This returns an ESTIMATED count based on:
20936
+ * 1. Tokens tracked through ContextStream tools (actual)
20937
+ * 2. Estimated tokens per conversation turn (heuristic)
20938
+ *
20939
+ * Note: MCP servers cannot see actual AI token usage (responses, thinking,
20940
+ * system prompts). This estimate helps provide a more realistic context
20941
+ * pressure signal.
19690
20942
  */
19691
20943
  getSessionTokens() {
20944
+ const turnEstimate = this.conversationTurns * _SessionManager.TOKENS_PER_TURN_ESTIMATE;
20945
+ return this.sessionTokens + turnEstimate;
20946
+ }
20947
+ /**
20948
+ * Get the raw tracked tokens (without turn-based estimation).
20949
+ */
20950
+ getRawTrackedTokens() {
19692
20951
  return this.sessionTokens;
19693
20952
  }
20953
+ /**
20954
+ * Get the current conversation turn count.
20955
+ */
20956
+ getConversationTurns() {
20957
+ return this.conversationTurns;
20958
+ }
19694
20959
  /**
19695
20960
  * Get the context threshold (max tokens before compaction warning).
19696
20961
  */
@@ -19729,6 +20994,52 @@ var SessionManager = class {
19729
20994
  */
19730
20995
  resetTokenCount() {
19731
20996
  this.sessionTokens = 0;
20997
+ this.conversationTurns = 0;
20998
+ }
20999
+ /**
21000
+ * Record that context pressure is high/critical.
21001
+ * Called when context_smart returns high or critical pressure level.
21002
+ */
21003
+ markHighContextPressure() {
21004
+ this.lastHighPressureAt = Date.now();
21005
+ this.lastHighPressureTokens = this.getSessionTokens();
21006
+ }
21007
+ /**
21008
+ * Check if we should attempt post-compaction restoration.
21009
+ *
21010
+ * Detection heuristic:
21011
+ * 1. We recorded high/critical context pressure recently (within 10 minutes)
21012
+ * 2. Current token count is very low (< 5000) compared to when pressure was high
21013
+ * 3. We haven't already restored in this session
21014
+ *
21015
+ * This indicates compaction likely happened and we should restore context.
21016
+ */
21017
+ shouldRestorePostCompact() {
21018
+ if (this.postCompactRestoreCompleted) {
21019
+ return false;
21020
+ }
21021
+ if (!this.lastHighPressureAt) {
21022
+ return false;
21023
+ }
21024
+ const elapsed = Date.now() - this.lastHighPressureAt;
21025
+ if (elapsed > 10 * 60 * 1e3) {
21026
+ return false;
21027
+ }
21028
+ const currentTokens = this.getSessionTokens();
21029
+ const tokenDrop = this.lastHighPressureTokens - currentTokens;
21030
+ if (currentTokens > 1e4 || tokenDrop < this.lastHighPressureTokens * 0.5) {
21031
+ return false;
21032
+ }
21033
+ return true;
21034
+ }
21035
+ /**
21036
+ * Mark post-compaction restoration as completed.
21037
+ * Prevents multiple restoration attempts in the same session.
21038
+ */
21039
+ markPostCompactRestoreCompleted() {
21040
+ this.postCompactRestoreCompleted = true;
21041
+ this.lastHighPressureAt = null;
21042
+ this.lastHighPressureTokens = 0;
19732
21043
  }
19733
21044
  /**
19734
21045
  * Check if context_smart has been called and warn if not.
@@ -19994,6 +21305,112 @@ var SessionManager = class {
19994
21305
  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");
19995
21306
  return parts.join("\n");
19996
21307
  }
21308
+ // =========================================================================
21309
+ // Continuous Checkpointing
21310
+ // =========================================================================
21311
+ /**
21312
+ * Track a tool call for checkpointing purposes.
21313
+ * Call this after each tool execution to track files and trigger periodic checkpoints.
21314
+ */
21315
+ trackToolCall(toolName, input) {
21316
+ this.toolCallCount++;
21317
+ this.recentToolCalls.push({ name: toolName, timestamp: Date.now() });
21318
+ if (this.recentToolCalls.length > 50) {
21319
+ this.recentToolCalls = this.recentToolCalls.slice(-50);
21320
+ }
21321
+ if (input) {
21322
+ const filePath = input.file_path || input.notebook_path || input.path;
21323
+ if (filePath && typeof filePath === "string") {
21324
+ this.activeFiles.add(filePath);
21325
+ if (this.activeFiles.size > 30) {
21326
+ const arr = Array.from(this.activeFiles);
21327
+ this.activeFiles = new Set(arr.slice(-30));
21328
+ }
21329
+ }
21330
+ }
21331
+ this.maybeCheckpoint();
21332
+ }
21333
+ /**
21334
+ * Save a checkpoint if the interval has been reached.
21335
+ */
21336
+ async maybeCheckpoint() {
21337
+ if (!this.checkpointEnabled || !this.initialized || !this.context) {
21338
+ return;
21339
+ }
21340
+ const callsSinceLastCheckpoint = this.toolCallCount - this.lastCheckpointAt;
21341
+ if (callsSinceLastCheckpoint < this.checkpointInterval) {
21342
+ return;
21343
+ }
21344
+ this.lastCheckpointAt = this.toolCallCount;
21345
+ await this.saveCheckpoint("periodic");
21346
+ }
21347
+ /**
21348
+ * Get the list of active files being worked on.
21349
+ */
21350
+ getActiveFiles() {
21351
+ return Array.from(this.activeFiles);
21352
+ }
21353
+ /**
21354
+ * Get recent tool call names.
21355
+ */
21356
+ getRecentToolNames() {
21357
+ return this.recentToolCalls.map((t) => t.name);
21358
+ }
21359
+ /**
21360
+ * Get the current tool call count.
21361
+ */
21362
+ getToolCallCount() {
21363
+ return this.toolCallCount;
21364
+ }
21365
+ /**
21366
+ * Save a checkpoint snapshot to ContextStream.
21367
+ */
21368
+ async saveCheckpoint(trigger) {
21369
+ if (!this.initialized || !this.context) {
21370
+ return false;
21371
+ }
21372
+ const workspaceId = this.context.workspace_id;
21373
+ if (!workspaceId) {
21374
+ return false;
21375
+ }
21376
+ const checkpointData = {
21377
+ trigger,
21378
+ checkpoint_number: Math.floor(this.toolCallCount / this.checkpointInterval),
21379
+ tool_call_count: this.toolCallCount,
21380
+ session_tokens: this.sessionTokens,
21381
+ active_files: this.getActiveFiles(),
21382
+ recent_tools: this.getRecentToolNames().slice(-10),
21383
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
21384
+ auto_captured: true
21385
+ };
21386
+ try {
21387
+ await this.client.captureContext({
21388
+ workspace_id: workspaceId,
21389
+ project_id: this.context.project_id,
21390
+ event_type: "session_snapshot",
21391
+ title: `Checkpoint #${checkpointData.checkpoint_number} (${trigger})`,
21392
+ content: JSON.stringify(checkpointData),
21393
+ importance: trigger === "periodic" ? "low" : "medium",
21394
+ tags: ["session_snapshot", "checkpoint", trigger]
21395
+ });
21396
+ return true;
21397
+ } catch (err) {
21398
+ console.error("[ContextStream] Failed to save checkpoint:", err);
21399
+ return false;
21400
+ }
21401
+ }
21402
+ /**
21403
+ * Enable or disable continuous checkpointing.
21404
+ */
21405
+ setCheckpointEnabled(enabled) {
21406
+ this.checkpointEnabled = enabled;
21407
+ }
21408
+ /**
21409
+ * Set the checkpoint interval (tool calls between checkpoints).
21410
+ */
21411
+ setCheckpointInterval(interval) {
21412
+ this.checkpointInterval = Math.max(5, interval);
21413
+ }
19997
21414
  };
19998
21415
 
19999
21416
  // src/http-gateway.ts
@@ -20749,6 +22166,9 @@ function buildContextStreamMcpServer(params) {
20749
22166
  env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
20750
22167
  }
20751
22168
  env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
22169
+ if (params.restoreContextEnabled === false) {
22170
+ env.CONTEXTSTREAM_RESTORE_CONTEXT = "false";
22171
+ }
20752
22172
  if (params.showTiming) {
20753
22173
  env.CONTEXTSTREAM_SHOW_TIMING = "true";
20754
22174
  }
@@ -20774,6 +22194,9 @@ function buildContextStreamVsCodeServer(params) {
20774
22194
  env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
20775
22195
  }
20776
22196
  env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
22197
+ if (params.restoreContextEnabled === false) {
22198
+ env.CONTEXTSTREAM_RESTORE_CONTEXT = "false";
22199
+ }
20777
22200
  if (params.showTiming) {
20778
22201
  env.CONTEXTSTREAM_SHOW_TIMING = "true";
20779
22202
  }
@@ -20877,6 +22300,8 @@ async function upsertCodexTomlConfig(filePath, params) {
20877
22300
  ` : "";
20878
22301
  const contextPackLine = `CONTEXTSTREAM_CONTEXT_PACK = "${params.contextPackEnabled === false ? "false" : "true"}"
20879
22302
  `;
22303
+ const restoreContextLine = params.restoreContextEnabled === false ? `CONTEXTSTREAM_RESTORE_CONTEXT = "false"
22304
+ ` : "";
20880
22305
  const showTimingLine = params.showTiming ? `CONTEXTSTREAM_SHOW_TIMING = "true"
20881
22306
  ` : "";
20882
22307
  const commandLine = IS_WINDOWS ? `command = "cmd"
@@ -20892,7 +22317,7 @@ args = ["-y", "@contextstream/mcp-server"]
20892
22317
  [mcp_servers.contextstream.env]
20893
22318
  CONTEXTSTREAM_API_URL = "${params.apiUrl}"
20894
22319
  CONTEXTSTREAM_API_KEY = "${params.apiKey}"
20895
- ` + toolsetLine + contextPackLine + showTimingLine;
22320
+ ` + toolsetLine + contextPackLine + restoreContextLine + showTimingLine;
20896
22321
  if (!exists) {
20897
22322
  await fs7.writeFile(filePath, block.trimStart(), "utf8");
20898
22323
  return "created";
@@ -21254,6 +22679,12 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21254
22679
  console.log(" Useful for debugging performance; disabled by default.");
21255
22680
  const showTimingChoice = normalizeInput(await rl.question("Show response timing? [y/N]: "));
21256
22681
  const showTiming = showTimingChoice.toLowerCase() === "y" || showTimingChoice.toLowerCase() === "yes";
22682
+ console.log("\nAutomatic Context Restoration:");
22683
+ console.log(" Automatically restore context from recent snapshots on every session_init.");
22684
+ console.log(" This enables seamless continuation across conversations and after compaction.");
22685
+ console.log(" Enabled by default; disable if you prefer explicit control.");
22686
+ const restoreContextChoice = normalizeInput(await rl.question("Enable automatic context restoration? [Y/n]: "));
22687
+ const restoreContextEnabled = !(restoreContextChoice.toLowerCase() === "n" || restoreContextChoice.toLowerCase() === "no");
21257
22688
  const editors = [
21258
22689
  "codex",
21259
22690
  "claude",
@@ -21304,7 +22735,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21304
22735
  console.log(" 1) Global");
21305
22736
  console.log(" 2) Project");
21306
22737
  console.log(" 3) Both");
21307
- const scopeChoice = normalizeInput(await rl.question("Choose [1/2/3] (default 3): ")) || "3";
22738
+ const scopeChoice = normalizeInput(await rl.question("Choose [1/2/3] (default 2): ")) || "2";
21308
22739
  const scope = scopeChoice === "1" ? "global" : scopeChoice === "2" ? "project" : "both";
21309
22740
  console.log("\nInstall MCP server config as:");
21310
22741
  if (hasCodex && !hasProjectMcpEditors) {
@@ -21328,20 +22759,22 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21328
22759
  )
21329
22760
  ) || mcpChoiceDefault;
21330
22761
  const mcpScope = mcpChoice === "2" && hasCodex && !hasProjectMcpEditors ? "skip" : mcpChoice === "4" ? "skip" : mcpChoice === "1" ? "global" : mcpChoice === "2" ? "project" : "both";
21331
- const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, toolset, contextPackEnabled, showTiming });
22762
+ const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, toolset, contextPackEnabled, showTiming, restoreContextEnabled });
21332
22763
  const mcpServerClaude = buildContextStreamMcpServer({
21333
22764
  apiUrl,
21334
22765
  apiKey,
21335
22766
  toolset,
21336
22767
  contextPackEnabled,
21337
- showTiming
22768
+ showTiming,
22769
+ restoreContextEnabled
21338
22770
  });
21339
22771
  const vsCodeServer = buildContextStreamVsCodeServer({
21340
22772
  apiUrl,
21341
22773
  apiKey,
21342
22774
  toolset,
21343
22775
  contextPackEnabled,
21344
- showTiming
22776
+ showTiming,
22777
+ restoreContextEnabled
21345
22778
  });
21346
22779
  const needsGlobalMcpConfig = mcpScope === "global" || mcpScope === "both" || mcpScope === "project" && hasCodex;
21347
22780
  if (needsGlobalMcpConfig) {
@@ -21361,7 +22794,8 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21361
22794
  apiKey,
21362
22795
  toolset,
21363
22796
  contextPackEnabled,
21364
- showTiming
22797
+ showTiming,
22798
+ restoreContextEnabled
21365
22799
  });
21366
22800
  writeActions.push({ kind: "mcp-config", target: filePath, status });
21367
22801
  console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
@@ -21443,53 +22877,69 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
21443
22877
  }
21444
22878
  }
21445
22879
  }
21446
- if (configuredEditors.includes("claude")) {
22880
+ const HOOKS_SUPPORTED_EDITORS = {
22881
+ claude: "claude",
22882
+ cursor: "cursor",
22883
+ windsurf: "windsurf",
22884
+ cline: "cline",
22885
+ roo: "roo",
22886
+ kilo: "kilo",
22887
+ codex: null,
22888
+ // No hooks API
22889
+ aider: null,
22890
+ // No hooks API
22891
+ antigravity: null
22892
+ // No hooks API
22893
+ };
22894
+ const hookEligibleEditors = configuredEditors.filter(
22895
+ (e) => HOOKS_SUPPORTED_EDITORS[e] !== null
22896
+ );
22897
+ if (hookEligibleEditors.length > 0) {
21447
22898
  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");
21448
- console.log("\u2502 Claude Code Hooks (Recommended) \u2502");
22899
+ console.log("\u2502 AI Editor Hooks (Recommended) \u2502");
21449
22900
  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");
21450
22901
  console.log("");
21451
- console.log(" Problem: Claude Code often ignores CLAUDE.md instructions and uses");
21452
- console.log(" its default tools (Grep/Glob/Search) instead of ContextStream search.");
21453
- console.log(" This happens because instructions decay over long conversations.");
22902
+ console.log(" Problem: AI editors often use their default tools (Grep/Glob/Search)");
22903
+ console.log(" instead of ContextStream smart search. Instructions decay over long chats.");
21454
22904
  console.log("");
21455
22905
  console.log(" Solution: Install hooks that:");
21456
- console.log(" \u2713 Block default search tools (Grep/Glob/Search) \u2192 redirect to ContextStream");
21457
- console.log(" \u2713 Block built-in plan mode \u2192 redirect to ContextStream plans (persistent)");
21458
- console.log(" \u2713 Inject reminders on every message to keep rules in context");
21459
- console.log(" \u2713 Result: Faster searches, persistent plans across sessions");
22906
+ console.log(" \u2713 Use ContextStream (indexed, faster) with default tool use");
22907
+ console.log(" \u2713 Use ContextStream plans (persistent) with default tool use");
22908
+ console.log(" \u2713 Inject reminders to keep rules in context");
22909
+ console.log("");
22910
+ console.log(` Hooks available for: ${hookEligibleEditors.map((e) => EDITOR_LABELS[e]).join(", ")}`);
21460
22911
  console.log("");
21461
22912
  console.log(" You can disable hooks anytime with CONTEXTSTREAM_HOOK_ENABLED=false");
21462
22913
  console.log("");
21463
22914
  const installHooks = normalizeInput(
21464
- await rl.question("Install Claude Code hooks? [Y/n] (recommended): ")
22915
+ await rl.question("Install editor hooks? [Y/n] (recommended): ")
21465
22916
  ).toLowerCase();
21466
22917
  if (installHooks !== "n" && installHooks !== "no") {
21467
- try {
21468
- if (dryRun) {
21469
- console.log("- Would install hooks to ~/.claude/hooks/");
21470
- console.log("- Would update ~/.claude/settings.json");
21471
- writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "hooks", "contextstream-redirect.py"), status: "dry-run" });
21472
- writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "hooks", "contextstream-reminder.py"), status: "dry-run" });
21473
- writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "settings.json"), status: "dry-run" });
21474
- } else {
21475
- const result = await installClaudeCodeHooks({ scope: "user" });
21476
- result.scripts.forEach((script) => {
21477
- writeActions.push({ kind: "mcp-config", target: script, status: "created" });
21478
- console.log(`- Created hook: ${script}`);
21479
- });
21480
- result.settings.forEach((settings) => {
21481
- writeActions.push({ kind: "mcp-config", target: settings, status: "updated" });
21482
- console.log(`- Updated settings: ${settings}`);
22918
+ for (const editor of hookEligibleEditors) {
22919
+ const hookEditor = HOOKS_SUPPORTED_EDITORS[editor];
22920
+ if (!hookEditor) continue;
22921
+ try {
22922
+ if (dryRun) {
22923
+ console.log(`- ${EDITOR_LABELS[editor]}: would install hooks`);
22924
+ continue;
22925
+ }
22926
+ const result = await installEditorHooks({
22927
+ editor: hookEditor,
22928
+ scope: "global"
21483
22929
  });
22930
+ for (const script of result.installed) {
22931
+ writeActions.push({ kind: "hooks", target: script, status: "created" });
22932
+ console.log(`- ${EDITOR_LABELS[editor]}: installed ${path8.basename(script)}`);
22933
+ }
22934
+ } catch (err) {
22935
+ const message = err instanceof Error ? err.message : String(err);
22936
+ console.log(`- ${EDITOR_LABELS[editor]}: failed to install hooks: ${message}`);
21484
22937
  }
21485
- console.log(" Hooks installed. Disable with CONTEXTSTREAM_HOOK_ENABLED=false");
21486
- } catch (err) {
21487
- const message = err instanceof Error ? err.message : String(err);
21488
- console.log(`- Failed to install hooks: ${message}`);
21489
22938
  }
22939
+ console.log(" Hooks installed. Disable with CONTEXTSTREAM_HOOK_ENABLED=false");
21490
22940
  } else {
21491
22941
  console.log("- Skipped hooks installation.");
21492
- console.log(" Note: Without hooks, Claude may still use default tools instead of ContextStream.");
22942
+ console.log(" Note: Without hooks, AI may still use default tools instead of ContextStream.");
21493
22943
  }
21494
22944
  }
21495
22945
  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");