@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.
- package/dist/index.js +1662 -212
- 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
|
-
##
|
|
8875
|
+
## ContextStream Rules
|
|
8876
8876
|
|
|
8877
8877
|
<contextstream_rules>
|
|
8878
|
-
1. **
|
|
8879
|
-
2. **
|
|
8880
|
-
3. **
|
|
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
|
-
**
|
|
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
|
|
8944
|
-
-
|
|
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 -
|
|
8969
|
+
## TL;DR - WHEN TO USE CONTEXT
|
|
8969
8970
|
|
|
8970
|
-
|
|
|
8971
|
-
|
|
8972
|
-
|
|
|
8973
|
-
| **\
|
|
8974
|
-
|
|
|
8975
|
-
| **\
|
|
8976
|
-
|
|
|
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
|
-
|
|
8979
|
+
### Simple Utility Operations - FAST PATH
|
|
8982
8980
|
|
|
8983
|
-
**
|
|
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
|
|
8998
|
-
- **\`session_init\`** - Initialize session with workspace detection + context
|
|
8999
|
-
- **\`context_smart\`** - Semantic search for relevant context (
|
|
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
|
|
9067
|
+
### Rules, Version & Lessons Notices
|
|
9068
|
+
|
|
9069
|
+
**[RULES_NOTICE]** - Update rules via \`generate_rules()\` (or rerun setup).
|
|
9047
9070
|
|
|
9048
|
-
|
|
9049
|
-
|
|
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
|
|
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
|
-
|
|
|
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
|
-
**
|
|
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
|
-
##
|
|
9309
|
+
## ContextStream Rules
|
|
9270
9310
|
|
|
9271
9311
|
<contextstream_rules>
|
|
9272
|
-
1. **
|
|
9273
|
-
2. **
|
|
9274
|
-
3. **
|
|
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
|
-
**
|
|
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
|
-
###
|
|
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 \`
|
|
9330
|
-
-
|
|
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
|
-
|
|
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": "...
|
|
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
|
-
#
|
|
9848
|
-
|
|
9849
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
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
|
-
|
|
10097
|
+
Active files detected: {files_list}
|
|
10098
|
+
Tool calls in session: {transcript_data.get('tool_call_count', 0)}
|
|
9861
10099
|
|
|
9862
|
-
|
|
9863
|
-
|
|
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
|
|
9966
|
-
const
|
|
9967
|
-
const
|
|
9968
|
-
await fs4.mkdir(
|
|
9969
|
-
await fs4.writeFile(
|
|
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
|
|
9972
|
-
|
|
9973
|
-
|
|
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
|
|
9986
|
-
const
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
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
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
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,
|
|
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
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
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
|
-
|
|
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
|
|
10028
|
-
const
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
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
|
-
\
|
|
10128
|
-
|
|
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
|
|
10168
|
-
|
|
10169
|
-
|
|
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
|
-
|
|
10172
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
14845
|
+
const listResult = await client.listMemoryEvents({
|
|
13854
14846
|
workspace_id: workspaceIdForRestore,
|
|
13855
14847
|
project_id: projectIdForRestore,
|
|
13856
|
-
|
|
13857
|
-
event_types: ["session_snapshot"],
|
|
13858
|
-
limit: 1
|
|
14848
|
+
limit: 50
|
|
13859
14849
|
});
|
|
13860
|
-
const
|
|
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
|
|
15026
|
-
|
|
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
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
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
|
-
|
|
15040
|
-
|
|
16111
|
+
hooksResults = [];
|
|
16112
|
+
const allHookResults = await installAllEditorHooks({
|
|
16113
|
+
scope: "global",
|
|
16114
|
+
editors: hookSupportedEditors,
|
|
15041
16115
|
includePreCompact: input.include_pre_compact
|
|
15042
16116
|
});
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
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 (
|
|
15052
|
-
hooksPrompt = "Hooks skipped.
|
|
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
|
-
|
|
15472
|
-
const
|
|
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:
|
|
16672
|
+
text: finalContext + footer + allWarnings
|
|
15508
16673
|
}
|
|
15509
16674
|
],
|
|
15510
|
-
structuredContent: toStructured(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
21452
|
-
console.log("
|
|
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
|
|
21457
|
-
console.log(" \u2713
|
|
21458
|
-
console.log(" \u2713 Inject reminders
|
|
21459
|
-
console.log("
|
|
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
|
|
22915
|
+
await rl.question("Install editor hooks? [Y/n] (recommended): ")
|
|
21465
22916
|
).toLowerCase();
|
|
21466
22917
|
if (installHooks !== "n" && installHooks !== "no") {
|
|
21467
|
-
|
|
21468
|
-
|
|
21469
|
-
|
|
21470
|
-
|
|
21471
|
-
|
|
21472
|
-
|
|
21473
|
-
|
|
21474
|
-
|
|
21475
|
-
const result = await
|
|
21476
|
-
|
|
21477
|
-
|
|
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,
|
|
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");
|