@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 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 __commonJS = (cb, mod) => function __require() {
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
- if (!projectId && workspaceId && rootPath && params.auto_index !== false) {
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
- ## \u{1F6A8} MANDATORY RULE: DISPLAY AT START OF EVERY RESPONSE \u{1F6A8}
8875
+ ## ContextStream Rules
8799
8876
 
8800
8877
  <contextstream_rules>
8801
- 1. **SEARCH FIRST:** Before Glob/Grep/Search/Read \u2192 \`search(mode="hybrid", query="...")\` FIRST
8802
- 2. **EVERY MESSAGE:** Call \`context_smart(user_message="...")\` at start of response
8803
- 3. **LOCAL TOOLS:** ONLY if ContextStream search returns 0 results after retry
8878
+ 1. **SIMPLE UTILITY?** (list/show/version) \u2192 Just execute, skip context calls
8879
+ 2. **CODING TASK?** \u2192 session_init \u2192 context_smart \u2192 work \u2192 capture
8880
+ 3. **SEARCH FIRST:** Before Glob/Grep/Read \u2192 \`search(mode="hybrid")\` FIRST
8804
8881
  </contextstream_rules>
8805
8882
 
8806
- **You MUST display the <contextstream_rules> block verbatim at the start of EVERY response.**
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 any lessons in the returned context
8867
- - Lessons may be included based on semantic relevance to the user's message
8942
+ - Check for \`[LESSONS_WARNING]\` tag in the response
8943
+ - If present, you **MUST** tell the user about the lessons before proceeding
8944
+ - Lessons are proactively fetched when risky actions are detected (refactor, migrate, deploy, etc.)
8945
+ - **Do not skip or bury this warning** - lessons represent real past mistakes
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 - REQUIRED EVERY MESSAGE
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
- | Message | What to Call |
8894
- |---------|--------------|
8895
- | **1st message** | \`session_init(folder_path="...", context_hint="<user's message>")\`, then \`context_smart(...)\` |
8896
- | **\u26A0\uFE0F After session_init** | **CHECK \`lessons\` field** - if present, read and apply them BEFORE any work |
8897
- | **2nd+ messages** | \`context_smart(user_message="<user's message>", format="minified", max_tokens=400)\` |
8898
- | **\u{1F50D} ANY code search** | \`search(mode="hybrid", query="...")\` \u2014 ALWAYS before Glob/Grep/Search/Read |
8899
- | **\u26A0\uFE0F Before ANY risky work** | \`session(action="get_lessons", query="<topic>")\` \u2014 **MANDATORY, not optional** |
8900
- | **After completing task** | \`session(action="capture", event_type="decision", ...)\` - MUST capture |
8901
- | **User frustration/correction** | \`session(action="capture_lesson", ...)\` - MUST capture lessons |
8902
- | **Command/tool error + fix** | \`session(action="capture_lesson", ...)\` - MUST capture lessons |
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
- **NO EXCEPTIONS.** Do not skip even if you think you have enough context.
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 (Always Call)
8921
- - **\`session_init\`** - Initialize session with workspace detection + context
8922
- - **\`context_smart\`** - Semantic search for relevant context (CALL EVERY MESSAGE, including immediately after \`session_init\`)
9018
+ ### Standalone Tools
9019
+ - **\`session_init\`** - Initialize session with workspace detection + context (skip for simple utility operations)
9020
+ - **\`context_smart\`** - Semantic search for relevant context (skip for simple utility operations)
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 Update Notices
9067
+ ### Rules, Version & Lessons Notices
8970
9068
 
8971
- - If you see **[RULES_NOTICE]**, update rules via \`generate_rules()\` (or rerun setup).
8972
- - If you see **[VERSION_NOTICE]**, tell the user to update MCP using the provided command.
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 (MANDATORY)
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
- | You complete a task | \`session(action="capture", event_type="task", ...)\` | Capture what was done |
9218
+ | Complete significant task | \`session(action="capture", event_type="task", ...)\` | Capture what was done |
9077
9219
  | Need past context | \`session(action="recall", query="...")\` | "What did we decide about X?" |
9078
9220
 
9079
- **You MUST capture after completing any significant task.** This ensures future sessions have context.
9221
+ **DO NOT capture utility operations:**
9222
+ - \u274C "Listed workspaces" - not meaningful context
9223
+ - \u274C "Showed version" - not a decision
9224
+ - \u274C "Listed projects" - just data retrieval
9225
+
9226
+ **DO capture meaningful work:**
9227
+ - \u2705 Decisions, preferences, completed features
9228
+ - \u2705 Lessons from mistakes
9229
+ - \u2705 Insights about architecture or patterns
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
- ## \u{1F6A8} MANDATORY RULE: DISPLAY AT START OF EVERY RESPONSE \u{1F6A8}
9309
+ ## ContextStream Rules
9160
9310
 
9161
9311
  <contextstream_rules>
9162
- 1. **SEARCH FIRST:** Before Glob/Grep/Search/Read \u2192 \`search(mode="hybrid", query="...")\` FIRST
9163
- 2. **EVERY MESSAGE:** Call \`context_smart(user_message="...")\` at start of response
9164
- 3. **LOCAL TOOLS:** ONLY if ContextStream search returns 0 results after retry
9312
+ 1. **SIMPLE UTILITY?** (list/show/version) \u2192 Just execute, skip context calls
9313
+ 2. **CODING TASK?** \u2192 session_init \u2192 context_smart \u2192 work \u2192 capture
9314
+ 3. **SEARCH FIRST:** Before Glob/Grep/Read \u2192 \`search(mode="hybrid")\` FIRST
9165
9315
  </contextstream_rules>
9166
9316
 
9167
- **You MUST display the <contextstream_rules> block verbatim at the start of EVERY response.**
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
- ### Required Every Message
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
- return {
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
- return { preToolUse: preToolUsePath, userPrompt: userPromptPath };
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
- // src/token-savings.ts
9822
- var TOKEN_SAVINGS_FORMULA_VERSION = 1;
9823
- var MAX_CHARS_PER_EVENT = 2e7;
9824
- var BASE_OVERHEAD_CHARS = 500;
9825
- var CANDIDATE_MULTIPLIERS = {
9826
- // context_smart: Replaces reading multiple files to gather context
9827
- context_smart: 5,
9828
- ai_context_budget: 5,
9829
- // search: Semantic search replaces iterative Glob/Grep/Read cycles
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
- // src/tools.ts
9885
- var LESSON_DEDUP_WINDOW_MS = 2 * 60 * 1e3;
9886
- var recentLessonCaptures = /* @__PURE__ */ new Map();
9887
- var SEARCH_RULES_REMINDER_ENABLED = process.env.CONTEXTSTREAM_SEARCH_REMINDER?.toLowerCase() !== "false";
9888
- var SEARCH_RULES_REMINDER = `
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
- ${LESSONS_REMINDER_PREFIX}
9912
- ${lessonLines.join("\n")}`;
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
- **UPDATE NOW:** Run \`mcp__contextstream__generate_rules(overwrite_existing=true)\`
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
- **UPDATE NOW:** Run \`${versionNotice.upgrade_command || "npm update @contextstream/mcp-server"}\`
9940
- Then restart Claude Code to use the new version.
9941
- `.trim();
9942
- }
9943
- var DEFAULT_PARAM_DESCRIPTIONS = {
9944
- api_key: "ContextStream API key.",
9945
- apiKey: "ContextStream API key.",
9946
- jwt: "ContextStream JWT for authentication.",
9947
- workspace_id: "Workspace ID (UUID).",
9948
- workspaceId: "Workspace ID (UUID).",
9949
- project_id: "Project ID (UUID).",
9950
- projectId: "Project ID (UUID).",
9951
- node_id: "Node ID (UUID).",
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 (13)
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 (14)
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 hasClaude = editors.includes("claude");
14485
- const shouldInstallHooks = hasClaude && input.install_hooks !== false;
16057
+ const editorHookMap = {
16058
+ claude: "claude",
16059
+ cline: "cline",
16060
+ roo: "roo",
16061
+ kilo: "kilo",
16062
+ cursor: "cursor",
16063
+ windsurf: "windsurf"
16064
+ };
16065
+ const hookSupportedEditors = editors.filter((e) => e in editorHookMap);
16066
+ const shouldInstallHooks = hookSupportedEditors.length > 0 && input.install_hooks !== false;
14486
16067
  if (shouldInstallHooks) {
14487
16068
  try {
14488
16069
  if (input.dry_run) {
14489
- hooksResults = [
14490
- { file: "~/.claude/hooks/contextstream-redirect.py", status: "dry run - would create" },
14491
- { file: "~/.claude/hooks/contextstream-reminder.py", status: "dry run - would create" },
14492
- { file: "~/.claude/settings.json", status: "dry run - would update" }
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
- const hookResult = await installClaudeCodeHooks({ scope: "user" });
14496
- hooksResults = [
14497
- ...hookResult.scripts.map((f) => ({ file: f, status: "created" })),
14498
- ...hookResult.settings.map((f) => ({ file: f, status: "updated" }))
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 (hasClaude && input.install_hooks === false) {
14505
- hooksPrompt = "Hooks skipped. Claude may use default tools instead of ContextStream search.";
16126
+ } else if (hookSupportedEditors.length > 0 && input.install_hooks === false) {
16127
+ hooksPrompt = "Hooks skipped. AI may use default tools instead of ContextStream search.";
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
- const hasLessons = result.context.includes("|L:") || result.context.includes("L:") || result.context.toLowerCase().includes("lesson");
14904
- const lessonsWarningLine = hasLessons ? "\n\n\u26A0\uFE0F [LESSONS DETECTED] Review the L: items above - these are past mistakes. STOP and review before making similar changes." : "";
16592
+ let lessonsWarningLine = "";
16593
+ const riskyKeywords = detectRiskyActions(input.user_message);
16594
+ if (riskyKeywords.length > 0 && workspaceId) {
16595
+ try {
16596
+ const lessons = await client.getHighPriorityLessons({
16597
+ workspace_id: workspaceId,
16598
+ project_id: projectId,
16599
+ context_hint: riskyKeywords.join(" "),
16600
+ limit: 5
16601
+ });
16602
+ if (lessons.length > 0) {
16603
+ const lessonLines = lessons.slice(0, 5).map((l, i) => {
16604
+ const severity = l.severity === "critical" ? "\u{1F6A8}" : l.severity === "high" ? "\u26A0\uFE0F" : "\u{1F4DD}";
16605
+ const title = l.title || "Untitled lesson";
16606
+ const prevention = l.prevention || "";
16607
+ return `${i + 1}. ${severity} ${title}${prevention ? `: ${prevention.slice(0, 100)}` : ""}`;
16608
+ });
16609
+ lessonsWarningLine = `
16610
+
16611
+ \u{1F6A8} [LESSONS_WARNING] Relevant Lessons for "${riskyKeywords.slice(0, 3).join(", ")}"
16612
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
16613
+ \u26A0\uFE0F IMPORTANT: You MUST tell the user about these lessons before proceeding.
16614
+ These are past mistakes that may be relevant to the current task.
16615
+
16616
+ ${lessonLines.join("\n")}
16617
+
16618
+ Action: Review each lesson and explain to the user how you will avoid these mistakes.
16619
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`;
16620
+ }
16621
+ } catch {
16622
+ }
16623
+ }
16624
+ if (!lessonsWarningLine) {
16625
+ const hasLessonsInContext = result.context.includes("|L:") || result.context.includes("L:") || result.context.toLowerCase().includes("lesson");
16626
+ if (hasLessonsInContext) {
16627
+ lessonsWarningLine = "\n\n\u26A0\uFE0F [LESSONS_WARNING] Lessons found in context - review the L: items above before making changes.";
16628
+ }
16629
+ }
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: result.context + footer + allWarnings
16672
+ text: finalContext + footer + allWarnings
14923
16673
  }
14924
16674
  ],
14925
- structuredContent: toStructured(enrichedResult)
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 outputText = formatContent(result);
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 3): ")) || "3";
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
- if (configuredEditors.includes("claude")) {
22880
+ const HOOKS_SUPPORTED_EDITORS = {
22881
+ claude: "claude",
22882
+ cursor: "cursor",
22883
+ windsurf: "windsurf",
22884
+ cline: "cline",
22885
+ roo: "roo",
22886
+ kilo: "kilo",
22887
+ codex: null,
22888
+ // No hooks API
22889
+ aider: null,
22890
+ // No hooks API
22891
+ antigravity: null
22892
+ // No hooks API
22893
+ };
22894
+ const hookEligibleEditors = configuredEditors.filter(
22895
+ (e) => HOOKS_SUPPORTED_EDITORS[e] !== null
22896
+ );
22897
+ if (hookEligibleEditors.length > 0) {
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 Claude Code Hooks (Recommended) \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: Claude Code often ignores CLAUDE.md instructions and uses");
20712
- console.log(" its default tools (Grep/Glob/Search) instead of ContextStream search.");
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 Block default search tools (Grep/Glob/Search) \u2192 redirect to ContextStream");
20717
- console.log(" \u2713 Block built-in plan mode \u2192 redirect to ContextStream plans (persistent)");
20718
- console.log(" \u2713 Inject reminders on every message to keep rules in context");
20719
- console.log(" \u2713 Result: Faster searches, persistent plans across sessions");
22906
+ console.log(" \u2713 Use ContextStream (indexed, faster) with default tool use");
22907
+ console.log(" \u2713 Use ContextStream plans (persistent) with default tool use");
22908
+ console.log(" \u2713 Inject reminders to keep rules in context");
22909
+ console.log("");
22910
+ console.log(` Hooks available for: ${hookEligibleEditors.map((e) => EDITOR_LABELS[e]).join(", ")}`);
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 Claude Code hooks? [Y/n] (recommended): ")
22915
+ await rl.question("Install editor hooks? [Y/n] (recommended): ")
20725
22916
  ).toLowerCase();
20726
22917
  if (installHooks !== "n" && installHooks !== "no") {
20727
- try {
20728
- if (dryRun) {
20729
- console.log("- Would install hooks to ~/.claude/hooks/");
20730
- console.log("- Would update ~/.claude/settings.json");
20731
- writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "hooks", "contextstream-redirect.py"), status: "dry-run" });
20732
- writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "hooks", "contextstream-reminder.py"), status: "dry-run" });
20733
- writeActions.push({ kind: "mcp-config", target: path8.join(homedir5(), ".claude", "settings.json"), status: "dry-run" });
20734
- } else {
20735
- const result = await installClaudeCodeHooks({ scope: "user" });
20736
- result.scripts.forEach((script) => {
20737
- writeActions.push({ kind: "mcp-config", target: script, status: "created" });
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, Claude may still use default tools instead of ContextStream.");
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"');