@contextstream/mcp-server 0.4.37 → 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 +2495 -296
- package/dist/test-server.js +5 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var
|
|
8
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
9
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
+
}) : x)(function(x) {
|
|
11
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
|
+
});
|
|
14
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
9
15
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
16
|
};
|
|
11
17
|
var __export = (target, all) => {
|
|
@@ -4676,7 +4682,8 @@ var configSchema = external_exports.object({
|
|
|
4676
4682
|
defaultProjectId: external_exports.string().uuid().optional(),
|
|
4677
4683
|
userAgent: external_exports.string().default(`contextstream-mcp/${VERSION}`),
|
|
4678
4684
|
allowHeaderAuth: external_exports.boolean().optional(),
|
|
4679
|
-
contextPackEnabled: external_exports.boolean().default(true)
|
|
4685
|
+
contextPackEnabled: external_exports.boolean().default(true),
|
|
4686
|
+
showTiming: external_exports.boolean().default(false)
|
|
4680
4687
|
});
|
|
4681
4688
|
var MISSING_CREDENTIALS_ERROR = "Set CONTEXTSTREAM_API_KEY or CONTEXTSTREAM_JWT for authentication (or CONTEXTSTREAM_ALLOW_HEADER_AUTH=true for header-based auth).";
|
|
4682
4689
|
function isMissingCredentialsError(err) {
|
|
@@ -4690,6 +4697,7 @@ function loadConfig() {
|
|
|
4690
4697
|
const contextPackEnabled = parseBooleanEnv(
|
|
4691
4698
|
process.env.CONTEXTSTREAM_CONTEXT_PACK ?? process.env.CONTEXTSTREAM_CONTEXT_PACK_ENABLED
|
|
4692
4699
|
);
|
|
4700
|
+
const showTiming = parseBooleanEnv(process.env.CONTEXTSTREAM_SHOW_TIMING);
|
|
4693
4701
|
const parsed = configSchema.safeParse({
|
|
4694
4702
|
apiUrl: process.env.CONTEXTSTREAM_API_URL,
|
|
4695
4703
|
apiKey: process.env.CONTEXTSTREAM_API_KEY,
|
|
@@ -4698,7 +4706,8 @@ function loadConfig() {
|
|
|
4698
4706
|
defaultProjectId: process.env.CONTEXTSTREAM_PROJECT_ID,
|
|
4699
4707
|
userAgent: process.env.CONTEXTSTREAM_USER_AGENT,
|
|
4700
4708
|
allowHeaderAuth,
|
|
4701
|
-
contextPackEnabled
|
|
4709
|
+
contextPackEnabled,
|
|
4710
|
+
showTiming
|
|
4702
4711
|
});
|
|
4703
4712
|
if (!parsed.success) {
|
|
4704
4713
|
const missing = parsed.error.errors.map((e) => e.path.join(".")).join(", ");
|
|
@@ -5572,6 +5581,47 @@ var INGEST_BENEFITS = [
|
|
|
5572
5581
|
"Allow the AI assistant to find relevant code without manual file navigation",
|
|
5573
5582
|
"Build a searchable knowledge base of your codebase structure"
|
|
5574
5583
|
];
|
|
5584
|
+
var PROJECT_MARKERS = [
|
|
5585
|
+
".git",
|
|
5586
|
+
"package.json",
|
|
5587
|
+
"Cargo.toml",
|
|
5588
|
+
"pyproject.toml",
|
|
5589
|
+
"go.mod",
|
|
5590
|
+
"pom.xml",
|
|
5591
|
+
"build.gradle",
|
|
5592
|
+
"Gemfile",
|
|
5593
|
+
"composer.json",
|
|
5594
|
+
".contextstream"
|
|
5595
|
+
];
|
|
5596
|
+
function isMultiProjectFolder(folderPath) {
|
|
5597
|
+
try {
|
|
5598
|
+
const fs8 = __require("fs");
|
|
5599
|
+
const pathModule = __require("path");
|
|
5600
|
+
const rootHasGit = fs8.existsSync(pathModule.join(folderPath, ".git"));
|
|
5601
|
+
const entries = fs8.readdirSync(folderPath, { withFileTypes: true });
|
|
5602
|
+
const subdirs = entries.filter(
|
|
5603
|
+
(e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules"
|
|
5604
|
+
);
|
|
5605
|
+
const projectSubdirs = [];
|
|
5606
|
+
for (const subdir of subdirs) {
|
|
5607
|
+
const subdirPath = pathModule.join(folderPath, subdir.name);
|
|
5608
|
+
for (const marker of PROJECT_MARKERS) {
|
|
5609
|
+
if (fs8.existsSync(pathModule.join(subdirPath, marker))) {
|
|
5610
|
+
projectSubdirs.push(subdir.name);
|
|
5611
|
+
break;
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5615
|
+
const isMultiProject = projectSubdirs.length >= 1 && (!rootHasGit || projectSubdirs.length >= 2);
|
|
5616
|
+
return {
|
|
5617
|
+
isMultiProject,
|
|
5618
|
+
projectCount: projectSubdirs.length,
|
|
5619
|
+
projectNames: projectSubdirs
|
|
5620
|
+
};
|
|
5621
|
+
} catch {
|
|
5622
|
+
return { isMultiProject: false, projectCount: 0, projectNames: [] };
|
|
5623
|
+
}
|
|
5624
|
+
}
|
|
5575
5625
|
var ContextStreamClient = class {
|
|
5576
5626
|
constructor(config) {
|
|
5577
5627
|
this.config = config;
|
|
@@ -6607,7 +6657,25 @@ var ContextStreamClient = class {
|
|
|
6607
6657
|
return context;
|
|
6608
6658
|
}
|
|
6609
6659
|
}
|
|
6610
|
-
|
|
6660
|
+
let autoDetectedMultiProject = false;
|
|
6661
|
+
if (!params.skip_project_creation && !projectId && rootPath) {
|
|
6662
|
+
const detection = isMultiProjectFolder(rootPath);
|
|
6663
|
+
if (detection.isMultiProject) {
|
|
6664
|
+
autoDetectedMultiProject = true;
|
|
6665
|
+
context.workspace_only_mode = true;
|
|
6666
|
+
context.auto_detected_multi_project = true;
|
|
6667
|
+
context.detected_projects = detection.projectNames;
|
|
6668
|
+
context.project_skipped_reason = `Auto-detected ${detection.projectCount} projects in folder: ${detection.projectNames.slice(0, 5).join(", ")}${detection.projectCount > 5 ? "..." : ""}. Working at workspace level.`;
|
|
6669
|
+
console.error(
|
|
6670
|
+
`[ContextStream] Auto-detected multi-project folder with ${detection.projectCount} projects: ${detection.projectNames.slice(0, 5).join(", ")}`
|
|
6671
|
+
);
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
if (params.skip_project_creation) {
|
|
6675
|
+
context.workspace_only_mode = true;
|
|
6676
|
+
context.project_skipped_reason = "skip_project_creation=true - working at workspace level for multi-project folder";
|
|
6677
|
+
} else if (autoDetectedMultiProject) {
|
|
6678
|
+
} else if (!projectId && workspaceId && rootPath && params.auto_index !== false) {
|
|
6611
6679
|
const projectName = path4.basename(rootPath) || "My Project";
|
|
6612
6680
|
try {
|
|
6613
6681
|
const projects = await this.listProjects({ workspace_id: workspaceId });
|
|
@@ -6918,11 +6986,16 @@ var ContextStreamClient = class {
|
|
|
6918
6986
|
* Persists the selection to .contextstream/config.json for future sessions.
|
|
6919
6987
|
*/
|
|
6920
6988
|
async associateWorkspace(params) {
|
|
6921
|
-
const { folder_path, workspace_id, workspace_name, create_parent_mapping } = params;
|
|
6989
|
+
const { folder_path, workspace_id, workspace_name, create_parent_mapping, version, configured_editors, context_pack, api_url } = params;
|
|
6922
6990
|
const saved = writeLocalConfig(folder_path, {
|
|
6923
6991
|
workspace_id,
|
|
6924
6992
|
workspace_name,
|
|
6925
|
-
associated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6993
|
+
associated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6994
|
+
version,
|
|
6995
|
+
configured_editors,
|
|
6996
|
+
context_pack,
|
|
6997
|
+
api_url,
|
|
6998
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6926
6999
|
});
|
|
6927
7000
|
if (create_parent_mapping) {
|
|
6928
7001
|
const parentDir = path4.dirname(folder_path);
|
|
@@ -7593,7 +7666,10 @@ var ContextStreamClient = class {
|
|
|
7593
7666
|
distill: params.distill,
|
|
7594
7667
|
client_version: VERSION,
|
|
7595
7668
|
rules_version: VERSION,
|
|
7596
|
-
notice_inline: false
|
|
7669
|
+
notice_inline: false,
|
|
7670
|
+
// Session token tracking for context pressure
|
|
7671
|
+
...params.session_tokens !== void 0 && { session_tokens: params.session_tokens },
|
|
7672
|
+
...params.context_threshold !== void 0 && { context_threshold: params.context_threshold }
|
|
7597
7673
|
}
|
|
7598
7674
|
});
|
|
7599
7675
|
const data = unwrapApiResponse(apiResult);
|
|
@@ -7613,7 +7689,8 @@ var ContextStreamClient = class {
|
|
|
7613
7689
|
project_id: withDefaults.project_id,
|
|
7614
7690
|
...versionNotice2 ? { version_notice: versionNotice2 } : {},
|
|
7615
7691
|
...Array.isArray(data?.errors) ? { errors: data.errors } : {},
|
|
7616
|
-
...this.indexRefreshInProgress ? { index_status: "refreshing" } : {}
|
|
7692
|
+
...this.indexRefreshInProgress ? { index_status: "refreshing" } : {},
|
|
7693
|
+
...data?.context_pressure ? { context_pressure: data.context_pressure } : {}
|
|
7617
7694
|
};
|
|
7618
7695
|
} catch (err) {
|
|
7619
7696
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
@@ -8795,16 +8872,15 @@ function applyMcpToolPrefix(markdown, toolPrefix) {
|
|
|
8795
8872
|
return markdown.replace(toolRegex, `${toolPrefix}$1`);
|
|
8796
8873
|
}
|
|
8797
8874
|
var CONTEXTSTREAM_RULES_FULL = `
|
|
8798
|
-
##
|
|
8875
|
+
## ContextStream Rules
|
|
8799
8876
|
|
|
8800
8877
|
<contextstream_rules>
|
|
8801
|
-
1. **
|
|
8802
|
-
2. **
|
|
8803
|
-
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
|
|
8804
8881
|
</contextstream_rules>
|
|
8805
8882
|
|
|
8806
|
-
**
|
|
8807
|
-
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.**
|
|
8808
8884
|
|
|
8809
8885
|
---
|
|
8810
8886
|
|
|
@@ -8863,8 +8939,10 @@ This applies to **EVERY search** throughout the **ENTIRE conversation**, not jus
|
|
|
8863
8939
|
- **Apply the prevention steps** from each lesson to avoid repeating mistakes
|
|
8864
8940
|
|
|
8865
8941
|
### On \`context_smart\`:
|
|
8866
|
-
- Check for
|
|
8867
|
-
-
|
|
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
|
|
8868
8946
|
|
|
8869
8947
|
### Before ANY Non-Trivial Work:
|
|
8870
8948
|
**ALWAYS call \`session(action="get_lessons", query="<topic>")\`** where \`<topic>\` matches what you're about to do:
|
|
@@ -8888,22 +8966,42 @@ You have access to ContextStream MCP tools for persistent memory and context.
|
|
|
8888
8966
|
v0.4.x uses **~11 consolidated domain tools** for ~75% token reduction vs previous versions.
|
|
8889
8967
|
Rules Version: ${RULES_VERSION}
|
|
8890
8968
|
|
|
8891
|
-
## TL;DR -
|
|
8969
|
+
## TL;DR - WHEN TO USE CONTEXT
|
|
8970
|
+
|
|
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", ...)\` |
|
|
8978
|
+
|
|
8979
|
+
### Simple Utility Operations - FAST PATH
|
|
8892
8980
|
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
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", ...)\` |
|
|
8903
8998
|
|
|
8904
|
-
**
|
|
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?"
|
|
8905
9003
|
|
|
8906
|
-
**First message rule:** After \`session_init\`:
|
|
9004
|
+
**First message rule (for coding tasks):** After \`session_init\`:
|
|
8907
9005
|
1. Check for \`lessons\` in response - if present, READ and SUMMARIZE them to user
|
|
8908
9006
|
2. Then call \`context_smart\` before any other tool or response
|
|
8909
9007
|
|
|
@@ -8917,9 +9015,9 @@ Rules Version: ${RULES_VERSION}
|
|
|
8917
9015
|
|
|
8918
9016
|
v0.4.x consolidates ~58 individual tools into ~11 domain tools with action/mode dispatch:
|
|
8919
9017
|
|
|
8920
|
-
### Standalone Tools
|
|
8921
|
-
- **\`session_init\`** - Initialize session with workspace detection + context
|
|
8922
|
-
- **\`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)
|
|
8923
9021
|
|
|
8924
9022
|
### Domain Tools (Use action/mode parameter)
|
|
8925
9023
|
|
|
@@ -8966,10 +9064,21 @@ If context still feels missing, use \`session(action="recall", query="...")\` fo
|
|
|
8966
9064
|
|
|
8967
9065
|
---
|
|
8968
9066
|
|
|
8969
|
-
### Rules
|
|
9067
|
+
### Rules, Version & Lessons Notices
|
|
8970
9068
|
|
|
8971
|
-
|
|
8972
|
-
|
|
9069
|
+
**[RULES_NOTICE]** - Update rules via \`generate_rules()\` (or rerun setup).
|
|
9070
|
+
|
|
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.
|
|
8973
9082
|
|
|
8974
9083
|
---
|
|
8975
9084
|
|
|
@@ -8981,6 +9090,39 @@ If context still feels missing, use \`session(action="recall", query="...")\` fo
|
|
|
8981
9090
|
|
|
8982
9091
|
---
|
|
8983
9092
|
|
|
9093
|
+
### Context Pressure & Compaction Awareness
|
|
9094
|
+
|
|
9095
|
+
ContextStream tracks context pressure to help you stay ahead of conversation compaction:
|
|
9096
|
+
|
|
9097
|
+
**Automatic tracking:** Token usage is tracked automatically. \`context_smart\` returns \`context_pressure\` when usage is high.
|
|
9098
|
+
|
|
9099
|
+
**When \`context_smart\` returns \`context_pressure\` with high/critical level:**
|
|
9100
|
+
1. Review the \`suggested_action\` field:
|
|
9101
|
+
- \`prepare_save\`: Start thinking about saving important state
|
|
9102
|
+
- \`save_now\`: Immediately call \`session(action="capture", event_type="session_snapshot")\` to preserve state
|
|
9103
|
+
|
|
9104
|
+
**PreCompact Hook (Optional):** If enabled, Claude Code will inject a reminder to save state before compaction.
|
|
9105
|
+
Enable with: \`generate_rules(install_hooks=true, include_pre_compact=true)\`
|
|
9106
|
+
|
|
9107
|
+
**Before compaction happens (when warned):**
|
|
9108
|
+
\`\`\`
|
|
9109
|
+
session(action="capture", event_type="session_snapshot", title="Pre-compaction snapshot", content="{
|
|
9110
|
+
\\"conversation_summary\\": \\"<summarize what we've been doing>\\",
|
|
9111
|
+
\\"current_goal\\": \\"<the main task>\\",
|
|
9112
|
+
\\"active_files\\": [\\"file1.ts\\", \\"file2.ts\\"],
|
|
9113
|
+
\\"recent_decisions\\": [{title: \\"...\\", rationale: \\"...\\"}],
|
|
9114
|
+
\\"unfinished_work\\": [{task: \\"...\\", status: \\"...\\", next_steps: \\"...\\"}]
|
|
9115
|
+
}")
|
|
9116
|
+
\`\`\`
|
|
9117
|
+
|
|
9118
|
+
**After compaction (when context seems lost):**
|
|
9119
|
+
1. Call \`session_init(folder_path="...", is_post_compact=true)\` - this auto-restores the most recent snapshot
|
|
9120
|
+
2. Or call \`session_restore_context()\` directly to get the saved state
|
|
9121
|
+
3. Review the \`restored_context\` to understand prior work
|
|
9122
|
+
4. Acknowledge to the user what was restored and continue
|
|
9123
|
+
|
|
9124
|
+
---
|
|
9125
|
+
|
|
8984
9126
|
### Index Status (Auto-Managed)
|
|
8985
9127
|
|
|
8986
9128
|
**Indexing is automatic.** After \`session_init\`, the project is auto-indexed in the background.
|
|
@@ -9067,16 +9209,24 @@ If ContextStream returns results, stop and use them. NEVER use local Search/Expl
|
|
|
9067
9209
|
|
|
9068
9210
|
---
|
|
9069
9211
|
|
|
9070
|
-
### When to Capture
|
|
9212
|
+
### When to Capture
|
|
9071
9213
|
|
|
9072
9214
|
| When | Call | Example |
|
|
9073
9215
|
|------|------|---------|
|
|
9074
9216
|
| User makes decision | \`session(action="capture", event_type="decision", ...)\` | "Let's use PostgreSQL" |
|
|
9075
9217
|
| User states preference | \`session(action="capture", event_type="preference", ...)\` | "I prefer TypeScript" |
|
|
9076
|
-
|
|
|
9218
|
+
| Complete significant task | \`session(action="capture", event_type="task", ...)\` | Capture what was done |
|
|
9077
9219
|
| Need past context | \`session(action="recall", query="...")\` | "What did we decide about X?" |
|
|
9078
9220
|
|
|
9079
|
-
**
|
|
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
|
|
9080
9230
|
|
|
9081
9231
|
---
|
|
9082
9232
|
|
|
@@ -9156,16 +9306,15 @@ memory(action="create_task", title="<task title>", plan_id="<plan_id from step 1
|
|
|
9156
9306
|
See full documentation: https://contextstream.io/docs/mcp/tools
|
|
9157
9307
|
`.trim();
|
|
9158
9308
|
var CONTEXTSTREAM_RULES_MINIMAL = `
|
|
9159
|
-
##
|
|
9309
|
+
## ContextStream Rules
|
|
9160
9310
|
|
|
9161
9311
|
<contextstream_rules>
|
|
9162
|
-
1. **
|
|
9163
|
-
2. **
|
|
9164
|
-
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
|
|
9165
9315
|
</contextstream_rules>
|
|
9166
9316
|
|
|
9167
|
-
**
|
|
9168
|
-
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.**
|
|
9169
9318
|
|
|
9170
9319
|
---
|
|
9171
9320
|
|
|
@@ -9174,7 +9323,7 @@ This is a self-referential rule that ensures these instructions stay in context.
|
|
|
9174
9323
|
Rules Version: ${RULES_VERSION}
|
|
9175
9324
|
**Note:** PreToolUse hooks block Glob/Grep/Search when ContextStream is available.
|
|
9176
9325
|
|
|
9177
|
-
###
|
|
9326
|
+
### For Coding Tasks
|
|
9178
9327
|
|
|
9179
9328
|
| Action | Tool Call |
|
|
9180
9329
|
|--------|-----------|
|
|
@@ -9208,18 +9357,79 @@ ContextStream search is **indexed** and returns semantic matches + context in ON
|
|
|
9208
9357
|
| \`memory\` | \`memory(action="list_events", limit=10)\` |
|
|
9209
9358
|
| \`graph\` | \`graph(action="dependencies", file_path="...")\` |
|
|
9210
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
|
+
|
|
9211
9387
|
### Lessons (Past Mistakes)
|
|
9212
9388
|
|
|
9213
9389
|
- After \`session_init\`: Check for \`lessons\` field and apply before work
|
|
9214
9390
|
- Before risky work: \`session(action="get_lessons", query="<topic>")\`
|
|
9215
9391
|
- On mistakes: \`session(action="capture_lesson", title="...", trigger="...", impact="...", prevention="...")\`
|
|
9216
9392
|
|
|
9393
|
+
### Context Pressure & Compaction
|
|
9394
|
+
|
|
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
|
|
9416
|
+
|
|
9217
9417
|
### Plans & Tasks
|
|
9218
9418
|
|
|
9219
9419
|
When user asks for a plan, use ContextStream (not EnterPlanMode):
|
|
9220
9420
|
1. \`session(action="capture_plan", title="...", steps=[...])\`
|
|
9221
9421
|
2. \`memory(action="create_task", title="...", plan_id="<id>")\`
|
|
9222
9422
|
|
|
9423
|
+
### Workspace-Only Mode (Multi-Project Folders)
|
|
9424
|
+
|
|
9425
|
+
If working in a parent folder containing multiple projects:
|
|
9426
|
+
\`\`\`
|
|
9427
|
+
session_init(folder_path="...", skip_project_creation=true)
|
|
9428
|
+
\`\`\`
|
|
9429
|
+
|
|
9430
|
+
This enables workspace-level memory and context without project-specific indexing.
|
|
9431
|
+
Use for monorepos or folders with multiple independent projects.
|
|
9432
|
+
|
|
9223
9433
|
Full docs: https://contextstream.io/docs/mcp/tools
|
|
9224
9434
|
`.trim();
|
|
9225
9435
|
var TEMPLATES = {
|
|
@@ -9672,6 +9882,234 @@ def main():
|
|
|
9672
9882
|
print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": REMINDER}}))
|
|
9673
9883
|
sys.exit(0)
|
|
9674
9884
|
|
|
9885
|
+
if __name__ == "__main__":
|
|
9886
|
+
main()
|
|
9887
|
+
`;
|
|
9888
|
+
var PRECOMPACT_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
9889
|
+
"""
|
|
9890
|
+
ContextStream PreCompact Hook for Claude Code
|
|
9891
|
+
|
|
9892
|
+
Runs BEFORE conversation context is compacted (manual via /compact or automatic).
|
|
9893
|
+
Automatically saves conversation state to ContextStream by parsing the transcript.
|
|
9894
|
+
|
|
9895
|
+
Input (via stdin):
|
|
9896
|
+
{
|
|
9897
|
+
"session_id": "...",
|
|
9898
|
+
"transcript_path": "/path/to/transcript.jsonl",
|
|
9899
|
+
"permission_mode": "default",
|
|
9900
|
+
"hook_event_name": "PreCompact",
|
|
9901
|
+
"trigger": "manual" | "auto",
|
|
9902
|
+
"custom_instructions": "..."
|
|
9903
|
+
}
|
|
9904
|
+
|
|
9905
|
+
Output (to stdout):
|
|
9906
|
+
{
|
|
9907
|
+
"hookSpecificOutput": {
|
|
9908
|
+
"hookEventName": "PreCompact",
|
|
9909
|
+
"additionalContext": "... status message ..."
|
|
9910
|
+
}
|
|
9911
|
+
}
|
|
9912
|
+
"""
|
|
9913
|
+
|
|
9914
|
+
import json
|
|
9915
|
+
import sys
|
|
9916
|
+
import os
|
|
9917
|
+
import re
|
|
9918
|
+
import urllib.request
|
|
9919
|
+
import urllib.error
|
|
9920
|
+
|
|
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)
|
|
10060
|
+
|
|
10061
|
+
def main():
|
|
10062
|
+
if not ENABLED:
|
|
10063
|
+
sys.exit(0)
|
|
10064
|
+
|
|
10065
|
+
try:
|
|
10066
|
+
data = json.load(sys.stdin)
|
|
10067
|
+
except:
|
|
10068
|
+
sys.exit(0)
|
|
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", "")
|
|
10076
|
+
trigger = data.get("trigger", "unknown")
|
|
10077
|
+
custom_instructions = data.get("custom_instructions", "")
|
|
10078
|
+
|
|
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)
|
|
10083
|
+
|
|
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}]"
|
|
10092
|
+
|
|
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}
|
|
10096
|
+
|
|
10097
|
+
Active files detected: {files_list}
|
|
10098
|
+
Tool calls in session: {transcript_data.get('tool_call_count', 0)}
|
|
10099
|
+
|
|
10100
|
+
After compaction, call session_init(is_post_compact=true) to restore context.
|
|
10101
|
+
{f"User instructions: {custom_instructions}" if custom_instructions else ""}"""
|
|
10102
|
+
|
|
10103
|
+
output = {
|
|
10104
|
+
"hookSpecificOutput": {
|
|
10105
|
+
"hookEventName": "PreCompact",
|
|
10106
|
+
"additionalContext": context
|
|
10107
|
+
}
|
|
10108
|
+
}
|
|
10109
|
+
|
|
10110
|
+
print(json.dumps(output))
|
|
10111
|
+
sys.exit(0)
|
|
10112
|
+
|
|
9675
10113
|
if __name__ == "__main__":
|
|
9676
10114
|
main()
|
|
9677
10115
|
`;
|
|
@@ -9687,11 +10125,12 @@ function getClaudeSettingsPath(scope, projectPath) {
|
|
|
9687
10125
|
function getHooksDir() {
|
|
9688
10126
|
return path5.join(homedir2(), ".claude", "hooks");
|
|
9689
10127
|
}
|
|
9690
|
-
function buildHooksConfig() {
|
|
10128
|
+
function buildHooksConfig(options) {
|
|
9691
10129
|
const hooksDir = getHooksDir();
|
|
9692
10130
|
const preToolUsePath = path5.join(hooksDir, "contextstream-redirect.py");
|
|
9693
10131
|
const userPromptPath = path5.join(hooksDir, "contextstream-reminder.py");
|
|
9694
|
-
|
|
10132
|
+
const preCompactPath = path5.join(hooksDir, "contextstream-precompact.py");
|
|
10133
|
+
const config = {
|
|
9695
10134
|
PreToolUse: [
|
|
9696
10135
|
{
|
|
9697
10136
|
matcher: "Glob|Grep|Search|Task|EnterPlanMode",
|
|
@@ -9717,15 +10156,40 @@ function buildHooksConfig() {
|
|
|
9717
10156
|
}
|
|
9718
10157
|
]
|
|
9719
10158
|
};
|
|
10159
|
+
if (options?.includePreCompact) {
|
|
10160
|
+
config.PreCompact = [
|
|
10161
|
+
{
|
|
10162
|
+
// Match both manual (/compact) and automatic compaction
|
|
10163
|
+
matcher: "*",
|
|
10164
|
+
hooks: [
|
|
10165
|
+
{
|
|
10166
|
+
type: "command",
|
|
10167
|
+
command: `python3 "${preCompactPath}"`,
|
|
10168
|
+
timeout: 10
|
|
10169
|
+
}
|
|
10170
|
+
]
|
|
10171
|
+
}
|
|
10172
|
+
];
|
|
10173
|
+
}
|
|
10174
|
+
return config;
|
|
9720
10175
|
}
|
|
9721
|
-
async function installHookScripts() {
|
|
10176
|
+
async function installHookScripts(options) {
|
|
9722
10177
|
const hooksDir = getHooksDir();
|
|
9723
10178
|
await fs4.mkdir(hooksDir, { recursive: true });
|
|
9724
10179
|
const preToolUsePath = path5.join(hooksDir, "contextstream-redirect.py");
|
|
9725
10180
|
const userPromptPath = path5.join(hooksDir, "contextstream-reminder.py");
|
|
10181
|
+
const preCompactPath = path5.join(hooksDir, "contextstream-precompact.py");
|
|
9726
10182
|
await fs4.writeFile(preToolUsePath, PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
|
|
9727
10183
|
await fs4.writeFile(userPromptPath, USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
|
|
9728
|
-
|
|
10184
|
+
const result = {
|
|
10185
|
+
preToolUse: preToolUsePath,
|
|
10186
|
+
userPrompt: userPromptPath
|
|
10187
|
+
};
|
|
10188
|
+
if (options?.includePreCompact) {
|
|
10189
|
+
await fs4.writeFile(preCompactPath, PRECOMPACT_HOOK_SCRIPT, { mode: 493 });
|
|
10190
|
+
result.preCompact = preCompactPath;
|
|
10191
|
+
}
|
|
10192
|
+
return result;
|
|
9729
10193
|
}
|
|
9730
10194
|
async function readClaudeSettings(scope, projectPath) {
|
|
9731
10195
|
const settingsPath = getClaudeSettingsPath(scope, projectPath);
|
|
@@ -9756,39 +10220,6 @@ function mergeHooksIntoSettings(existingSettings, newHooks) {
|
|
|
9756
10220
|
settings.hooks = existingHooks;
|
|
9757
10221
|
return settings;
|
|
9758
10222
|
}
|
|
9759
|
-
async function installClaudeCodeHooks(options) {
|
|
9760
|
-
const result = { scripts: [], settings: [] };
|
|
9761
|
-
if (!options.dryRun) {
|
|
9762
|
-
const scripts = await installHookScripts();
|
|
9763
|
-
result.scripts.push(scripts.preToolUse, scripts.userPrompt);
|
|
9764
|
-
} else {
|
|
9765
|
-
const hooksDir = getHooksDir();
|
|
9766
|
-
result.scripts.push(
|
|
9767
|
-
path5.join(hooksDir, "contextstream-redirect.py"),
|
|
9768
|
-
path5.join(hooksDir, "contextstream-reminder.py")
|
|
9769
|
-
);
|
|
9770
|
-
}
|
|
9771
|
-
const hooksConfig = buildHooksConfig();
|
|
9772
|
-
if (options.scope === "user" || options.scope === "both") {
|
|
9773
|
-
const settingsPath = getClaudeSettingsPath("user");
|
|
9774
|
-
if (!options.dryRun) {
|
|
9775
|
-
const existing = await readClaudeSettings("user");
|
|
9776
|
-
const merged = mergeHooksIntoSettings(existing, hooksConfig);
|
|
9777
|
-
await writeClaudeSettings(merged, "user");
|
|
9778
|
-
}
|
|
9779
|
-
result.settings.push(settingsPath);
|
|
9780
|
-
}
|
|
9781
|
-
if ((options.scope === "project" || options.scope === "both") && options.projectPath) {
|
|
9782
|
-
const settingsPath = getClaudeSettingsPath("project", options.projectPath);
|
|
9783
|
-
if (!options.dryRun) {
|
|
9784
|
-
const existing = await readClaudeSettings("project", options.projectPath);
|
|
9785
|
-
const merged = mergeHooksIntoSettings(existing, hooksConfig);
|
|
9786
|
-
await writeClaudeSettings(merged, "project", options.projectPath);
|
|
9787
|
-
}
|
|
9788
|
-
result.settings.push(settingsPath);
|
|
9789
|
-
}
|
|
9790
|
-
return result;
|
|
9791
|
-
}
|
|
9792
10223
|
function getIndexStatusPath() {
|
|
9793
10224
|
return path5.join(homedir2(), ".contextstream", "indexed-projects.json");
|
|
9794
10225
|
}
|
|
@@ -9817,138 +10248,928 @@ async function markProjectIndexed(projectPath, options) {
|
|
|
9817
10248
|
};
|
|
9818
10249
|
await writeIndexStatus(status);
|
|
9819
10250
|
}
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
search_semantic: 4.5,
|
|
9831
|
-
search_hybrid: 4,
|
|
9832
|
-
search_keyword: 2.5,
|
|
9833
|
-
search_pattern: 3,
|
|
9834
|
-
search_exhaustive: 3.5,
|
|
9835
|
-
search_refactor: 3,
|
|
9836
|
-
// session: Recall/search replaces reading through history
|
|
9837
|
-
session_recall: 5,
|
|
9838
|
-
session_smart_search: 4,
|
|
9839
|
-
session_user_context: 3,
|
|
9840
|
-
session_summary: 4,
|
|
9841
|
-
// graph: Would require extensive file traversal
|
|
9842
|
-
graph_dependencies: 8,
|
|
9843
|
-
graph_impact: 10,
|
|
9844
|
-
graph_call_path: 8,
|
|
9845
|
-
graph_related: 6,
|
|
9846
|
-
// memory: Context retrieval
|
|
9847
|
-
memory_search: 3.5,
|
|
9848
|
-
memory_decisions: 3,
|
|
9849
|
-
memory_timeline: 3,
|
|
9850
|
-
memory_summary: 4
|
|
9851
|
-
};
|
|
9852
|
-
function clampCharCount(value) {
|
|
9853
|
-
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
9854
|
-
return Math.min(MAX_CHARS_PER_EVENT, Math.floor(value));
|
|
9855
|
-
}
|
|
9856
|
-
function trackToolTokenSavings(client, tool, contextText, params, extraMetadata) {
|
|
9857
|
-
try {
|
|
9858
|
-
const contextChars = clampCharCount(contextText.length);
|
|
9859
|
-
const multiplier = CANDIDATE_MULTIPLIERS[tool] ?? 3;
|
|
9860
|
-
const baseOverhead = contextChars > 0 ? BASE_OVERHEAD_CHARS : 0;
|
|
9861
|
-
const estimatedCandidate = Math.round(contextChars * multiplier + baseOverhead);
|
|
9862
|
-
const candidateChars = Math.max(contextChars, clampCharCount(estimatedCandidate));
|
|
9863
|
-
client.trackTokenSavings({
|
|
9864
|
-
tool,
|
|
9865
|
-
workspace_id: params?.workspace_id,
|
|
9866
|
-
project_id: params?.project_id,
|
|
9867
|
-
candidate_chars: candidateChars,
|
|
9868
|
-
context_chars: contextChars,
|
|
9869
|
-
max_tokens: params?.max_tokens,
|
|
9870
|
-
metadata: {
|
|
9871
|
-
method: "multiplier_estimate",
|
|
9872
|
-
formula_version: TOKEN_SAVINGS_FORMULA_VERSION,
|
|
9873
|
-
source: "mcp-server",
|
|
9874
|
-
multiplier,
|
|
9875
|
-
base_overhead_chars: baseOverhead,
|
|
9876
|
-
...extraMetadata ?? {}
|
|
9877
|
-
}
|
|
9878
|
-
}).catch(() => {
|
|
9879
|
-
});
|
|
9880
|
-
} catch {
|
|
9881
|
-
}
|
|
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"
|
|
9882
10261
|
}
|
|
10262
|
+
"""
|
|
9883
10263
|
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
\u26A0\uFE0F [SEARCH RULES - READ EVERY TIME]
|
|
9890
|
-
BEFORE using Glob/Grep/Read/Search/Explore \u2192 call mcp__contextstream__search(mode="hybrid", query="...") FIRST
|
|
9891
|
-
BEFORE using EnterPlanMode/Task(Plan) \u2192 call mcp__contextstream__session(action="capture_plan", ...) instead
|
|
9892
|
-
Local tools ONLY if ContextStream returns 0 results after retry.
|
|
9893
|
-
`.trim();
|
|
9894
|
-
var LESSONS_REMINDER_PREFIX = `
|
|
9895
|
-
\u26A0\uFE0F [LESSONS - REVIEW BEFORE CHANGES]
|
|
9896
|
-
Past mistakes found that may be relevant. STOP and review before proceeding:
|
|
9897
|
-
`.trim();
|
|
9898
|
-
function generateLessonsReminder(result) {
|
|
9899
|
-
const lessons = result.lessons;
|
|
9900
|
-
if (!lessons || lessons.length === 0) {
|
|
9901
|
-
return "";
|
|
9902
|
-
}
|
|
9903
|
-
const lessonLines = lessons.slice(0, 5).map((l, i) => {
|
|
9904
|
-
const severity = l.severity === "critical" ? "\u{1F6A8}" : l.severity === "high" ? "\u26A0\uFE0F" : "\u{1F4DD}";
|
|
9905
|
-
const title = l.title || "Untitled lesson";
|
|
9906
|
-
const prevention = l.prevention || l.trigger || "";
|
|
9907
|
-
return `${i + 1}. ${severity} ${title}${prevention ? `: ${prevention.slice(0, 100)}` : ""}`;
|
|
9908
|
-
});
|
|
9909
|
-
return `
|
|
10264
|
+
import json
|
|
10265
|
+
import sys
|
|
10266
|
+
import os
|
|
10267
|
+
from pathlib import Path
|
|
10268
|
+
from datetime import datetime, timedelta
|
|
9910
10269
|
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
function generateRulesUpdateWarning(rulesNotice) {
|
|
9915
|
-
if (!rulesNotice || rulesNotice.status !== "behind" && rulesNotice.status !== "missing") {
|
|
9916
|
-
return "";
|
|
9917
|
-
}
|
|
9918
|
-
const isMissing = rulesNotice.status === "missing";
|
|
9919
|
-
const current = rulesNotice.current ?? "none";
|
|
9920
|
-
const latest = rulesNotice.latest;
|
|
9921
|
-
return `
|
|
9922
|
-
\u{1F6A8} [RULES ${isMissing ? "MISSING" : "OUTDATED"} - ACTION REQUIRED]
|
|
9923
|
-
${isMissing ? "ContextStream rules are NOT installed." : `Rules version ${current} \u2192 ${latest} available.`}
|
|
9924
|
-
${isMissing ? "AI behavior may be suboptimal without proper rules." : "New rules include important improvements for better AI behavior."}
|
|
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
|
|
9925
10273
|
|
|
9926
|
-
|
|
9927
|
-
This is SAFE - only the ContextStream block is updated, your custom rules are preserved.
|
|
9928
|
-
`.trim();
|
|
9929
|
-
}
|
|
9930
|
-
function generateVersionUpdateWarning(versionNotice) {
|
|
9931
|
-
if (!versionNotice?.behind) {
|
|
9932
|
-
return "";
|
|
9933
|
-
}
|
|
9934
|
-
return `
|
|
9935
|
-
\u{1F6A8} [MCP SERVER OUTDATED - UPDATE RECOMMENDED]
|
|
9936
|
-
Current: ${versionNotice.current} \u2192 Latest: ${versionNotice.latest}
|
|
9937
|
-
New version may include critical bug fixes, performance improvements, and new features.
|
|
10274
|
+
DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
|
|
9938
10275
|
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
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");
|
|
10832
|
+
return JSON.parse(content);
|
|
10833
|
+
} catch {
|
|
10834
|
+
return { hooks: {} };
|
|
10835
|
+
}
|
|
10836
|
+
}
|
|
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));
|
|
10842
|
+
}
|
|
10843
|
+
function filterWindsurfContextStreamHooks(hooks) {
|
|
10844
|
+
if (!hooks) return [];
|
|
10845
|
+
return hooks.filter((h) => !h.command?.includes("contextstream"));
|
|
10846
|
+
}
|
|
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
|
+
]
|
|
10877
|
+
}
|
|
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);
|
|
10898
|
+
const merged = mergeHooksIntoSettings(existing, hooksConfig);
|
|
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
|
+
};
|
|
10907
|
+
}
|
|
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
|
+
};
|
|
10939
|
+
}
|
|
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}`);
|
|
10950
|
+
}
|
|
10951
|
+
}
|
|
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
|
+
}
|
|
10967
|
+
}
|
|
10968
|
+
return results;
|
|
10969
|
+
}
|
|
10970
|
+
|
|
10971
|
+
// src/token-savings.ts
|
|
10972
|
+
var TOKEN_SAVINGS_FORMULA_VERSION = 1;
|
|
10973
|
+
var MAX_CHARS_PER_EVENT = 2e7;
|
|
10974
|
+
var BASE_OVERHEAD_CHARS = 500;
|
|
10975
|
+
var CANDIDATE_MULTIPLIERS = {
|
|
10976
|
+
// context_smart: Replaces reading multiple files to gather context
|
|
10977
|
+
context_smart: 5,
|
|
10978
|
+
ai_context_budget: 5,
|
|
10979
|
+
// search: Semantic search replaces iterative Glob/Grep/Read cycles
|
|
10980
|
+
search_semantic: 4.5,
|
|
10981
|
+
search_hybrid: 4,
|
|
10982
|
+
search_keyword: 2.5,
|
|
10983
|
+
search_pattern: 3,
|
|
10984
|
+
search_exhaustive: 3.5,
|
|
10985
|
+
search_refactor: 3,
|
|
10986
|
+
// session: Recall/search replaces reading through history
|
|
10987
|
+
session_recall: 5,
|
|
10988
|
+
session_smart_search: 4,
|
|
10989
|
+
session_user_context: 3,
|
|
10990
|
+
session_summary: 4,
|
|
10991
|
+
// graph: Would require extensive file traversal
|
|
10992
|
+
graph_dependencies: 8,
|
|
10993
|
+
graph_impact: 10,
|
|
10994
|
+
graph_call_path: 8,
|
|
10995
|
+
graph_related: 6,
|
|
10996
|
+
// memory: Context retrieval
|
|
10997
|
+
memory_search: 3.5,
|
|
10998
|
+
memory_decisions: 3,
|
|
10999
|
+
memory_timeline: 3,
|
|
11000
|
+
memory_summary: 4
|
|
11001
|
+
};
|
|
11002
|
+
function clampCharCount(value) {
|
|
11003
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
11004
|
+
return Math.min(MAX_CHARS_PER_EVENT, Math.floor(value));
|
|
11005
|
+
}
|
|
11006
|
+
function trackToolTokenSavings(client, tool, contextText, params, extraMetadata) {
|
|
11007
|
+
try {
|
|
11008
|
+
const contextChars = clampCharCount(contextText.length);
|
|
11009
|
+
const multiplier = CANDIDATE_MULTIPLIERS[tool] ?? 3;
|
|
11010
|
+
const baseOverhead = contextChars > 0 ? BASE_OVERHEAD_CHARS : 0;
|
|
11011
|
+
const estimatedCandidate = Math.round(contextChars * multiplier + baseOverhead);
|
|
11012
|
+
const candidateChars = Math.max(contextChars, clampCharCount(estimatedCandidate));
|
|
11013
|
+
client.trackTokenSavings({
|
|
11014
|
+
tool,
|
|
11015
|
+
workspace_id: params?.workspace_id,
|
|
11016
|
+
project_id: params?.project_id,
|
|
11017
|
+
candidate_chars: candidateChars,
|
|
11018
|
+
context_chars: contextChars,
|
|
11019
|
+
max_tokens: params?.max_tokens,
|
|
11020
|
+
metadata: {
|
|
11021
|
+
method: "multiplier_estimate",
|
|
11022
|
+
formula_version: TOKEN_SAVINGS_FORMULA_VERSION,
|
|
11023
|
+
source: "mcp-server",
|
|
11024
|
+
multiplier,
|
|
11025
|
+
base_overhead_chars: baseOverhead,
|
|
11026
|
+
...extraMetadata ?? {}
|
|
11027
|
+
}
|
|
11028
|
+
}).catch(() => {
|
|
11029
|
+
});
|
|
11030
|
+
} catch {
|
|
11031
|
+
}
|
|
11032
|
+
}
|
|
11033
|
+
|
|
11034
|
+
// src/tools.ts
|
|
11035
|
+
var LESSON_DEDUP_WINDOW_MS = 2 * 60 * 1e3;
|
|
11036
|
+
var recentLessonCaptures = /* @__PURE__ */ new Map();
|
|
11037
|
+
var SEARCH_RULES_REMINDER_ENABLED = process.env.CONTEXTSTREAM_SEARCH_REMINDER?.toLowerCase() !== "false";
|
|
11038
|
+
var SEARCH_RULES_REMINDER = `
|
|
11039
|
+
\u26A0\uFE0F [SEARCH RULES - READ EVERY TIME]
|
|
11040
|
+
BEFORE using Glob/Grep/Read/Search/Explore \u2192 call mcp__contextstream__search(mode="hybrid", query="...") FIRST
|
|
11041
|
+
BEFORE using EnterPlanMode/Task(Plan) \u2192 call mcp__contextstream__session(action="capture_plan", ...) instead
|
|
11042
|
+
Local tools ONLY if ContextStream returns 0 results after retry.
|
|
11043
|
+
`.trim();
|
|
11044
|
+
var LESSONS_REMINDER_PREFIX = `
|
|
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.
|
|
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
|
+
}
|
|
11114
|
+
function generateLessonsReminder(result) {
|
|
11115
|
+
const lessons = result.lessons;
|
|
11116
|
+
if (!lessons || lessons.length === 0) {
|
|
11117
|
+
return "";
|
|
11118
|
+
}
|
|
11119
|
+
const lessonLines = lessons.slice(0, 5).map((l, i) => {
|
|
11120
|
+
const severity = l.severity === "critical" ? "\u{1F6A8}" : l.severity === "high" ? "\u26A0\uFE0F" : "\u{1F4DD}";
|
|
11121
|
+
const title = l.title || "Untitled lesson";
|
|
11122
|
+
const prevention = l.prevention || l.trigger || "";
|
|
11123
|
+
return `${i + 1}. ${severity} ${title}${prevention ? `: ${prevention.slice(0, 100)}` : ""}`;
|
|
11124
|
+
});
|
|
11125
|
+
return `
|
|
11126
|
+
|
|
11127
|
+
${LESSONS_REMINDER_PREFIX}
|
|
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`;
|
|
11130
|
+
}
|
|
11131
|
+
function generateRulesUpdateWarning(rulesNotice) {
|
|
11132
|
+
if (!rulesNotice || rulesNotice.status !== "behind" && rulesNotice.status !== "missing") {
|
|
11133
|
+
return "";
|
|
11134
|
+
}
|
|
11135
|
+
const isMissing = rulesNotice.status === "missing";
|
|
11136
|
+
const current = rulesNotice.current ?? "none";
|
|
11137
|
+
const latest = rulesNotice.latest;
|
|
11138
|
+
return `
|
|
11139
|
+
\u{1F6A8} [RULES ${isMissing ? "MISSING" : "OUTDATED"} - ACTION REQUIRED]
|
|
11140
|
+
${isMissing ? "ContextStream rules are NOT installed." : `Rules version ${current} \u2192 ${latest} available.`}
|
|
11141
|
+
${isMissing ? "AI behavior may be suboptimal without proper rules." : "New rules include important improvements for better AI behavior."}
|
|
11142
|
+
|
|
11143
|
+
**UPDATE NOW:** Run \`mcp__contextstream__generate_rules(overwrite_existing=true)\`
|
|
11144
|
+
This is SAFE - only the ContextStream block is updated, your custom rules are preserved.
|
|
11145
|
+
`.trim();
|
|
11146
|
+
}
|
|
11147
|
+
function generateVersionUpdateWarning(versionNotice) {
|
|
11148
|
+
if (!versionNotice?.behind) {
|
|
11149
|
+
return "";
|
|
11150
|
+
}
|
|
11151
|
+
return `
|
|
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}
|
|
11155
|
+
|
|
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
|
|
11162
|
+
`.trim();
|
|
11163
|
+
}
|
|
11164
|
+
var DEFAULT_PARAM_DESCRIPTIONS = {
|
|
11165
|
+
api_key: "ContextStream API key.",
|
|
11166
|
+
apiKey: "ContextStream API key.",
|
|
11167
|
+
jwt: "ContextStream JWT for authentication.",
|
|
11168
|
+
workspace_id: "Workspace ID (UUID).",
|
|
11169
|
+
workspaceId: "Workspace ID (UUID).",
|
|
11170
|
+
project_id: "Project ID (UUID).",
|
|
11171
|
+
projectId: "Project ID (UUID).",
|
|
11172
|
+
node_id: "Node ID (UUID).",
|
|
9952
11173
|
event_id: "Event ID (UUID).",
|
|
9953
11174
|
reminder_id: "Reminder ID (UUID).",
|
|
9954
11175
|
folder_path: "Absolute path to the local folder.",
|
|
@@ -10618,13 +11839,17 @@ function resolveAuthOverride(extra) {
|
|
|
10618
11839
|
return { workspaceId, projectId };
|
|
10619
11840
|
}
|
|
10620
11841
|
var LIGHT_TOOLSET = /* @__PURE__ */ new Set([
|
|
10621
|
-
// Core session tools (
|
|
11842
|
+
// Core session tools (15)
|
|
10622
11843
|
"session_init",
|
|
10623
11844
|
"session_tools",
|
|
10624
11845
|
"context_smart",
|
|
10625
11846
|
"context_feedback",
|
|
10626
11847
|
"session_summary",
|
|
10627
11848
|
"session_capture",
|
|
11849
|
+
"session_capture_smart",
|
|
11850
|
+
// Pre-compaction state capture
|
|
11851
|
+
"session_restore_context",
|
|
11852
|
+
// Post-compaction context restore
|
|
10628
11853
|
"session_capture_lesson",
|
|
10629
11854
|
"session_get_lessons",
|
|
10630
11855
|
"session_recall",
|
|
@@ -10664,13 +11889,17 @@ var LIGHT_TOOLSET = /* @__PURE__ */ new Set([
|
|
|
10664
11889
|
"mcp_server_version"
|
|
10665
11890
|
]);
|
|
10666
11891
|
var STANDARD_TOOLSET = /* @__PURE__ */ new Set([
|
|
10667
|
-
// Core session tools (
|
|
11892
|
+
// Core session tools (16)
|
|
10668
11893
|
"session_init",
|
|
10669
11894
|
"session_tools",
|
|
10670
11895
|
"context_smart",
|
|
10671
11896
|
"context_feedback",
|
|
10672
11897
|
"session_summary",
|
|
10673
11898
|
"session_capture",
|
|
11899
|
+
"session_capture_smart",
|
|
11900
|
+
// Pre-compaction state capture
|
|
11901
|
+
"session_restore_context",
|
|
11902
|
+
// Post-compaction context restore
|
|
10674
11903
|
"session_capture_lesson",
|
|
10675
11904
|
"session_get_lessons",
|
|
10676
11905
|
"session_recall",
|
|
@@ -10790,6 +12019,7 @@ var ALL_INTEGRATION_TOOLS = /* @__PURE__ */ new Set([
|
|
|
10790
12019
|
...CROSS_INTEGRATION_TOOLS
|
|
10791
12020
|
]);
|
|
10792
12021
|
var AUTO_HIDE_INTEGRATIONS = process.env.CONTEXTSTREAM_AUTO_HIDE_INTEGRATIONS !== "false";
|
|
12022
|
+
var RESTORE_CONTEXT_DEFAULT = process.env.CONTEXTSTREAM_RESTORE_CONTEXT !== "false";
|
|
10793
12023
|
var TOKEN_SENSITIVE_CLIENTS = /* @__PURE__ */ new Set([
|
|
10794
12024
|
"claude",
|
|
10795
12025
|
"claude-code",
|
|
@@ -11121,6 +12351,7 @@ function parsePositiveInt(raw, fallback) {
|
|
|
11121
12351
|
}
|
|
11122
12352
|
var OUTPUT_FORMAT = process.env.CONTEXTSTREAM_OUTPUT_FORMAT || "compact";
|
|
11123
12353
|
var COMPACT_OUTPUT = OUTPUT_FORMAT === "compact";
|
|
12354
|
+
var SHOW_TIMING = process.env.CONTEXTSTREAM_SHOW_TIMING === "true" || process.env.CONTEXTSTREAM_SHOW_TIMING === "1";
|
|
11124
12355
|
var DEFAULT_SEARCH_LIMIT = parsePositiveInt(process.env.CONTEXTSTREAM_SEARCH_LIMIT, 3);
|
|
11125
12356
|
var DEFAULT_SEARCH_CONTENT_MAX_CHARS = parsePositiveInt(
|
|
11126
12357
|
process.env.CONTEXTSTREAM_SEARCH_MAX_CHARS,
|
|
@@ -11233,6 +12464,31 @@ function toStructured(data) {
|
|
|
11233
12464
|
}
|
|
11234
12465
|
return void 0;
|
|
11235
12466
|
}
|
|
12467
|
+
function formatTimingSummary(roundTripMs, resultCount) {
|
|
12468
|
+
if (!SHOW_TIMING) return "";
|
|
12469
|
+
const countStr = resultCount !== void 0 ? `${resultCount} results` : "done";
|
|
12470
|
+
return `\u2713 ${countStr} in ${roundTripMs}ms
|
|
12471
|
+
|
|
12472
|
+
`;
|
|
12473
|
+
}
|
|
12474
|
+
function getResultCount(data) {
|
|
12475
|
+
if (!data || typeof data !== "object") return void 0;
|
|
12476
|
+
const response = data;
|
|
12477
|
+
const dataObj = response.data;
|
|
12478
|
+
if (dataObj?.results && Array.isArray(dataObj.results)) {
|
|
12479
|
+
return dataObj.results.length;
|
|
12480
|
+
}
|
|
12481
|
+
if (typeof dataObj?.total === "number") {
|
|
12482
|
+
return dataObj.total;
|
|
12483
|
+
}
|
|
12484
|
+
if (typeof dataObj?.count === "number") {
|
|
12485
|
+
return dataObj.count;
|
|
12486
|
+
}
|
|
12487
|
+
if (dataObj?.paths && Array.isArray(dataObj.paths)) {
|
|
12488
|
+
return dataObj.paths.length;
|
|
12489
|
+
}
|
|
12490
|
+
return void 0;
|
|
12491
|
+
}
|
|
11236
12492
|
function readStatNumber(payload, key) {
|
|
11237
12493
|
if (!payload || typeof payload !== "object") return void 0;
|
|
11238
12494
|
const direct = payload[key];
|
|
@@ -13553,10 +14809,17 @@ This does semantic search on the first message. You only need context_smart on s
|
|
|
13553
14809
|
auto_index: external_exports.boolean().optional().describe("Automatically create and index project from IDE workspace (default: true)"),
|
|
13554
14810
|
allow_no_workspace: external_exports.boolean().optional().describe(
|
|
13555
14811
|
"If true, allow session_init to return connected even if no workspace is resolved (workspace-level tools may not work)."
|
|
14812
|
+
),
|
|
14813
|
+
skip_project_creation: external_exports.boolean().optional().describe(
|
|
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."
|
|
14815
|
+
),
|
|
14816
|
+
is_post_compact: external_exports.boolean().optional().describe(
|
|
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."
|
|
13556
14818
|
)
|
|
13557
14819
|
})
|
|
13558
14820
|
},
|
|
13559
14821
|
async (input) => {
|
|
14822
|
+
const startTime = Date.now();
|
|
13560
14823
|
let ideRoots = [];
|
|
13561
14824
|
try {
|
|
13562
14825
|
const rootsResponse = await server.server.listRoots();
|
|
@@ -13572,8 +14835,90 @@ This does semantic search on the first message. You only need context_smart on s
|
|
|
13572
14835
|
}
|
|
13573
14836
|
const result = await client.initSession(input, ideRoots);
|
|
13574
14837
|
result.tools_hint = getCoreToolsHint();
|
|
14838
|
+
const shouldRestoreContext = input.is_post_compact ?? RESTORE_CONTEXT_DEFAULT;
|
|
14839
|
+
if (shouldRestoreContext) {
|
|
14840
|
+
result.is_post_compact = true;
|
|
14841
|
+
const workspaceIdForRestore = typeof result.workspace_id === "string" ? result.workspace_id : void 0;
|
|
14842
|
+
const projectIdForRestore = typeof result.project_id === "string" ? result.project_id : void 0;
|
|
14843
|
+
if (workspaceIdForRestore) {
|
|
14844
|
+
try {
|
|
14845
|
+
const listResult = await client.listMemoryEvents({
|
|
14846
|
+
workspace_id: workspaceIdForRestore,
|
|
14847
|
+
project_id: projectIdForRestore,
|
|
14848
|
+
limit: 50
|
|
14849
|
+
});
|
|
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
|
+
);
|
|
14854
|
+
if (snapshots && snapshots.length > 0) {
|
|
14855
|
+
const latestSnapshot = snapshots[0];
|
|
14856
|
+
let snapshotData;
|
|
14857
|
+
try {
|
|
14858
|
+
snapshotData = JSON.parse(latestSnapshot.content);
|
|
14859
|
+
} catch {
|
|
14860
|
+
snapshotData = { conversation_summary: latestSnapshot.content };
|
|
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
|
+
}
|
|
14900
|
+
result.restored_context = {
|
|
14901
|
+
snapshot_id: latestSnapshot.id,
|
|
14902
|
+
captured_at: snapshotData.captured_at || latestSnapshot.created_at,
|
|
14903
|
+
session_linking: Object.keys(sessionLinking).length > 0 ? sessionLinking : void 0,
|
|
14904
|
+
...snapshotData
|
|
14905
|
+
};
|
|
14906
|
+
result.is_post_compact = true;
|
|
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.";
|
|
14908
|
+
} else {
|
|
14909
|
+
result.is_post_compact = true;
|
|
14910
|
+
result.post_compact_hint = "Post-compaction session started, but no snapshots found. Use context_smart to retrieve relevant context.";
|
|
14911
|
+
}
|
|
14912
|
+
} catch (err) {
|
|
14913
|
+
console.error("[ContextStream] Failed to restore post-compact context:", err);
|
|
14914
|
+
result.is_post_compact = true;
|
|
14915
|
+
result.post_compact_hint = "Post-compaction session started. Snapshot restoration failed, use context_smart for context.";
|
|
14916
|
+
}
|
|
14917
|
+
}
|
|
14918
|
+
}
|
|
13575
14919
|
if (sessionManager) {
|
|
13576
14920
|
sessionManager.markInitialized(result);
|
|
14921
|
+
sessionManager.resetTokenCount();
|
|
13577
14922
|
}
|
|
13578
14923
|
const folderPathForRules = input.folder_path || ideRoots[0] || resolveFolderPath(void 0, sessionManager);
|
|
13579
14924
|
if (sessionManager && folderPathForRules) {
|
|
@@ -13720,6 +15065,12 @@ ${noticeLines.filter(Boolean).join("\n")}`;
|
|
|
13720
15065
|
text = `${text}
|
|
13721
15066
|
|
|
13722
15067
|
${SEARCH_RULES_REMINDER}`;
|
|
15068
|
+
}
|
|
15069
|
+
const roundTripMs = Date.now() - startTime;
|
|
15070
|
+
if (SHOW_TIMING) {
|
|
15071
|
+
text = `\u2713 session initialized in ${roundTripMs}ms
|
|
15072
|
+
|
|
15073
|
+
${text}`;
|
|
13723
15074
|
}
|
|
13724
15075
|
return {
|
|
13725
15076
|
content: [{ type: "text", text }],
|
|
@@ -13997,8 +15348,11 @@ Use this to persist decisions, insights, preferences, or important information.`
|
|
|
13997
15348
|
// Extracted lesson from correction
|
|
13998
15349
|
"warning",
|
|
13999
15350
|
// Proactive reminder
|
|
14000
|
-
"frustration"
|
|
15351
|
+
"frustration",
|
|
14001
15352
|
// User expressed frustration
|
|
15353
|
+
// Compaction awareness
|
|
15354
|
+
"session_snapshot"
|
|
15355
|
+
// Pre-compaction state capture
|
|
14002
15356
|
]).describe("Type of context being captured"),
|
|
14003
15357
|
title: external_exports.string().describe("Brief title for the captured context"),
|
|
14004
15358
|
content: external_exports.string().describe("Full content/details to capture"),
|
|
@@ -14030,27 +15384,245 @@ Use this to persist decisions, insights, preferences, or important information.`
|
|
|
14030
15384
|
workspaceId = ctx.workspace_id;
|
|
14031
15385
|
projectId = projectId || ctx.project_id;
|
|
14032
15386
|
}
|
|
14033
|
-
}
|
|
14034
|
-
if (!workspaceId) {
|
|
15387
|
+
}
|
|
15388
|
+
if (!workspaceId) {
|
|
15389
|
+
return {
|
|
15390
|
+
content: [
|
|
15391
|
+
{
|
|
15392
|
+
type: "text",
|
|
15393
|
+
text: "Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly."
|
|
15394
|
+
}
|
|
15395
|
+
],
|
|
15396
|
+
isError: true
|
|
15397
|
+
};
|
|
15398
|
+
}
|
|
15399
|
+
const result = await client.captureContext({
|
|
15400
|
+
...input,
|
|
15401
|
+
workspace_id: workspaceId,
|
|
15402
|
+
project_id: projectId
|
|
15403
|
+
});
|
|
15404
|
+
return {
|
|
15405
|
+
content: [{ type: "text", text: formatContent(result) }],
|
|
15406
|
+
structuredContent: toStructured(result)
|
|
15407
|
+
};
|
|
15408
|
+
}
|
|
15409
|
+
);
|
|
15410
|
+
registerTool(
|
|
15411
|
+
"session_capture_smart",
|
|
15412
|
+
{
|
|
15413
|
+
title: "Smart capture for conversation compaction",
|
|
15414
|
+
description: `Intelligently capture conversation state before compaction or context loss.
|
|
15415
|
+
This creates a session_snapshot that can be restored after compaction.
|
|
15416
|
+
|
|
15417
|
+
Use when:
|
|
15418
|
+
- Context pressure is high/critical (context_smart returns threshold_warning)
|
|
15419
|
+
- Before manual /compact commands
|
|
15420
|
+
- When significant work progress needs preservation
|
|
15421
|
+
|
|
15422
|
+
Captures:
|
|
15423
|
+
- Conversation summary and current goals
|
|
15424
|
+
- Active files being worked on
|
|
15425
|
+
- Recent decisions with rationale
|
|
15426
|
+
- Unfinished work items
|
|
15427
|
+
- User preferences expressed in session
|
|
15428
|
+
|
|
15429
|
+
The snapshot is automatically prioritized during post-compaction session_init.`,
|
|
15430
|
+
inputSchema: external_exports.object({
|
|
15431
|
+
workspace_id: external_exports.string().uuid().optional(),
|
|
15432
|
+
project_id: external_exports.string().uuid().optional(),
|
|
15433
|
+
conversation_summary: external_exports.string().describe("AI's summary of the conversation so far - what was discussed and accomplished"),
|
|
15434
|
+
current_goal: external_exports.string().optional().describe("The primary goal or task being worked on"),
|
|
15435
|
+
active_files: external_exports.array(external_exports.string()).optional().describe("List of files currently being worked on"),
|
|
15436
|
+
recent_decisions: external_exports.array(
|
|
15437
|
+
external_exports.object({
|
|
15438
|
+
title: external_exports.string(),
|
|
15439
|
+
rationale: external_exports.string().optional()
|
|
15440
|
+
})
|
|
15441
|
+
).optional().describe("Key decisions made in this session with their rationale"),
|
|
15442
|
+
unfinished_work: external_exports.array(
|
|
15443
|
+
external_exports.object({
|
|
15444
|
+
task: external_exports.string(),
|
|
15445
|
+
status: external_exports.string().optional(),
|
|
15446
|
+
next_steps: external_exports.string().optional()
|
|
15447
|
+
})
|
|
15448
|
+
).optional().describe("Work items that are in progress or pending"),
|
|
15449
|
+
user_preferences: external_exports.array(external_exports.string()).optional().describe("Preferences expressed by user during this session"),
|
|
15450
|
+
priority_items: external_exports.array(external_exports.string()).optional().describe("User-flagged important items to remember"),
|
|
15451
|
+
metadata: external_exports.record(external_exports.unknown()).optional().describe("Additional context to preserve")
|
|
15452
|
+
})
|
|
15453
|
+
},
|
|
15454
|
+
async (input) => {
|
|
15455
|
+
let workspaceId = input.workspace_id;
|
|
15456
|
+
let projectId = input.project_id;
|
|
15457
|
+
if (!workspaceId && sessionManager) {
|
|
15458
|
+
const ctx = sessionManager.getContext();
|
|
15459
|
+
if (ctx) {
|
|
15460
|
+
workspaceId = ctx.workspace_id;
|
|
15461
|
+
projectId = projectId || ctx.project_id;
|
|
15462
|
+
}
|
|
15463
|
+
}
|
|
15464
|
+
if (!workspaceId) {
|
|
15465
|
+
return errorResult(
|
|
15466
|
+
"Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly."
|
|
15467
|
+
);
|
|
15468
|
+
}
|
|
15469
|
+
const snapshotContent = {
|
|
15470
|
+
conversation_summary: input.conversation_summary,
|
|
15471
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
15472
|
+
};
|
|
15473
|
+
if (input.current_goal) {
|
|
15474
|
+
snapshotContent.current_goal = input.current_goal;
|
|
15475
|
+
}
|
|
15476
|
+
if (input.active_files?.length) {
|
|
15477
|
+
snapshotContent.active_files = input.active_files;
|
|
15478
|
+
}
|
|
15479
|
+
if (input.recent_decisions?.length) {
|
|
15480
|
+
snapshotContent.recent_decisions = input.recent_decisions;
|
|
15481
|
+
}
|
|
15482
|
+
if (input.unfinished_work?.length) {
|
|
15483
|
+
snapshotContent.unfinished_work = input.unfinished_work;
|
|
15484
|
+
}
|
|
15485
|
+
if (input.user_preferences?.length) {
|
|
15486
|
+
snapshotContent.user_preferences = input.user_preferences;
|
|
15487
|
+
}
|
|
15488
|
+
if (input.priority_items?.length) {
|
|
15489
|
+
snapshotContent.priority_items = input.priority_items;
|
|
15490
|
+
}
|
|
15491
|
+
if (input.metadata) {
|
|
15492
|
+
snapshotContent.metadata = input.metadata;
|
|
15493
|
+
}
|
|
15494
|
+
const result = await client.captureContext({
|
|
15495
|
+
workspace_id: workspaceId,
|
|
15496
|
+
project_id: projectId,
|
|
15497
|
+
event_type: "session_snapshot",
|
|
15498
|
+
title: `Session Snapshot: ${input.current_goal || "Conversation State"}`,
|
|
15499
|
+
content: JSON.stringify(snapshotContent, null, 2),
|
|
15500
|
+
importance: "high",
|
|
15501
|
+
tags: ["session_snapshot", "pre_compaction"]
|
|
15502
|
+
});
|
|
15503
|
+
const response = {
|
|
15504
|
+
...result,
|
|
15505
|
+
snapshot_id: result?.data?.id || result?.id,
|
|
15506
|
+
message: "Session state captured successfully. This snapshot will be prioritized after compaction.",
|
|
15507
|
+
hint: "After compaction, call session_init with is_post_compact=true to restore this context."
|
|
15508
|
+
};
|
|
15509
|
+
return {
|
|
15510
|
+
content: [{ type: "text", text: formatContent(response) }],
|
|
15511
|
+
structuredContent: toStructured(response)
|
|
15512
|
+
};
|
|
15513
|
+
}
|
|
15514
|
+
);
|
|
15515
|
+
registerTool(
|
|
15516
|
+
"session_restore_context",
|
|
15517
|
+
{
|
|
15518
|
+
title: "Restore context after compaction",
|
|
15519
|
+
description: `Restore conversation context after compaction or context loss.
|
|
15520
|
+
Call this after conversation compaction to retrieve saved session state.
|
|
15521
|
+
|
|
15522
|
+
Returns structured context including:
|
|
15523
|
+
- conversation_summary: What was being discussed
|
|
15524
|
+
- current_goal: The primary task being worked on
|
|
15525
|
+
- active_files: Files that were being modified
|
|
15526
|
+
- recent_decisions: Key decisions made in the session
|
|
15527
|
+
- unfinished_work: Tasks that are still in progress
|
|
15528
|
+
- user_preferences: Preferences expressed during the session
|
|
15529
|
+
|
|
15530
|
+
Use this in combination with session_init(is_post_compact=true) for seamless continuation.`,
|
|
15531
|
+
inputSchema: external_exports.object({
|
|
15532
|
+
workspace_id: external_exports.string().uuid().optional(),
|
|
15533
|
+
project_id: external_exports.string().uuid().optional(),
|
|
15534
|
+
snapshot_id: external_exports.string().uuid().optional().describe("Specific snapshot ID to restore (defaults to most recent)"),
|
|
15535
|
+
max_snapshots: external_exports.number().optional().default(1).describe("Number of recent snapshots to consider (default: 1)")
|
|
15536
|
+
})
|
|
15537
|
+
},
|
|
15538
|
+
async (input) => {
|
|
15539
|
+
let workspaceId = input.workspace_id;
|
|
15540
|
+
let projectId = input.project_id;
|
|
15541
|
+
if (!workspaceId && sessionManager) {
|
|
15542
|
+
const ctx = sessionManager.getContext();
|
|
15543
|
+
if (ctx) {
|
|
15544
|
+
workspaceId = ctx.workspace_id;
|
|
15545
|
+
projectId = projectId || ctx.project_id;
|
|
15546
|
+
}
|
|
15547
|
+
}
|
|
15548
|
+
if (!workspaceId) {
|
|
15549
|
+
return errorResult(
|
|
15550
|
+
"Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly."
|
|
15551
|
+
);
|
|
15552
|
+
}
|
|
15553
|
+
try {
|
|
15554
|
+
if (input.snapshot_id) {
|
|
15555
|
+
const eventResult = await client.getEvent(input.snapshot_id);
|
|
15556
|
+
const event = eventResult?.data || eventResult;
|
|
15557
|
+
if (!event || !event.content) {
|
|
15558
|
+
return errorResult(
|
|
15559
|
+
`Snapshot not found: ${input.snapshot_id}. The snapshot may have been deleted or does not exist.`
|
|
15560
|
+
);
|
|
15561
|
+
}
|
|
15562
|
+
let snapshotData2;
|
|
15563
|
+
try {
|
|
15564
|
+
snapshotData2 = JSON.parse(event.content);
|
|
15565
|
+
} catch {
|
|
15566
|
+
snapshotData2 = { conversation_summary: event.content };
|
|
15567
|
+
}
|
|
15568
|
+
const response2 = {
|
|
15569
|
+
restored: true,
|
|
15570
|
+
snapshot_id: event.id,
|
|
15571
|
+
captured_at: snapshotData2.captured_at || event.created_at,
|
|
15572
|
+
...snapshotData2,
|
|
15573
|
+
hint: "Context restored. Continue the conversation with awareness of the above state."
|
|
15574
|
+
};
|
|
15575
|
+
return {
|
|
15576
|
+
content: [{ type: "text", text: formatContent(response2) }],
|
|
15577
|
+
structuredContent: toStructured(response2)
|
|
15578
|
+
};
|
|
15579
|
+
}
|
|
15580
|
+
const listResult = await client.listMemoryEvents({
|
|
15581
|
+
workspace_id: workspaceId,
|
|
15582
|
+
project_id: projectId,
|
|
15583
|
+
limit: 50
|
|
15584
|
+
// Fetch more to filter
|
|
15585
|
+
});
|
|
15586
|
+
const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
|
|
15587
|
+
const events = allEvents.filter(
|
|
15588
|
+
(e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
|
|
15589
|
+
).slice(0, input.max_snapshots || 1);
|
|
15590
|
+
if (!events || events.length === 0) {
|
|
15591
|
+
return {
|
|
15592
|
+
content: [
|
|
15593
|
+
{
|
|
15594
|
+
type: "text",
|
|
15595
|
+
text: formatContent({
|
|
15596
|
+
restored: false,
|
|
15597
|
+
message: "No session snapshots found. This may be a new session or snapshots have not been captured.",
|
|
15598
|
+
hint: "Use session_capture_smart to save session state before compaction."
|
|
15599
|
+
})
|
|
15600
|
+
}
|
|
15601
|
+
]
|
|
15602
|
+
};
|
|
15603
|
+
}
|
|
15604
|
+
const latestEvent = events[0];
|
|
15605
|
+
let snapshotData;
|
|
15606
|
+
try {
|
|
15607
|
+
snapshotData = JSON.parse(latestEvent.content);
|
|
15608
|
+
} catch {
|
|
15609
|
+
snapshotData = { conversation_summary: latestEvent.content };
|
|
15610
|
+
}
|
|
15611
|
+
const response = {
|
|
15612
|
+
restored: true,
|
|
15613
|
+
snapshot_id: latestEvent.id,
|
|
15614
|
+
captured_at: snapshotData.captured_at || latestEvent.created_at,
|
|
15615
|
+
...snapshotData,
|
|
15616
|
+
hint: "Context restored. Continue the conversation with awareness of the above state."
|
|
15617
|
+
};
|
|
14035
15618
|
return {
|
|
14036
|
-
content: [
|
|
14037
|
-
|
|
14038
|
-
type: "text",
|
|
14039
|
-
text: "Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly."
|
|
14040
|
-
}
|
|
14041
|
-
],
|
|
14042
|
-
isError: true
|
|
15619
|
+
content: [{ type: "text", text: formatContent(response) }],
|
|
15620
|
+
structuredContent: toStructured(response)
|
|
14043
15621
|
};
|
|
15622
|
+
} catch (error) {
|
|
15623
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15624
|
+
return errorResult(`Failed to restore context: ${message}`);
|
|
14044
15625
|
}
|
|
14045
|
-
const result = await client.captureContext({
|
|
14046
|
-
...input,
|
|
14047
|
-
workspace_id: workspaceId,
|
|
14048
|
-
project_id: projectId
|
|
14049
|
-
});
|
|
14050
|
-
return {
|
|
14051
|
-
content: [{ type: "text", text: formatContent(result) }],
|
|
14052
|
-
structuredContent: toStructured(result)
|
|
14053
|
-
};
|
|
14054
15626
|
}
|
|
14055
15627
|
);
|
|
14056
15628
|
registerTool(
|
|
@@ -14411,6 +15983,7 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
|
|
|
14411
15983
|
overwrite_existing: external_exports.boolean().optional().describe("Allow overwriting existing rule files (ContextStream block only)"),
|
|
14412
15984
|
apply_global: external_exports.boolean().optional().describe("Also write global rule files for supported editors"),
|
|
14413
15985
|
install_hooks: external_exports.boolean().optional().describe("Install Claude Code hooks to enforce ContextStream-first search. Defaults to true for Claude users. Set to false to skip."),
|
|
15986
|
+
include_pre_compact: external_exports.boolean().optional().describe("Include PreCompact hook for automatic state saving before context compaction. Defaults to false."),
|
|
14414
15987
|
dry_run: external_exports.boolean().optional().describe("If true, return content without writing files")
|
|
14415
15988
|
})
|
|
14416
15989
|
},
|
|
@@ -14481,28 +16054,77 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
|
|
|
14481
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.";
|
|
14482
16055
|
let hooksResults;
|
|
14483
16056
|
let hooksPrompt;
|
|
14484
|
-
const
|
|
14485
|
-
|
|
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;
|
|
14486
16067
|
if (shouldInstallHooks) {
|
|
14487
16068
|
try {
|
|
14488
16069
|
if (input.dry_run) {
|
|
14489
|
-
hooksResults = [
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
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
|
+
}
|
|
16109
|
+
}
|
|
14494
16110
|
} else {
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
16111
|
+
hooksResults = [];
|
|
16112
|
+
const allHookResults = await installAllEditorHooks({
|
|
16113
|
+
scope: "global",
|
|
16114
|
+
editors: hookSupportedEditors,
|
|
16115
|
+
includePreCompact: input.include_pre_compact
|
|
16116
|
+
});
|
|
16117
|
+
for (const result of allHookResults) {
|
|
16118
|
+
for (const file of result.installed) {
|
|
16119
|
+
hooksResults.push({ editor: result.editor, file, status: "created" });
|
|
16120
|
+
}
|
|
16121
|
+
}
|
|
14500
16122
|
}
|
|
14501
16123
|
} catch (err) {
|
|
14502
16124
|
hooksResults = [{ file: "hooks", status: `error: ${err.message}` }];
|
|
14503
16125
|
}
|
|
14504
|
-
} else if (
|
|
14505
|
-
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.";
|
|
14506
16128
|
}
|
|
14507
16129
|
const summary = {
|
|
14508
16130
|
folder: folderPath,
|
|
@@ -14851,10 +16473,13 @@ This saves ~80% tokens compared to including full chat history.`,
|
|
|
14851
16473
|
max_tokens: external_exports.number().optional().describe("Maximum tokens for context (default: 800)"),
|
|
14852
16474
|
format: external_exports.enum(["minified", "readable", "structured"]).optional().describe("Context format (default: minified)"),
|
|
14853
16475
|
mode: external_exports.enum(["standard", "pack"]).optional().describe("Context pack mode (default: pack when enabled)"),
|
|
14854
|
-
distill: external_exports.boolean().optional().describe("Use distillation for context pack (default: true)")
|
|
16476
|
+
distill: external_exports.boolean().optional().describe("Use distillation for context pack (default: true)"),
|
|
16477
|
+
session_tokens: external_exports.number().optional().describe("Cumulative session token count for context pressure calculation"),
|
|
16478
|
+
context_threshold: external_exports.number().optional().describe("Custom context window threshold (defaults to 70k)")
|
|
14855
16479
|
})
|
|
14856
16480
|
},
|
|
14857
16481
|
async (input) => {
|
|
16482
|
+
const startTime = Date.now();
|
|
14858
16483
|
if (sessionManager) {
|
|
14859
16484
|
sessionManager.markContextSmartCalled();
|
|
14860
16485
|
}
|
|
@@ -14867,6 +16492,63 @@ This saves ~80% tokens compared to including full chat history.`,
|
|
|
14867
16492
|
projectId = projectId || ctx.project_id;
|
|
14868
16493
|
}
|
|
14869
16494
|
}
|
|
16495
|
+
let sessionTokens = input.session_tokens;
|
|
16496
|
+
let contextThreshold = input.context_threshold;
|
|
16497
|
+
if (sessionManager) {
|
|
16498
|
+
if (sessionTokens === void 0) {
|
|
16499
|
+
sessionTokens = sessionManager.getSessionTokens();
|
|
16500
|
+
}
|
|
16501
|
+
if (contextThreshold === void 0) {
|
|
16502
|
+
contextThreshold = sessionManager.getContextThreshold();
|
|
16503
|
+
}
|
|
16504
|
+
sessionManager.addTokens(input.user_message);
|
|
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
|
+
}
|
|
14870
16552
|
const result = await client.getSmartContext({
|
|
14871
16553
|
user_message: input.user_message,
|
|
14872
16554
|
workspace_id: workspaceId,
|
|
@@ -14874,11 +16556,18 @@ This saves ~80% tokens compared to including full chat history.`,
|
|
|
14874
16556
|
max_tokens: input.max_tokens,
|
|
14875
16557
|
format: input.format,
|
|
14876
16558
|
mode: input.mode,
|
|
14877
|
-
distill: input.distill
|
|
16559
|
+
distill: input.distill,
|
|
16560
|
+
session_tokens: sessionTokens,
|
|
16561
|
+
context_threshold: contextThreshold
|
|
14878
16562
|
});
|
|
16563
|
+
if (sessionManager && result.token_estimate) {
|
|
16564
|
+
sessionManager.addTokens(result.token_estimate);
|
|
16565
|
+
}
|
|
16566
|
+
const roundTripMs = Date.now() - startTime;
|
|
16567
|
+
const timingStr = SHOW_TIMING ? ` | ${roundTripMs}ms` : "";
|
|
14879
16568
|
const footer = `
|
|
14880
16569
|
---
|
|
14881
|
-
\u{1F3AF} ${result.sources_used} sources | ~${result.token_estimate} tokens | format: ${result.format}`;
|
|
16570
|
+
\u{1F3AF} ${result.sources_used} sources | ~${result.token_estimate} tokens | format: ${result.format}${timingStr}`;
|
|
14882
16571
|
const folderPathForRules = resolveFolderPath(void 0, sessionManager);
|
|
14883
16572
|
const rulesNotice = getRulesNotice(folderPathForRules, detectedClientInfo?.name);
|
|
14884
16573
|
let versionNotice = result.version_notice;
|
|
@@ -14900,11 +16589,69 @@ This saves ~80% tokens compared to including full chat history.`,
|
|
|
14900
16589
|
project_id: projectId,
|
|
14901
16590
|
max_tokens: input.max_tokens
|
|
14902
16591
|
});
|
|
14903
|
-
|
|
14904
|
-
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
|
+
}
|
|
14905
16630
|
const searchRulesLine = SEARCH_RULES_REMINDER_ENABLED ? `
|
|
14906
16631
|
|
|
14907
16632
|
${SEARCH_RULES_REMINDER}` : "";
|
|
16633
|
+
let contextPressureWarning = "";
|
|
16634
|
+
if (result.context_pressure) {
|
|
16635
|
+
const cp = result.context_pressure;
|
|
16636
|
+
if (cp.level === "critical") {
|
|
16637
|
+
if (sessionManager) {
|
|
16638
|
+
sessionManager.markHighContextPressure();
|
|
16639
|
+
}
|
|
16640
|
+
contextPressureWarning = `
|
|
16641
|
+
|
|
16642
|
+
\u{1F6A8} [CONTEXT PRESSURE: CRITICAL] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
|
|
16643
|
+
Action: ${cp.suggested_action === "save_now" ? 'SAVE STATE NOW - Call session(action="capture") to preserve conversation state before compaction.' : cp.suggested_action}
|
|
16644
|
+
The conversation may compact soon. Save important decisions, insights, and progress immediately.`;
|
|
16645
|
+
} else if (cp.level === "high") {
|
|
16646
|
+
if (sessionManager) {
|
|
16647
|
+
sessionManager.markHighContextPressure();
|
|
16648
|
+
}
|
|
16649
|
+
contextPressureWarning = `
|
|
16650
|
+
|
|
16651
|
+
\u26A0\uFE0F [CONTEXT PRESSURE: HIGH] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
|
|
16652
|
+
Action: ${cp.suggested_action === "prepare_save" ? "Consider saving important decisions and conversation state soon." : cp.suggested_action}`;
|
|
16653
|
+
}
|
|
16654
|
+
}
|
|
14908
16655
|
const allWarnings = [
|
|
14909
16656
|
lessonsWarningLine,
|
|
14910
16657
|
rulesWarningLine ? `
|
|
@@ -14913,16 +16660,19 @@ ${rulesWarningLine}` : "",
|
|
|
14913
16660
|
versionWarningLine ? `
|
|
14914
16661
|
|
|
14915
16662
|
${versionWarningLine}` : "",
|
|
16663
|
+
contextPressureWarning,
|
|
14916
16664
|
searchRulesLine
|
|
14917
16665
|
].filter(Boolean).join("");
|
|
16666
|
+
const finalContext = postCompactContext + result.context;
|
|
16667
|
+
const enrichedResultWithRestore = postCompactRestored ? { ...enrichedResult, post_compact_restored: true } : enrichedResult;
|
|
14918
16668
|
return {
|
|
14919
16669
|
content: [
|
|
14920
16670
|
{
|
|
14921
16671
|
type: "text",
|
|
14922
|
-
text:
|
|
16672
|
+
text: finalContext + footer + allWarnings
|
|
14923
16673
|
}
|
|
14924
16674
|
],
|
|
14925
|
-
structuredContent: toStructured(
|
|
16675
|
+
structuredContent: toStructured(enrichedResultWithRestore)
|
|
14926
16676
|
};
|
|
14927
16677
|
}
|
|
14928
16678
|
);
|
|
@@ -15943,6 +17693,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
|
|
|
15943
17693
|
},
|
|
15944
17694
|
async (input) => {
|
|
15945
17695
|
const params = normalizeSearchParams(input);
|
|
17696
|
+
const startTime = Date.now();
|
|
15946
17697
|
let result;
|
|
15947
17698
|
let toolType;
|
|
15948
17699
|
switch (input.mode) {
|
|
@@ -15973,7 +17724,9 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
|
|
|
15973
17724
|
default:
|
|
15974
17725
|
toolType = "search_hybrid";
|
|
15975
17726
|
}
|
|
15976
|
-
const
|
|
17727
|
+
const roundTripMs = Date.now() - startTime;
|
|
17728
|
+
const timingSummary = formatTimingSummary(roundTripMs, getResultCount(result));
|
|
17729
|
+
const outputText = timingSummary + formatContent(result);
|
|
15977
17730
|
trackToolTokenSavings(client, toolType, outputText, {
|
|
15978
17731
|
workspace_id: params.workspace_id,
|
|
15979
17732
|
project_id: params.project_id
|
|
@@ -15988,7 +17741,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
|
|
|
15988
17741
|
"session",
|
|
15989
17742
|
{
|
|
15990
17743
|
title: "Session",
|
|
15991
|
-
description: `Session management operations. Actions: capture (save decision/insight), capture_lesson (save lesson from mistake), get_lessons (retrieve lessons), recall (natural language recall), remember (quick save), user_context (get preferences), summary (workspace summary), compress (compress chat), delta (changes since timestamp), smart_search (context-enriched search), decision_trace (trace decision provenance). Plan actions: capture_plan (save implementation plan), get_plan (retrieve plan with tasks), update_plan (modify plan), list_plans (list all plans).`,
|
|
17744
|
+
description: `Session management operations. Actions: capture (save decision/insight), capture_lesson (save lesson from mistake), get_lessons (retrieve lessons), recall (natural language recall), remember (quick save), user_context (get preferences), summary (workspace summary), compress (compress chat), delta (changes since timestamp), smart_search (context-enriched search), decision_trace (trace decision provenance), restore_context (restore state after compaction). Plan actions: capture_plan (save implementation plan), get_plan (retrieve plan with tasks), update_plan (modify plan), list_plans (list all plans).`,
|
|
15992
17745
|
inputSchema: external_exports.object({
|
|
15993
17746
|
action: external_exports.enum([
|
|
15994
17747
|
"capture",
|
|
@@ -16006,7 +17759,9 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
|
|
|
16006
17759
|
"capture_plan",
|
|
16007
17760
|
"get_plan",
|
|
16008
17761
|
"update_plan",
|
|
16009
|
-
"list_plans"
|
|
17762
|
+
"list_plans",
|
|
17763
|
+
// Context restore
|
|
17764
|
+
"restore_context"
|
|
16010
17765
|
]).describe("Action to perform"),
|
|
16011
17766
|
workspace_id: external_exports.string().uuid().optional(),
|
|
16012
17767
|
project_id: external_exports.string().uuid().optional(),
|
|
@@ -16028,7 +17783,8 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
|
|
|
16028
17783
|
"lesson",
|
|
16029
17784
|
"warning",
|
|
16030
17785
|
"frustration",
|
|
16031
|
-
"conversation"
|
|
17786
|
+
"conversation",
|
|
17787
|
+
"session_snapshot"
|
|
16032
17788
|
]).optional().describe("Event type for capture"),
|
|
16033
17789
|
importance: external_exports.enum(["low", "medium", "high", "critical"]).optional(),
|
|
16034
17790
|
tags: external_exports.array(external_exports.string()).optional(),
|
|
@@ -16078,7 +17834,10 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
|
|
|
16078
17834
|
status: external_exports.enum(["draft", "active", "completed", "archived", "abandoned"]).optional().describe("Plan status"),
|
|
16079
17835
|
due_at: external_exports.string().optional().describe("Due date for plan (ISO timestamp)"),
|
|
16080
17836
|
source_tool: external_exports.string().optional().describe("Tool that generated this plan"),
|
|
16081
|
-
include_tasks: external_exports.boolean().optional().describe("Include tasks when getting plan")
|
|
17837
|
+
include_tasks: external_exports.boolean().optional().describe("Include tasks when getting plan"),
|
|
17838
|
+
// Restore context params
|
|
17839
|
+
snapshot_id: external_exports.string().uuid().optional().describe("Specific snapshot ID to restore (defaults to most recent)"),
|
|
17840
|
+
max_snapshots: external_exports.number().optional().default(1).describe("Number of recent snapshots to consider (default: 1)")
|
|
16082
17841
|
})
|
|
16083
17842
|
},
|
|
16084
17843
|
async (input) => {
|
|
@@ -16384,6 +18143,143 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
|
|
|
16384
18143
|
structuredContent: toStructured(result)
|
|
16385
18144
|
};
|
|
16386
18145
|
}
|
|
18146
|
+
case "restore_context": {
|
|
18147
|
+
if (!workspaceId) {
|
|
18148
|
+
return errorResult(
|
|
18149
|
+
"restore_context requires workspace_id. Call session_init first."
|
|
18150
|
+
);
|
|
18151
|
+
}
|
|
18152
|
+
if (input.snapshot_id) {
|
|
18153
|
+
const eventResult = await client.getEvent(input.snapshot_id);
|
|
18154
|
+
const event = eventResult?.data || eventResult;
|
|
18155
|
+
if (!event || !event.content) {
|
|
18156
|
+
return errorResult(
|
|
18157
|
+
`Snapshot not found: ${input.snapshot_id}. The snapshot may have been deleted or does not exist.`
|
|
18158
|
+
);
|
|
18159
|
+
}
|
|
18160
|
+
let snapshotData;
|
|
18161
|
+
try {
|
|
18162
|
+
snapshotData = JSON.parse(event.content);
|
|
18163
|
+
} catch {
|
|
18164
|
+
snapshotData = { conversation_summary: event.content };
|
|
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
|
+
}
|
|
18188
|
+
const response2 = {
|
|
18189
|
+
restored: true,
|
|
18190
|
+
snapshot_id: event.id,
|
|
18191
|
+
captured_at: snapshotData.captured_at || event.created_at,
|
|
18192
|
+
session_linking: Object.keys(sessionLinking2).length > 0 ? sessionLinking2 : void 0,
|
|
18193
|
+
...snapshotData,
|
|
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."
|
|
18195
|
+
};
|
|
18196
|
+
return {
|
|
18197
|
+
content: [{ type: "text", text: formatContent(response2) }],
|
|
18198
|
+
structuredContent: toStructured(response2)
|
|
18199
|
+
};
|
|
18200
|
+
}
|
|
18201
|
+
const listResult = await client.listMemoryEvents({
|
|
18202
|
+
workspace_id: workspaceId,
|
|
18203
|
+
project_id: projectId,
|
|
18204
|
+
limit: 50
|
|
18205
|
+
// Fetch more to filter
|
|
18206
|
+
});
|
|
18207
|
+
const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
|
|
18208
|
+
const snapshotEvents = allEvents.filter(
|
|
18209
|
+
(e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
|
|
18210
|
+
).slice(0, input.max_snapshots || 1);
|
|
18211
|
+
if (!snapshotEvents || snapshotEvents.length === 0) {
|
|
18212
|
+
return {
|
|
18213
|
+
content: [
|
|
18214
|
+
{
|
|
18215
|
+
type: "text",
|
|
18216
|
+
text: formatContent({
|
|
18217
|
+
restored: false,
|
|
18218
|
+
message: "No session snapshots found. Use session_capture_smart to save state before compaction.",
|
|
18219
|
+
hint: "Start fresh or use session_init to get recent context."
|
|
18220
|
+
})
|
|
18221
|
+
}
|
|
18222
|
+
]
|
|
18223
|
+
};
|
|
18224
|
+
}
|
|
18225
|
+
const snapshots = snapshotEvents.map((event) => {
|
|
18226
|
+
let snapshotData;
|
|
18227
|
+
try {
|
|
18228
|
+
snapshotData = JSON.parse(event.content || "{}");
|
|
18229
|
+
} catch {
|
|
18230
|
+
snapshotData = { conversation_summary: event.content };
|
|
18231
|
+
}
|
|
18232
|
+
return {
|
|
18233
|
+
snapshot_id: event.id,
|
|
18234
|
+
captured_at: snapshotData.captured_at || event.created_at,
|
|
18235
|
+
session_id: snapshotData.session_id || event.session_id,
|
|
18236
|
+
...snapshotData
|
|
18237
|
+
};
|
|
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
|
+
}
|
|
18270
|
+
const response = {
|
|
18271
|
+
restored: true,
|
|
18272
|
+
snapshots_found: snapshots.length,
|
|
18273
|
+
latest: snapshots[0],
|
|
18274
|
+
all_snapshots: snapshots.length > 1 ? snapshots : void 0,
|
|
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."
|
|
18277
|
+
};
|
|
18278
|
+
return {
|
|
18279
|
+
content: [{ type: "text", text: formatContent(response) }],
|
|
18280
|
+
structuredContent: toStructured(response)
|
|
18281
|
+
};
|
|
18282
|
+
}
|
|
16387
18283
|
default:
|
|
16388
18284
|
return errorResult(`Unknown action: ${input.action}`);
|
|
16389
18285
|
}
|
|
@@ -18948,7 +20844,7 @@ function registerPrompts(server) {
|
|
|
18948
20844
|
}
|
|
18949
20845
|
|
|
18950
20846
|
// src/session-manager.ts
|
|
18951
|
-
var SessionManager = class {
|
|
20847
|
+
var SessionManager = class _SessionManager {
|
|
18952
20848
|
constructor(server, client) {
|
|
18953
20849
|
this.server = server;
|
|
18954
20850
|
this.client = client;
|
|
@@ -18959,6 +20855,31 @@ var SessionManager = class {
|
|
|
18959
20855
|
this.folderPath = null;
|
|
18960
20856
|
this.contextSmartCalled = false;
|
|
18961
20857
|
this.warningShown = false;
|
|
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)
|
|
20861
|
+
this.sessionTokens = 0;
|
|
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;
|
|
18962
20883
|
}
|
|
18963
20884
|
/**
|
|
18964
20885
|
* Check if session has been auto-initialized
|
|
@@ -19001,10 +20922,124 @@ var SessionManager = class {
|
|
|
19001
20922
|
this.folderPath = path9;
|
|
19002
20923
|
}
|
|
19003
20924
|
/**
|
|
19004
|
-
* 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.
|
|
19005
20927
|
*/
|
|
19006
20928
|
markContextSmartCalled() {
|
|
19007
20929
|
this.contextSmartCalled = true;
|
|
20930
|
+
this.conversationTurns++;
|
|
20931
|
+
}
|
|
20932
|
+
/**
|
|
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.
|
|
20942
|
+
*/
|
|
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() {
|
|
20951
|
+
return this.sessionTokens;
|
|
20952
|
+
}
|
|
20953
|
+
/**
|
|
20954
|
+
* Get the current conversation turn count.
|
|
20955
|
+
*/
|
|
20956
|
+
getConversationTurns() {
|
|
20957
|
+
return this.conversationTurns;
|
|
20958
|
+
}
|
|
20959
|
+
/**
|
|
20960
|
+
* Get the context threshold (max tokens before compaction warning).
|
|
20961
|
+
*/
|
|
20962
|
+
getContextThreshold() {
|
|
20963
|
+
return this.contextThreshold;
|
|
20964
|
+
}
|
|
20965
|
+
/**
|
|
20966
|
+
* Set a custom context threshold (useful if client provides model info).
|
|
20967
|
+
*/
|
|
20968
|
+
setContextThreshold(threshold) {
|
|
20969
|
+
this.contextThreshold = threshold;
|
|
20970
|
+
}
|
|
20971
|
+
/**
|
|
20972
|
+
* Add tokens to the session count.
|
|
20973
|
+
* Call this after each tool response to track token accumulation.
|
|
20974
|
+
*
|
|
20975
|
+
* @param tokens - Exact token count or text to estimate
|
|
20976
|
+
*/
|
|
20977
|
+
addTokens(tokens) {
|
|
20978
|
+
if (typeof tokens === "number") {
|
|
20979
|
+
this.sessionTokens += tokens;
|
|
20980
|
+
} else {
|
|
20981
|
+
this.sessionTokens += Math.ceil(tokens.length / 4);
|
|
20982
|
+
}
|
|
20983
|
+
}
|
|
20984
|
+
/**
|
|
20985
|
+
* Estimate tokens from a tool response.
|
|
20986
|
+
* Uses a simple heuristic: ~4 characters per token.
|
|
20987
|
+
*/
|
|
20988
|
+
estimateTokens(content) {
|
|
20989
|
+
const text = typeof content === "string" ? content : JSON.stringify(content);
|
|
20990
|
+
return Math.ceil(text.length / 4);
|
|
20991
|
+
}
|
|
20992
|
+
/**
|
|
20993
|
+
* Reset token count (e.g., after compaction or new session).
|
|
20994
|
+
*/
|
|
20995
|
+
resetTokenCount() {
|
|
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;
|
|
19008
21043
|
}
|
|
19009
21044
|
/**
|
|
19010
21045
|
* Check if context_smart has been called and warn if not.
|
|
@@ -19270,6 +21305,112 @@ var SessionManager = class {
|
|
|
19270
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");
|
|
19271
21306
|
return parts.join("\n");
|
|
19272
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
|
+
}
|
|
19273
21414
|
};
|
|
19274
21415
|
|
|
19275
21416
|
// src/http-gateway.ts
|
|
@@ -20025,6 +22166,12 @@ function buildContextStreamMcpServer(params) {
|
|
|
20025
22166
|
env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
|
|
20026
22167
|
}
|
|
20027
22168
|
env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
|
|
22169
|
+
if (params.restoreContextEnabled === false) {
|
|
22170
|
+
env.CONTEXTSTREAM_RESTORE_CONTEXT = "false";
|
|
22171
|
+
}
|
|
22172
|
+
if (params.showTiming) {
|
|
22173
|
+
env.CONTEXTSTREAM_SHOW_TIMING = "true";
|
|
22174
|
+
}
|
|
20028
22175
|
if (IS_WINDOWS) {
|
|
20029
22176
|
return {
|
|
20030
22177
|
command: "cmd",
|
|
@@ -20047,6 +22194,12 @@ function buildContextStreamVsCodeServer(params) {
|
|
|
20047
22194
|
env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
|
|
20048
22195
|
}
|
|
20049
22196
|
env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
|
|
22197
|
+
if (params.restoreContextEnabled === false) {
|
|
22198
|
+
env.CONTEXTSTREAM_RESTORE_CONTEXT = "false";
|
|
22199
|
+
}
|
|
22200
|
+
if (params.showTiming) {
|
|
22201
|
+
env.CONTEXTSTREAM_SHOW_TIMING = "true";
|
|
22202
|
+
}
|
|
20050
22203
|
if (IS_WINDOWS) {
|
|
20051
22204
|
return {
|
|
20052
22205
|
type: "stdio",
|
|
@@ -20147,6 +22300,10 @@ async function upsertCodexTomlConfig(filePath, params) {
|
|
|
20147
22300
|
` : "";
|
|
20148
22301
|
const contextPackLine = `CONTEXTSTREAM_CONTEXT_PACK = "${params.contextPackEnabled === false ? "false" : "true"}"
|
|
20149
22302
|
`;
|
|
22303
|
+
const restoreContextLine = params.restoreContextEnabled === false ? `CONTEXTSTREAM_RESTORE_CONTEXT = "false"
|
|
22304
|
+
` : "";
|
|
22305
|
+
const showTimingLine = params.showTiming ? `CONTEXTSTREAM_SHOW_TIMING = "true"
|
|
22306
|
+
` : "";
|
|
20150
22307
|
const commandLine = IS_WINDOWS ? `command = "cmd"
|
|
20151
22308
|
args = ["/c", "npx", "-y", "@contextstream/mcp-server"]
|
|
20152
22309
|
` : `command = "npx"
|
|
@@ -20160,7 +22317,7 @@ args = ["-y", "@contextstream/mcp-server"]
|
|
|
20160
22317
|
[mcp_servers.contextstream.env]
|
|
20161
22318
|
CONTEXTSTREAM_API_URL = "${params.apiUrl}"
|
|
20162
22319
|
CONTEXTSTREAM_API_KEY = "${params.apiKey}"
|
|
20163
|
-
` + toolsetLine + contextPackLine;
|
|
22320
|
+
` + toolsetLine + contextPackLine + restoreContextLine + showTimingLine;
|
|
20164
22321
|
if (!exists) {
|
|
20165
22322
|
await fs7.writeFile(filePath, block.trimStart(), "utf8");
|
|
20166
22323
|
return "created";
|
|
@@ -20517,6 +22674,17 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
|
|
|
20517
22674
|
console.log(" Uses more operations/credits; can be disabled in settings or via env.");
|
|
20518
22675
|
const contextPackChoice = normalizeInput(await rl.question("Enable Context Pack? [Y/n]: "));
|
|
20519
22676
|
const contextPackEnabled = !(contextPackChoice.toLowerCase() === "n" || contextPackChoice.toLowerCase() === "no");
|
|
22677
|
+
console.log("\nResponse Timing:");
|
|
22678
|
+
console.log(" Show response time for tool calls (e.g., '\u2713 3 results in 142ms').");
|
|
22679
|
+
console.log(" Useful for debugging performance; disabled by default.");
|
|
22680
|
+
const showTimingChoice = normalizeInput(await rl.question("Show response timing? [y/N]: "));
|
|
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");
|
|
20520
22688
|
const editors = [
|
|
20521
22689
|
"codex",
|
|
20522
22690
|
"claude",
|
|
@@ -20567,7 +22735,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
|
|
|
20567
22735
|
console.log(" 1) Global");
|
|
20568
22736
|
console.log(" 2) Project");
|
|
20569
22737
|
console.log(" 3) Both");
|
|
20570
|
-
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";
|
|
20571
22739
|
const scope = scopeChoice === "1" ? "global" : scopeChoice === "2" ? "project" : "both";
|
|
20572
22740
|
console.log("\nInstall MCP server config as:");
|
|
20573
22741
|
if (hasCodex && !hasProjectMcpEditors) {
|
|
@@ -20591,18 +22759,22 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
|
|
|
20591
22759
|
)
|
|
20592
22760
|
) || mcpChoiceDefault;
|
|
20593
22761
|
const mcpScope = mcpChoice === "2" && hasCodex && !hasProjectMcpEditors ? "skip" : mcpChoice === "4" ? "skip" : mcpChoice === "1" ? "global" : mcpChoice === "2" ? "project" : "both";
|
|
20594
|
-
const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, toolset, contextPackEnabled });
|
|
22762
|
+
const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, toolset, contextPackEnabled, showTiming, restoreContextEnabled });
|
|
20595
22763
|
const mcpServerClaude = buildContextStreamMcpServer({
|
|
20596
22764
|
apiUrl,
|
|
20597
22765
|
apiKey,
|
|
20598
22766
|
toolset,
|
|
20599
|
-
contextPackEnabled
|
|
22767
|
+
contextPackEnabled,
|
|
22768
|
+
showTiming,
|
|
22769
|
+
restoreContextEnabled
|
|
20600
22770
|
});
|
|
20601
22771
|
const vsCodeServer = buildContextStreamVsCodeServer({
|
|
20602
22772
|
apiUrl,
|
|
20603
22773
|
apiKey,
|
|
20604
22774
|
toolset,
|
|
20605
|
-
contextPackEnabled
|
|
22775
|
+
contextPackEnabled,
|
|
22776
|
+
showTiming,
|
|
22777
|
+
restoreContextEnabled
|
|
20606
22778
|
});
|
|
20607
22779
|
const needsGlobalMcpConfig = mcpScope === "global" || mcpScope === "both" || mcpScope === "project" && hasCodex;
|
|
20608
22780
|
if (needsGlobalMcpConfig) {
|
|
@@ -20621,7 +22793,9 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
|
|
|
20621
22793
|
apiUrl,
|
|
20622
22794
|
apiKey,
|
|
20623
22795
|
toolset,
|
|
20624
|
-
contextPackEnabled
|
|
22796
|
+
contextPackEnabled,
|
|
22797
|
+
showTiming,
|
|
22798
|
+
restoreContextEnabled
|
|
20625
22799
|
});
|
|
20626
22800
|
writeActions.push({ kind: "mcp-config", target: filePath, status });
|
|
20627
22801
|
console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
|
|
@@ -20703,53 +22877,69 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
|
|
|
20703
22877
|
}
|
|
20704
22878
|
}
|
|
20705
22879
|
}
|
|
20706
|
-
|
|
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) {
|
|
20707
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");
|
|
20708
|
-
console.log("\u2502
|
|
22899
|
+
console.log("\u2502 AI Editor Hooks (Recommended) \u2502");
|
|
20709
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");
|
|
20710
22901
|
console.log("");
|
|
20711
|
-
console.log(" Problem:
|
|
20712
|
-
console.log("
|
|
20713
|
-
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.");
|
|
20714
22904
|
console.log("");
|
|
20715
22905
|
console.log(" Solution: Install hooks that:");
|
|
20716
|
-
console.log(" \u2713
|
|
20717
|
-
console.log(" \u2713
|
|
20718
|
-
console.log(" \u2713 Inject reminders
|
|
20719
|
-
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(", ")}`);
|
|
20720
22911
|
console.log("");
|
|
20721
22912
|
console.log(" You can disable hooks anytime with CONTEXTSTREAM_HOOK_ENABLED=false");
|
|
20722
22913
|
console.log("");
|
|
20723
22914
|
const installHooks = normalizeInput(
|
|
20724
|
-
await rl.question("Install
|
|
22915
|
+
await rl.question("Install editor hooks? [Y/n] (recommended): ")
|
|
20725
22916
|
).toLowerCase();
|
|
20726
22917
|
if (installHooks !== "n" && installHooks !== "no") {
|
|
20727
|
-
|
|
20728
|
-
|
|
20729
|
-
|
|
20730
|
-
|
|
20731
|
-
|
|
20732
|
-
|
|
20733
|
-
|
|
20734
|
-
|
|
20735
|
-
const result = await
|
|
20736
|
-
|
|
20737
|
-
|
|
20738
|
-
console.log(`- Created hook: ${script}`);
|
|
20739
|
-
});
|
|
20740
|
-
result.settings.forEach((settings) => {
|
|
20741
|
-
writeActions.push({ kind: "mcp-config", target: settings, status: "updated" });
|
|
20742
|
-
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"
|
|
20743
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}`);
|
|
20744
22937
|
}
|
|
20745
|
-
console.log(" Hooks installed. Disable with CONTEXTSTREAM_HOOK_ENABLED=false");
|
|
20746
|
-
} catch (err) {
|
|
20747
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
20748
|
-
console.log(`- Failed to install hooks: ${message}`);
|
|
20749
22938
|
}
|
|
22939
|
+
console.log(" Hooks installed. Disable with CONTEXTSTREAM_HOOK_ENABLED=false");
|
|
20750
22940
|
} else {
|
|
20751
22941
|
console.log("- Skipped hooks installation.");
|
|
20752
|
-
console.log(" Note: Without hooks,
|
|
22942
|
+
console.log(" Note: Without hooks, AI may still use default tools instead of ContextStream.");
|
|
20753
22943
|
}
|
|
20754
22944
|
}
|
|
20755
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");
|
|
@@ -20936,7 +23126,12 @@ Applying to ${projects.length} project(s)...`);
|
|
|
20936
23126
|
folder_path: projectPath,
|
|
20937
23127
|
workspace_id: workspaceId,
|
|
20938
23128
|
workspace_name: workspaceName,
|
|
20939
|
-
create_parent_mapping: createParentMapping
|
|
23129
|
+
create_parent_mapping: createParentMapping,
|
|
23130
|
+
// Include version and config info for desktop app compatibility
|
|
23131
|
+
version: VERSION,
|
|
23132
|
+
configured_editors: configuredEditors,
|
|
23133
|
+
context_pack: contextPackEnabled,
|
|
23134
|
+
api_url: apiUrl
|
|
20940
23135
|
});
|
|
20941
23136
|
writeActions.push({
|
|
20942
23137
|
kind: "workspace-config",
|
|
@@ -21053,6 +23248,7 @@ Applying to ${projects.length} project(s)...`);
|
|
|
21053
23248
|
console.log(`Toolset: ${toolset} (${toolsetDesc})`);
|
|
21054
23249
|
console.log(`Token reduction: ~75% compared to previous versions.`);
|
|
21055
23250
|
console.log(`Context Pack: ${contextPackEnabled ? "enabled" : "disabled"}`);
|
|
23251
|
+
console.log(`Response Timing: ${showTiming ? "enabled" : "disabled"}`);
|
|
21056
23252
|
}
|
|
21057
23253
|
console.log("\nNext steps:");
|
|
21058
23254
|
console.log("- Restart your editor/CLI after changing MCP config or rules.");
|
|
@@ -21068,6 +23264,9 @@ Applying to ${projects.length} project(s)...`);
|
|
|
21068
23264
|
console.log(
|
|
21069
23265
|
"- Toggle Context Pack with CONTEXTSTREAM_CONTEXT_PACK=true|false (and in dashboard settings)."
|
|
21070
23266
|
);
|
|
23267
|
+
console.log(
|
|
23268
|
+
"- Toggle Response Timing with CONTEXTSTREAM_SHOW_TIMING=true|false."
|
|
23269
|
+
);
|
|
21071
23270
|
console.log("");
|
|
21072
23271
|
console.log("You're set up! Now try these prompts in your AI tool:");
|
|
21073
23272
|
console.log(' 1) "session summary"');
|