@contextstream/mcp-server 0.4.37 → 0.4.38

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);
@@ -8981,6 +9058,39 @@ If context still feels missing, use \`session(action="recall", query="...")\` fo
8981
9058
 
8982
9059
  ---
8983
9060
 
9061
+ ### Context Pressure & Compaction Awareness
9062
+
9063
+ ContextStream tracks context pressure to help you stay ahead of conversation compaction:
9064
+
9065
+ **Automatic tracking:** Token usage is tracked automatically. \`context_smart\` returns \`context_pressure\` when usage is high.
9066
+
9067
+ **When \`context_smart\` returns \`context_pressure\` with high/critical level:**
9068
+ 1. Review the \`suggested_action\` field:
9069
+ - \`prepare_save\`: Start thinking about saving important state
9070
+ - \`save_now\`: Immediately call \`session(action="capture", event_type="session_snapshot")\` to preserve state
9071
+
9072
+ **PreCompact Hook (Optional):** If enabled, Claude Code will inject a reminder to save state before compaction.
9073
+ Enable with: \`generate_rules(install_hooks=true, include_pre_compact=true)\`
9074
+
9075
+ **Before compaction happens (when warned):**
9076
+ \`\`\`
9077
+ session(action="capture", event_type="session_snapshot", title="Pre-compaction snapshot", content="{
9078
+ \\"conversation_summary\\": \\"<summarize what we've been doing>\\",
9079
+ \\"current_goal\\": \\"<the main task>\\",
9080
+ \\"active_files\\": [\\"file1.ts\\", \\"file2.ts\\"],
9081
+ \\"recent_decisions\\": [{title: \\"...\\", rationale: \\"...\\"}],
9082
+ \\"unfinished_work\\": [{task: \\"...\\", status: \\"...\\", next_steps: \\"...\\"}]
9083
+ }")
9084
+ \`\`\`
9085
+
9086
+ **After compaction (when context seems lost):**
9087
+ 1. Call \`session_init(folder_path="...", is_post_compact=true)\` - this auto-restores the most recent snapshot
9088
+ 2. Or call \`session_restore_context()\` directly to get the saved state
9089
+ 3. Review the \`restored_context\` to understand prior work
9090
+ 4. Acknowledge to the user what was restored and continue
9091
+
9092
+ ---
9093
+
8984
9094
  ### Index Status (Auto-Managed)
8985
9095
 
8986
9096
  **Indexing is automatic.** After \`session_init\`, the project is auto-indexed in the background.
@@ -9214,12 +9324,27 @@ ContextStream search is **indexed** and returns semantic matches + context in ON
9214
9324
  - Before risky work: \`session(action="get_lessons", query="<topic>")\`
9215
9325
  - On mistakes: \`session(action="capture_lesson", title="...", trigger="...", impact="...", prevention="...")\`
9216
9326
 
9327
+ ### Context Pressure & Compaction
9328
+
9329
+ - If \`context_smart\` returns high/critical \`context_pressure\`: call \`session_capture_smart(...)\` to save state
9330
+ - After compaction (context lost): call \`session_init(..., is_post_compact=true)\` or \`session_restore_context()\`
9331
+
9217
9332
  ### Plans & Tasks
9218
9333
 
9219
9334
  When user asks for a plan, use ContextStream (not EnterPlanMode):
9220
9335
  1. \`session(action="capture_plan", title="...", steps=[...])\`
9221
9336
  2. \`memory(action="create_task", title="...", plan_id="<id>")\`
9222
9337
 
9338
+ ### Workspace-Only Mode (Multi-Project Folders)
9339
+
9340
+ If working in a parent folder containing multiple projects:
9341
+ \`\`\`
9342
+ session_init(folder_path="...", skip_project_creation=true)
9343
+ \`\`\`
9344
+
9345
+ This enables workspace-level memory and context without project-specific indexing.
9346
+ Use for monorepos or folders with multiple independent projects.
9347
+
9223
9348
  Full docs: https://contextstream.io/docs/mcp/tools
9224
9349
  `.trim();
9225
9350
  var TEMPLATES = {
@@ -9672,6 +9797,81 @@ def main():
9672
9797
  print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": REMINDER}}))
9673
9798
  sys.exit(0)
9674
9799
 
9800
+ if __name__ == "__main__":
9801
+ main()
9802
+ `;
9803
+ var PRECOMPACT_HOOK_SCRIPT = `#!/usr/bin/env python3
9804
+ """
9805
+ ContextStream PreCompact Hook for Claude Code
9806
+
9807
+ Runs BEFORE conversation context is compacted (manual via /compact or automatic).
9808
+ Injects a reminder for the AI to save conversation state using session_capture_smart.
9809
+
9810
+ Input (via stdin):
9811
+ {
9812
+ "session_id": "...",
9813
+ "transcript_path": "/path/to/transcript.jsonl",
9814
+ "permission_mode": "default",
9815
+ "hook_event_name": "PreCompact",
9816
+ "trigger": "manual" | "auto",
9817
+ "custom_instructions": "..."
9818
+ }
9819
+
9820
+ Output (to stdout):
9821
+ {
9822
+ "hookSpecificOutput": {
9823
+ "hookEventName": "PreCompact",
9824
+ "additionalContext": "... instructions for AI ..."
9825
+ }
9826
+ }
9827
+ """
9828
+
9829
+ import json
9830
+ import sys
9831
+ import os
9832
+
9833
+ ENABLED = os.environ.get("CONTEXTSTREAM_PRECOMPACT_ENABLED", "true").lower() == "true"
9834
+
9835
+ def main():
9836
+ if not ENABLED:
9837
+ sys.exit(0)
9838
+
9839
+ try:
9840
+ data = json.load(sys.stdin)
9841
+ except:
9842
+ sys.exit(0)
9843
+
9844
+ trigger = data.get("trigger", "unknown")
9845
+ custom_instructions = data.get("custom_instructions", "")
9846
+
9847
+ # Build context injection for the AI
9848
+ context = f"""[CONTEXT COMPACTION IMMINENT - {trigger.upper()}]
9849
+ CRITICAL: Before context is compacted, you MUST save conversation state:
9850
+
9851
+ 1. IMMEDIATELY call: mcp__contextstream__session(action="capture", event_type="session_snapshot", title="Pre-compaction snapshot", content="<JSON with: conversation_summary, active_goals, recent_decisions, active_files, unfinished_work>")
9852
+
9853
+ 2. Include in the snapshot:
9854
+ - conversation_summary: Brief summary of what was discussed
9855
+ - active_goals: List of goals/tasks in progress
9856
+ - recent_decisions: Key decisions made in this session
9857
+ - active_files: Files currently being worked on
9858
+ - unfinished_work: Any incomplete tasks
9859
+
9860
+ 3. After compaction, call session_init(is_post_compact=true) to restore context.
9861
+
9862
+ {f"User instructions: {custom_instructions}" if custom_instructions else ""}
9863
+ [END COMPACTION WARNING]"""
9864
+
9865
+ output = {
9866
+ "hookSpecificOutput": {
9867
+ "hookEventName": "PreCompact",
9868
+ "additionalContext": context
9869
+ }
9870
+ }
9871
+
9872
+ print(json.dumps(output))
9873
+ sys.exit(0)
9874
+
9675
9875
  if __name__ == "__main__":
9676
9876
  main()
9677
9877
  `;
@@ -9687,11 +9887,12 @@ function getClaudeSettingsPath(scope, projectPath) {
9687
9887
  function getHooksDir() {
9688
9888
  return path5.join(homedir2(), ".claude", "hooks");
9689
9889
  }
9690
- function buildHooksConfig() {
9890
+ function buildHooksConfig(options) {
9691
9891
  const hooksDir = getHooksDir();
9692
9892
  const preToolUsePath = path5.join(hooksDir, "contextstream-redirect.py");
9693
9893
  const userPromptPath = path5.join(hooksDir, "contextstream-reminder.py");
9694
- return {
9894
+ const preCompactPath = path5.join(hooksDir, "contextstream-precompact.py");
9895
+ const config = {
9695
9896
  PreToolUse: [
9696
9897
  {
9697
9898
  matcher: "Glob|Grep|Search|Task|EnterPlanMode",
@@ -9717,15 +9918,40 @@ function buildHooksConfig() {
9717
9918
  }
9718
9919
  ]
9719
9920
  };
9921
+ if (options?.includePreCompact) {
9922
+ config.PreCompact = [
9923
+ {
9924
+ // Match both manual (/compact) and automatic compaction
9925
+ matcher: "*",
9926
+ hooks: [
9927
+ {
9928
+ type: "command",
9929
+ command: `python3 "${preCompactPath}"`,
9930
+ timeout: 10
9931
+ }
9932
+ ]
9933
+ }
9934
+ ];
9935
+ }
9936
+ return config;
9720
9937
  }
9721
- async function installHookScripts() {
9938
+ async function installHookScripts(options) {
9722
9939
  const hooksDir = getHooksDir();
9723
9940
  await fs4.mkdir(hooksDir, { recursive: true });
9724
9941
  const preToolUsePath = path5.join(hooksDir, "contextstream-redirect.py");
9725
9942
  const userPromptPath = path5.join(hooksDir, "contextstream-reminder.py");
9943
+ const preCompactPath = path5.join(hooksDir, "contextstream-precompact.py");
9726
9944
  await fs4.writeFile(preToolUsePath, PRETOOLUSE_HOOK_SCRIPT, { mode: 493 });
9727
9945
  await fs4.writeFile(userPromptPath, USER_PROMPT_HOOK_SCRIPT, { mode: 493 });
9728
- return { preToolUse: preToolUsePath, userPrompt: userPromptPath };
9946
+ const result = {
9947
+ preToolUse: preToolUsePath,
9948
+ userPrompt: userPromptPath
9949
+ };
9950
+ if (options?.includePreCompact) {
9951
+ await fs4.writeFile(preCompactPath, PRECOMPACT_HOOK_SCRIPT, { mode: 493 });
9952
+ result.preCompact = preCompactPath;
9953
+ }
9954
+ return result;
9729
9955
  }
9730
9956
  async function readClaudeSettings(scope, projectPath) {
9731
9957
  const settingsPath = getClaudeSettingsPath(scope, projectPath);
@@ -9759,16 +9985,22 @@ function mergeHooksIntoSettings(existingSettings, newHooks) {
9759
9985
  async function installClaudeCodeHooks(options) {
9760
9986
  const result = { scripts: [], settings: [] };
9761
9987
  if (!options.dryRun) {
9762
- const scripts = await installHookScripts();
9988
+ const scripts = await installHookScripts({ includePreCompact: options.includePreCompact });
9763
9989
  result.scripts.push(scripts.preToolUse, scripts.userPrompt);
9990
+ if (scripts.preCompact) {
9991
+ result.scripts.push(scripts.preCompact);
9992
+ }
9764
9993
  } else {
9765
9994
  const hooksDir = getHooksDir();
9766
9995
  result.scripts.push(
9767
9996
  path5.join(hooksDir, "contextstream-redirect.py"),
9768
9997
  path5.join(hooksDir, "contextstream-reminder.py")
9769
9998
  );
9999
+ if (options.includePreCompact) {
10000
+ result.scripts.push(path5.join(hooksDir, "contextstream-precompact.py"));
10001
+ }
9770
10002
  }
9771
- const hooksConfig = buildHooksConfig();
10003
+ const hooksConfig = buildHooksConfig({ includePreCompact: options.includePreCompact });
9772
10004
  if (options.scope === "user" || options.scope === "both") {
9773
10005
  const settingsPath = getClaudeSettingsPath("user");
9774
10006
  if (!options.dryRun) {
@@ -10618,13 +10850,17 @@ function resolveAuthOverride(extra) {
10618
10850
  return { workspaceId, projectId };
10619
10851
  }
10620
10852
  var LIGHT_TOOLSET = /* @__PURE__ */ new Set([
10621
- // Core session tools (13)
10853
+ // Core session tools (15)
10622
10854
  "session_init",
10623
10855
  "session_tools",
10624
10856
  "context_smart",
10625
10857
  "context_feedback",
10626
10858
  "session_summary",
10627
10859
  "session_capture",
10860
+ "session_capture_smart",
10861
+ // Pre-compaction state capture
10862
+ "session_restore_context",
10863
+ // Post-compaction context restore
10628
10864
  "session_capture_lesson",
10629
10865
  "session_get_lessons",
10630
10866
  "session_recall",
@@ -10664,13 +10900,17 @@ var LIGHT_TOOLSET = /* @__PURE__ */ new Set([
10664
10900
  "mcp_server_version"
10665
10901
  ]);
10666
10902
  var STANDARD_TOOLSET = /* @__PURE__ */ new Set([
10667
- // Core session tools (14)
10903
+ // Core session tools (16)
10668
10904
  "session_init",
10669
10905
  "session_tools",
10670
10906
  "context_smart",
10671
10907
  "context_feedback",
10672
10908
  "session_summary",
10673
10909
  "session_capture",
10910
+ "session_capture_smart",
10911
+ // Pre-compaction state capture
10912
+ "session_restore_context",
10913
+ // Post-compaction context restore
10674
10914
  "session_capture_lesson",
10675
10915
  "session_get_lessons",
10676
10916
  "session_recall",
@@ -11121,6 +11361,7 @@ function parsePositiveInt(raw, fallback) {
11121
11361
  }
11122
11362
  var OUTPUT_FORMAT = process.env.CONTEXTSTREAM_OUTPUT_FORMAT || "compact";
11123
11363
  var COMPACT_OUTPUT = OUTPUT_FORMAT === "compact";
11364
+ var SHOW_TIMING = process.env.CONTEXTSTREAM_SHOW_TIMING === "true" || process.env.CONTEXTSTREAM_SHOW_TIMING === "1";
11124
11365
  var DEFAULT_SEARCH_LIMIT = parsePositiveInt(process.env.CONTEXTSTREAM_SEARCH_LIMIT, 3);
11125
11366
  var DEFAULT_SEARCH_CONTENT_MAX_CHARS = parsePositiveInt(
11126
11367
  process.env.CONTEXTSTREAM_SEARCH_MAX_CHARS,
@@ -11233,6 +11474,31 @@ function toStructured(data) {
11233
11474
  }
11234
11475
  return void 0;
11235
11476
  }
11477
+ function formatTimingSummary(roundTripMs, resultCount) {
11478
+ if (!SHOW_TIMING) return "";
11479
+ const countStr = resultCount !== void 0 ? `${resultCount} results` : "done";
11480
+ return `\u2713 ${countStr} in ${roundTripMs}ms
11481
+
11482
+ `;
11483
+ }
11484
+ function getResultCount(data) {
11485
+ if (!data || typeof data !== "object") return void 0;
11486
+ const response = data;
11487
+ const dataObj = response.data;
11488
+ if (dataObj?.results && Array.isArray(dataObj.results)) {
11489
+ return dataObj.results.length;
11490
+ }
11491
+ if (typeof dataObj?.total === "number") {
11492
+ return dataObj.total;
11493
+ }
11494
+ if (typeof dataObj?.count === "number") {
11495
+ return dataObj.count;
11496
+ }
11497
+ if (dataObj?.paths && Array.isArray(dataObj.paths)) {
11498
+ return dataObj.paths.length;
11499
+ }
11500
+ return void 0;
11501
+ }
11236
11502
  function readStatNumber(payload, key) {
11237
11503
  if (!payload || typeof payload !== "object") return void 0;
11238
11504
  const direct = payload[key];
@@ -13553,10 +13819,17 @@ This does semantic search on the first message. You only need context_smart on s
13553
13819
  auto_index: external_exports.boolean().optional().describe("Automatically create and index project from IDE workspace (default: true)"),
13554
13820
  allow_no_workspace: external_exports.boolean().optional().describe(
13555
13821
  "If true, allow session_init to return connected even if no workspace is resolved (workspace-level tools may not work)."
13822
+ ),
13823
+ skip_project_creation: external_exports.boolean().optional().describe(
13824
+ "If true, skip automatic project creation/matching. Use for parent folders containing multiple projects where you want workspace-level context but no project-specific context."
13825
+ ),
13826
+ is_post_compact: external_exports.boolean().optional().describe(
13827
+ "Set to true when resuming after conversation compaction. This prioritizes session_snapshot restoration and recent decisions."
13556
13828
  )
13557
13829
  })
13558
13830
  },
13559
13831
  async (input) => {
13832
+ const startTime = Date.now();
13560
13833
  let ideRoots = [];
13561
13834
  try {
13562
13835
  const rootsResponse = await server.server.listRoots();
@@ -13572,8 +13845,48 @@ This does semantic search on the first message. You only need context_smart on s
13572
13845
  }
13573
13846
  const result = await client.initSession(input, ideRoots);
13574
13847
  result.tools_hint = getCoreToolsHint();
13848
+ if (input.is_post_compact) {
13849
+ const workspaceIdForRestore = typeof result.workspace_id === "string" ? result.workspace_id : void 0;
13850
+ const projectIdForRestore = typeof result.project_id === "string" ? result.project_id : void 0;
13851
+ if (workspaceIdForRestore) {
13852
+ try {
13853
+ const snapshotSearch = await client.searchEvents({
13854
+ workspace_id: workspaceIdForRestore,
13855
+ project_id: projectIdForRestore,
13856
+ query: "session_snapshot",
13857
+ event_types: ["session_snapshot"],
13858
+ limit: 1
13859
+ });
13860
+ const snapshots = snapshotSearch?.data?.results || snapshotSearch?.results || snapshotSearch?.data || [];
13861
+ if (snapshots && snapshots.length > 0) {
13862
+ const latestSnapshot = snapshots[0];
13863
+ let snapshotData;
13864
+ try {
13865
+ snapshotData = JSON.parse(latestSnapshot.content);
13866
+ } catch {
13867
+ snapshotData = { conversation_summary: latestSnapshot.content };
13868
+ }
13869
+ result.restored_context = {
13870
+ snapshot_id: latestSnapshot.id,
13871
+ captured_at: snapshotData.captured_at || latestSnapshot.created_at,
13872
+ ...snapshotData
13873
+ };
13874
+ result.is_post_compact = true;
13875
+ result.post_compact_hint = "Session restored from pre-compaction snapshot. Review the 'restored_context' to continue where you left off.";
13876
+ } else {
13877
+ result.is_post_compact = true;
13878
+ result.post_compact_hint = "Post-compaction session started, but no snapshots found. Use context_smart to retrieve relevant context.";
13879
+ }
13880
+ } catch (err) {
13881
+ console.error("[ContextStream] Failed to restore post-compact context:", err);
13882
+ result.is_post_compact = true;
13883
+ result.post_compact_hint = "Post-compaction session started. Snapshot restoration failed, use context_smart for context.";
13884
+ }
13885
+ }
13886
+ }
13575
13887
  if (sessionManager) {
13576
13888
  sessionManager.markInitialized(result);
13889
+ sessionManager.resetTokenCount();
13577
13890
  }
13578
13891
  const folderPathForRules = input.folder_path || ideRoots[0] || resolveFolderPath(void 0, sessionManager);
13579
13892
  if (sessionManager && folderPathForRules) {
@@ -13720,6 +14033,12 @@ ${noticeLines.filter(Boolean).join("\n")}`;
13720
14033
  text = `${text}
13721
14034
 
13722
14035
  ${SEARCH_RULES_REMINDER}`;
14036
+ }
14037
+ const roundTripMs = Date.now() - startTime;
14038
+ if (SHOW_TIMING) {
14039
+ text = `\u2713 session initialized in ${roundTripMs}ms
14040
+
14041
+ ${text}`;
13723
14042
  }
13724
14043
  return {
13725
14044
  content: [{ type: "text", text }],
@@ -13997,8 +14316,11 @@ Use this to persist decisions, insights, preferences, or important information.`
13997
14316
  // Extracted lesson from correction
13998
14317
  "warning",
13999
14318
  // Proactive reminder
14000
- "frustration"
14319
+ "frustration",
14001
14320
  // User expressed frustration
14321
+ // Compaction awareness
14322
+ "session_snapshot"
14323
+ // Pre-compaction state capture
14002
14324
  ]).describe("Type of context being captured"),
14003
14325
  title: external_exports.string().describe("Brief title for the captured context"),
14004
14326
  content: external_exports.string().describe("Full content/details to capture"),
@@ -14053,6 +14375,224 @@ Use this to persist decisions, insights, preferences, or important information.`
14053
14375
  };
14054
14376
  }
14055
14377
  );
14378
+ registerTool(
14379
+ "session_capture_smart",
14380
+ {
14381
+ title: "Smart capture for conversation compaction",
14382
+ description: `Intelligently capture conversation state before compaction or context loss.
14383
+ This creates a session_snapshot that can be restored after compaction.
14384
+
14385
+ Use when:
14386
+ - Context pressure is high/critical (context_smart returns threshold_warning)
14387
+ - Before manual /compact commands
14388
+ - When significant work progress needs preservation
14389
+
14390
+ Captures:
14391
+ - Conversation summary and current goals
14392
+ - Active files being worked on
14393
+ - Recent decisions with rationale
14394
+ - Unfinished work items
14395
+ - User preferences expressed in session
14396
+
14397
+ The snapshot is automatically prioritized during post-compaction session_init.`,
14398
+ inputSchema: external_exports.object({
14399
+ workspace_id: external_exports.string().uuid().optional(),
14400
+ project_id: external_exports.string().uuid().optional(),
14401
+ conversation_summary: external_exports.string().describe("AI's summary of the conversation so far - what was discussed and accomplished"),
14402
+ current_goal: external_exports.string().optional().describe("The primary goal or task being worked on"),
14403
+ active_files: external_exports.array(external_exports.string()).optional().describe("List of files currently being worked on"),
14404
+ recent_decisions: external_exports.array(
14405
+ external_exports.object({
14406
+ title: external_exports.string(),
14407
+ rationale: external_exports.string().optional()
14408
+ })
14409
+ ).optional().describe("Key decisions made in this session with their rationale"),
14410
+ unfinished_work: external_exports.array(
14411
+ external_exports.object({
14412
+ task: external_exports.string(),
14413
+ status: external_exports.string().optional(),
14414
+ next_steps: external_exports.string().optional()
14415
+ })
14416
+ ).optional().describe("Work items that are in progress or pending"),
14417
+ user_preferences: external_exports.array(external_exports.string()).optional().describe("Preferences expressed by user during this session"),
14418
+ priority_items: external_exports.array(external_exports.string()).optional().describe("User-flagged important items to remember"),
14419
+ metadata: external_exports.record(external_exports.unknown()).optional().describe("Additional context to preserve")
14420
+ })
14421
+ },
14422
+ async (input) => {
14423
+ let workspaceId = input.workspace_id;
14424
+ let projectId = input.project_id;
14425
+ if (!workspaceId && sessionManager) {
14426
+ const ctx = sessionManager.getContext();
14427
+ if (ctx) {
14428
+ workspaceId = ctx.workspace_id;
14429
+ projectId = projectId || ctx.project_id;
14430
+ }
14431
+ }
14432
+ if (!workspaceId) {
14433
+ return errorResult(
14434
+ "Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly."
14435
+ );
14436
+ }
14437
+ const snapshotContent = {
14438
+ conversation_summary: input.conversation_summary,
14439
+ captured_at: (/* @__PURE__ */ new Date()).toISOString()
14440
+ };
14441
+ if (input.current_goal) {
14442
+ snapshotContent.current_goal = input.current_goal;
14443
+ }
14444
+ if (input.active_files?.length) {
14445
+ snapshotContent.active_files = input.active_files;
14446
+ }
14447
+ if (input.recent_decisions?.length) {
14448
+ snapshotContent.recent_decisions = input.recent_decisions;
14449
+ }
14450
+ if (input.unfinished_work?.length) {
14451
+ snapshotContent.unfinished_work = input.unfinished_work;
14452
+ }
14453
+ if (input.user_preferences?.length) {
14454
+ snapshotContent.user_preferences = input.user_preferences;
14455
+ }
14456
+ if (input.priority_items?.length) {
14457
+ snapshotContent.priority_items = input.priority_items;
14458
+ }
14459
+ if (input.metadata) {
14460
+ snapshotContent.metadata = input.metadata;
14461
+ }
14462
+ const result = await client.captureContext({
14463
+ workspace_id: workspaceId,
14464
+ project_id: projectId,
14465
+ event_type: "session_snapshot",
14466
+ title: `Session Snapshot: ${input.current_goal || "Conversation State"}`,
14467
+ content: JSON.stringify(snapshotContent, null, 2),
14468
+ importance: "high",
14469
+ tags: ["session_snapshot", "pre_compaction"]
14470
+ });
14471
+ const response = {
14472
+ ...result,
14473
+ snapshot_id: result?.data?.id || result?.id,
14474
+ message: "Session state captured successfully. This snapshot will be prioritized after compaction.",
14475
+ hint: "After compaction, call session_init with is_post_compact=true to restore this context."
14476
+ };
14477
+ return {
14478
+ content: [{ type: "text", text: formatContent(response) }],
14479
+ structuredContent: toStructured(response)
14480
+ };
14481
+ }
14482
+ );
14483
+ registerTool(
14484
+ "session_restore_context",
14485
+ {
14486
+ title: "Restore context after compaction",
14487
+ description: `Restore conversation context after compaction or context loss.
14488
+ Call this after conversation compaction to retrieve saved session state.
14489
+
14490
+ Returns structured context including:
14491
+ - conversation_summary: What was being discussed
14492
+ - current_goal: The primary task being worked on
14493
+ - active_files: Files that were being modified
14494
+ - recent_decisions: Key decisions made in the session
14495
+ - unfinished_work: Tasks that are still in progress
14496
+ - user_preferences: Preferences expressed during the session
14497
+
14498
+ Use this in combination with session_init(is_post_compact=true) for seamless continuation.`,
14499
+ inputSchema: external_exports.object({
14500
+ workspace_id: external_exports.string().uuid().optional(),
14501
+ project_id: external_exports.string().uuid().optional(),
14502
+ snapshot_id: external_exports.string().uuid().optional().describe("Specific snapshot ID to restore (defaults to most recent)"),
14503
+ max_snapshots: external_exports.number().optional().default(1).describe("Number of recent snapshots to consider (default: 1)")
14504
+ })
14505
+ },
14506
+ async (input) => {
14507
+ let workspaceId = input.workspace_id;
14508
+ let projectId = input.project_id;
14509
+ if (!workspaceId && sessionManager) {
14510
+ const ctx = sessionManager.getContext();
14511
+ if (ctx) {
14512
+ workspaceId = ctx.workspace_id;
14513
+ projectId = projectId || ctx.project_id;
14514
+ }
14515
+ }
14516
+ if (!workspaceId) {
14517
+ return errorResult(
14518
+ "Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly."
14519
+ );
14520
+ }
14521
+ try {
14522
+ if (input.snapshot_id) {
14523
+ const eventResult = await client.getEvent(input.snapshot_id);
14524
+ const event = eventResult?.data || eventResult;
14525
+ if (!event || !event.content) {
14526
+ return errorResult(
14527
+ `Snapshot not found: ${input.snapshot_id}. The snapshot may have been deleted or does not exist.`
14528
+ );
14529
+ }
14530
+ let snapshotData2;
14531
+ try {
14532
+ snapshotData2 = JSON.parse(event.content);
14533
+ } catch {
14534
+ snapshotData2 = { conversation_summary: event.content };
14535
+ }
14536
+ const response2 = {
14537
+ restored: true,
14538
+ snapshot_id: event.id,
14539
+ captured_at: snapshotData2.captured_at || event.created_at,
14540
+ ...snapshotData2,
14541
+ hint: "Context restored. Continue the conversation with awareness of the above state."
14542
+ };
14543
+ return {
14544
+ content: [{ type: "text", text: formatContent(response2) }],
14545
+ structuredContent: toStructured(response2)
14546
+ };
14547
+ }
14548
+ const listResult = await client.listMemoryEvents({
14549
+ workspace_id: workspaceId,
14550
+ project_id: projectId,
14551
+ limit: 50
14552
+ // Fetch more to filter
14553
+ });
14554
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
14555
+ const events = allEvents.filter(
14556
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
14557
+ ).slice(0, input.max_snapshots || 1);
14558
+ if (!events || events.length === 0) {
14559
+ return {
14560
+ content: [
14561
+ {
14562
+ type: "text",
14563
+ text: formatContent({
14564
+ restored: false,
14565
+ message: "No session snapshots found. This may be a new session or snapshots have not been captured.",
14566
+ hint: "Use session_capture_smart to save session state before compaction."
14567
+ })
14568
+ }
14569
+ ]
14570
+ };
14571
+ }
14572
+ const latestEvent = events[0];
14573
+ let snapshotData;
14574
+ try {
14575
+ snapshotData = JSON.parse(latestEvent.content);
14576
+ } catch {
14577
+ snapshotData = { conversation_summary: latestEvent.content };
14578
+ }
14579
+ const response = {
14580
+ restored: true,
14581
+ snapshot_id: latestEvent.id,
14582
+ captured_at: snapshotData.captured_at || latestEvent.created_at,
14583
+ ...snapshotData,
14584
+ hint: "Context restored. Continue the conversation with awareness of the above state."
14585
+ };
14586
+ return {
14587
+ content: [{ type: "text", text: formatContent(response) }],
14588
+ structuredContent: toStructured(response)
14589
+ };
14590
+ } catch (error) {
14591
+ const message = error instanceof Error ? error.message : String(error);
14592
+ return errorResult(`Failed to restore context: ${message}`);
14593
+ }
14594
+ }
14595
+ );
14056
14596
  registerTool(
14057
14597
  "session_capture_lesson",
14058
14598
  {
@@ -14411,6 +14951,7 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
14411
14951
  overwrite_existing: external_exports.boolean().optional().describe("Allow overwriting existing rule files (ContextStream block only)"),
14412
14952
  apply_global: external_exports.boolean().optional().describe("Also write global rule files for supported editors"),
14413
14953
  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."),
14954
+ include_pre_compact: external_exports.boolean().optional().describe("Include PreCompact hook for automatic state saving before context compaction. Defaults to false."),
14414
14955
  dry_run: external_exports.boolean().optional().describe("If true, return content without writing files")
14415
14956
  })
14416
14957
  },
@@ -14491,8 +15032,14 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
14491
15032
  { file: "~/.claude/hooks/contextstream-reminder.py", status: "dry run - would create" },
14492
15033
  { file: "~/.claude/settings.json", status: "dry run - would update" }
14493
15034
  ];
15035
+ if (input.include_pre_compact) {
15036
+ hooksResults.push({ file: "~/.claude/hooks/contextstream-precompact.py", status: "dry run - would create" });
15037
+ }
14494
15038
  } else {
14495
- const hookResult = await installClaudeCodeHooks({ scope: "user" });
15039
+ const hookResult = await installClaudeCodeHooks({
15040
+ scope: "user",
15041
+ includePreCompact: input.include_pre_compact
15042
+ });
14496
15043
  hooksResults = [
14497
15044
  ...hookResult.scripts.map((f) => ({ file: f, status: "created" })),
14498
15045
  ...hookResult.settings.map((f) => ({ file: f, status: "updated" }))
@@ -14851,10 +15398,13 @@ This saves ~80% tokens compared to including full chat history.`,
14851
15398
  max_tokens: external_exports.number().optional().describe("Maximum tokens for context (default: 800)"),
14852
15399
  format: external_exports.enum(["minified", "readable", "structured"]).optional().describe("Context format (default: minified)"),
14853
15400
  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)")
15401
+ distill: external_exports.boolean().optional().describe("Use distillation for context pack (default: true)"),
15402
+ session_tokens: external_exports.number().optional().describe("Cumulative session token count for context pressure calculation"),
15403
+ context_threshold: external_exports.number().optional().describe("Custom context window threshold (defaults to 70k)")
14855
15404
  })
14856
15405
  },
14857
15406
  async (input) => {
15407
+ const startTime = Date.now();
14858
15408
  if (sessionManager) {
14859
15409
  sessionManager.markContextSmartCalled();
14860
15410
  }
@@ -14867,6 +15417,17 @@ This saves ~80% tokens compared to including full chat history.`,
14867
15417
  projectId = projectId || ctx.project_id;
14868
15418
  }
14869
15419
  }
15420
+ let sessionTokens = input.session_tokens;
15421
+ let contextThreshold = input.context_threshold;
15422
+ if (sessionManager) {
15423
+ if (sessionTokens === void 0) {
15424
+ sessionTokens = sessionManager.getSessionTokens();
15425
+ }
15426
+ if (contextThreshold === void 0) {
15427
+ contextThreshold = sessionManager.getContextThreshold();
15428
+ }
15429
+ sessionManager.addTokens(input.user_message);
15430
+ }
14870
15431
  const result = await client.getSmartContext({
14871
15432
  user_message: input.user_message,
14872
15433
  workspace_id: workspaceId,
@@ -14874,11 +15435,18 @@ This saves ~80% tokens compared to including full chat history.`,
14874
15435
  max_tokens: input.max_tokens,
14875
15436
  format: input.format,
14876
15437
  mode: input.mode,
14877
- distill: input.distill
15438
+ distill: input.distill,
15439
+ session_tokens: sessionTokens,
15440
+ context_threshold: contextThreshold
14878
15441
  });
15442
+ if (sessionManager && result.token_estimate) {
15443
+ sessionManager.addTokens(result.token_estimate);
15444
+ }
15445
+ const roundTripMs = Date.now() - startTime;
15446
+ const timingStr = SHOW_TIMING ? ` | ${roundTripMs}ms` : "";
14879
15447
  const footer = `
14880
15448
  ---
14881
- \u{1F3AF} ${result.sources_used} sources | ~${result.token_estimate} tokens | format: ${result.format}`;
15449
+ \u{1F3AF} ${result.sources_used} sources | ~${result.token_estimate} tokens | format: ${result.format}${timingStr}`;
14882
15450
  const folderPathForRules = resolveFolderPath(void 0, sessionManager);
14883
15451
  const rulesNotice = getRulesNotice(folderPathForRules, detectedClientInfo?.name);
14884
15452
  let versionNotice = result.version_notice;
@@ -14905,6 +15473,22 @@ This saves ~80% tokens compared to including full chat history.`,
14905
15473
  const searchRulesLine = SEARCH_RULES_REMINDER_ENABLED ? `
14906
15474
 
14907
15475
  ${SEARCH_RULES_REMINDER}` : "";
15476
+ let contextPressureWarning = "";
15477
+ if (result.context_pressure) {
15478
+ const cp = result.context_pressure;
15479
+ if (cp.level === "critical") {
15480
+ contextPressureWarning = `
15481
+
15482
+ \u{1F6A8} [CONTEXT PRESSURE: CRITICAL] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
15483
+ Action: ${cp.suggested_action === "save_now" ? 'SAVE STATE NOW - Call session(action="capture") to preserve conversation state before compaction.' : cp.suggested_action}
15484
+ The conversation may compact soon. Save important decisions, insights, and progress immediately.`;
15485
+ } else if (cp.level === "high") {
15486
+ contextPressureWarning = `
15487
+
15488
+ \u26A0\uFE0F [CONTEXT PRESSURE: HIGH] ${cp.usage_percent}% of context used (${cp.session_tokens}/${cp.threshold} tokens)
15489
+ Action: ${cp.suggested_action === "prepare_save" ? "Consider saving important decisions and conversation state soon." : cp.suggested_action}`;
15490
+ }
15491
+ }
14908
15492
  const allWarnings = [
14909
15493
  lessonsWarningLine,
14910
15494
  rulesWarningLine ? `
@@ -14913,6 +15497,7 @@ ${rulesWarningLine}` : "",
14913
15497
  versionWarningLine ? `
14914
15498
 
14915
15499
  ${versionWarningLine}` : "",
15500
+ contextPressureWarning,
14916
15501
  searchRulesLine
14917
15502
  ].filter(Boolean).join("");
14918
15503
  return {
@@ -15943,6 +16528,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15943
16528
  },
15944
16529
  async (input) => {
15945
16530
  const params = normalizeSearchParams(input);
16531
+ const startTime = Date.now();
15946
16532
  let result;
15947
16533
  let toolType;
15948
16534
  switch (input.mode) {
@@ -15973,7 +16559,9 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15973
16559
  default:
15974
16560
  toolType = "search_hybrid";
15975
16561
  }
15976
- const outputText = formatContent(result);
16562
+ const roundTripMs = Date.now() - startTime;
16563
+ const timingSummary = formatTimingSummary(roundTripMs, getResultCount(result));
16564
+ const outputText = timingSummary + formatContent(result);
15977
16565
  trackToolTokenSavings(client, toolType, outputText, {
15978
16566
  workspace_id: params.workspace_id,
15979
16567
  project_id: params.project_id
@@ -15988,7 +16576,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15988
16576
  "session",
15989
16577
  {
15990
16578
  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).`,
16579
+ 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
16580
  inputSchema: external_exports.object({
15993
16581
  action: external_exports.enum([
15994
16582
  "capture",
@@ -16006,7 +16594,9 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16006
16594
  "capture_plan",
16007
16595
  "get_plan",
16008
16596
  "update_plan",
16009
- "list_plans"
16597
+ "list_plans",
16598
+ // Context restore
16599
+ "restore_context"
16010
16600
  ]).describe("Action to perform"),
16011
16601
  workspace_id: external_exports.string().uuid().optional(),
16012
16602
  project_id: external_exports.string().uuid().optional(),
@@ -16028,7 +16618,8 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16028
16618
  "lesson",
16029
16619
  "warning",
16030
16620
  "frustration",
16031
- "conversation"
16621
+ "conversation",
16622
+ "session_snapshot"
16032
16623
  ]).optional().describe("Event type for capture"),
16033
16624
  importance: external_exports.enum(["low", "medium", "high", "critical"]).optional(),
16034
16625
  tags: external_exports.array(external_exports.string()).optional(),
@@ -16078,7 +16669,10 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16078
16669
  status: external_exports.enum(["draft", "active", "completed", "archived", "abandoned"]).optional().describe("Plan status"),
16079
16670
  due_at: external_exports.string().optional().describe("Due date for plan (ISO timestamp)"),
16080
16671
  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")
16672
+ include_tasks: external_exports.boolean().optional().describe("Include tasks when getting plan"),
16673
+ // Restore context params
16674
+ snapshot_id: external_exports.string().uuid().optional().describe("Specific snapshot ID to restore (defaults to most recent)"),
16675
+ max_snapshots: external_exports.number().optional().default(1).describe("Number of recent snapshots to consider (default: 1)")
16082
16676
  })
16083
16677
  },
16084
16678
  async (input) => {
@@ -16384,6 +16978,87 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16384
16978
  structuredContent: toStructured(result)
16385
16979
  };
16386
16980
  }
16981
+ case "restore_context": {
16982
+ if (!workspaceId) {
16983
+ return errorResult(
16984
+ "restore_context requires workspace_id. Call session_init first."
16985
+ );
16986
+ }
16987
+ if (input.snapshot_id) {
16988
+ const eventResult = await client.getEvent(input.snapshot_id);
16989
+ const event = eventResult?.data || eventResult;
16990
+ if (!event || !event.content) {
16991
+ return errorResult(
16992
+ `Snapshot not found: ${input.snapshot_id}. The snapshot may have been deleted or does not exist.`
16993
+ );
16994
+ }
16995
+ let snapshotData;
16996
+ try {
16997
+ snapshotData = JSON.parse(event.content);
16998
+ } catch {
16999
+ snapshotData = { conversation_summary: event.content };
17000
+ }
17001
+ const response2 = {
17002
+ restored: true,
17003
+ snapshot_id: event.id,
17004
+ captured_at: snapshotData.captured_at || event.created_at,
17005
+ ...snapshotData,
17006
+ hint: "Context restored. Continue the conversation with awareness of the above state."
17007
+ };
17008
+ return {
17009
+ content: [{ type: "text", text: formatContent(response2) }],
17010
+ structuredContent: toStructured(response2)
17011
+ };
17012
+ }
17013
+ const listResult = await client.listMemoryEvents({
17014
+ workspace_id: workspaceId,
17015
+ project_id: projectId,
17016
+ limit: 50
17017
+ // Fetch more to filter
17018
+ });
17019
+ const allEvents = listResult?.data?.items || listResult?.items || listResult?.data || [];
17020
+ const snapshotEvents = allEvents.filter(
17021
+ (e) => e.event_type === "session_snapshot" || e.metadata?.original_type === "session_snapshot" || e.metadata?.tags?.includes("session_snapshot") || e.tags?.includes("session_snapshot")
17022
+ ).slice(0, input.max_snapshots || 1);
17023
+ if (!snapshotEvents || snapshotEvents.length === 0) {
17024
+ return {
17025
+ content: [
17026
+ {
17027
+ type: "text",
17028
+ text: formatContent({
17029
+ restored: false,
17030
+ message: "No session snapshots found. Use session_capture_smart to save state before compaction.",
17031
+ hint: "Start fresh or use session_init to get recent context."
17032
+ })
17033
+ }
17034
+ ]
17035
+ };
17036
+ }
17037
+ const snapshots = snapshotEvents.map((event) => {
17038
+ let snapshotData;
17039
+ try {
17040
+ snapshotData = JSON.parse(event.content || "{}");
17041
+ } catch {
17042
+ snapshotData = { conversation_summary: event.content };
17043
+ }
17044
+ return {
17045
+ snapshot_id: event.id,
17046
+ captured_at: snapshotData.captured_at || event.created_at,
17047
+ ...snapshotData
17048
+ };
17049
+ });
17050
+ const response = {
17051
+ restored: true,
17052
+ snapshots_found: snapshots.length,
17053
+ latest: snapshots[0],
17054
+ all_snapshots: snapshots.length > 1 ? snapshots : void 0,
17055
+ hint: "Context restored. Continue the conversation with awareness of the above state."
17056
+ };
17057
+ return {
17058
+ content: [{ type: "text", text: formatContent(response) }],
17059
+ structuredContent: toStructured(response)
17060
+ };
17061
+ }
16387
17062
  default:
16388
17063
  return errorResult(`Unknown action: ${input.action}`);
16389
17064
  }
@@ -18949,6 +19624,7 @@ function registerPrompts(server) {
18949
19624
 
18950
19625
  // src/session-manager.ts
18951
19626
  var SessionManager = class {
19627
+ // Conservative default for 100k context window
18952
19628
  constructor(server, client) {
18953
19629
  this.server = server;
18954
19630
  this.client = client;
@@ -18959,6 +19635,9 @@ var SessionManager = class {
18959
19635
  this.folderPath = null;
18960
19636
  this.contextSmartCalled = false;
18961
19637
  this.warningShown = false;
19638
+ // Token tracking for context pressure calculation
19639
+ this.sessionTokens = 0;
19640
+ this.contextThreshold = 7e4;
18962
19641
  }
18963
19642
  /**
18964
19643
  * Check if session has been auto-initialized
@@ -19006,6 +19685,51 @@ var SessionManager = class {
19006
19685
  markContextSmartCalled() {
19007
19686
  this.contextSmartCalled = true;
19008
19687
  }
19688
+ /**
19689
+ * Get current session token count for context pressure calculation.
19690
+ */
19691
+ getSessionTokens() {
19692
+ return this.sessionTokens;
19693
+ }
19694
+ /**
19695
+ * Get the context threshold (max tokens before compaction warning).
19696
+ */
19697
+ getContextThreshold() {
19698
+ return this.contextThreshold;
19699
+ }
19700
+ /**
19701
+ * Set a custom context threshold (useful if client provides model info).
19702
+ */
19703
+ setContextThreshold(threshold) {
19704
+ this.contextThreshold = threshold;
19705
+ }
19706
+ /**
19707
+ * Add tokens to the session count.
19708
+ * Call this after each tool response to track token accumulation.
19709
+ *
19710
+ * @param tokens - Exact token count or text to estimate
19711
+ */
19712
+ addTokens(tokens) {
19713
+ if (typeof tokens === "number") {
19714
+ this.sessionTokens += tokens;
19715
+ } else {
19716
+ this.sessionTokens += Math.ceil(tokens.length / 4);
19717
+ }
19718
+ }
19719
+ /**
19720
+ * Estimate tokens from a tool response.
19721
+ * Uses a simple heuristic: ~4 characters per token.
19722
+ */
19723
+ estimateTokens(content) {
19724
+ const text = typeof content === "string" ? content : JSON.stringify(content);
19725
+ return Math.ceil(text.length / 4);
19726
+ }
19727
+ /**
19728
+ * Reset token count (e.g., after compaction or new session).
19729
+ */
19730
+ resetTokenCount() {
19731
+ this.sessionTokens = 0;
19732
+ }
19009
19733
  /**
19010
19734
  * Check if context_smart has been called and warn if not.
19011
19735
  * Returns true if a warning was shown, false otherwise.
@@ -20025,6 +20749,9 @@ function buildContextStreamMcpServer(params) {
20025
20749
  env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
20026
20750
  }
20027
20751
  env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
20752
+ if (params.showTiming) {
20753
+ env.CONTEXTSTREAM_SHOW_TIMING = "true";
20754
+ }
20028
20755
  if (IS_WINDOWS) {
20029
20756
  return {
20030
20757
  command: "cmd",
@@ -20047,6 +20774,9 @@ function buildContextStreamVsCodeServer(params) {
20047
20774
  env.CONTEXTSTREAM_PROGRESSIVE_MODE = "true";
20048
20775
  }
20049
20776
  env.CONTEXTSTREAM_CONTEXT_PACK = params.contextPackEnabled === false ? "false" : "true";
20777
+ if (params.showTiming) {
20778
+ env.CONTEXTSTREAM_SHOW_TIMING = "true";
20779
+ }
20050
20780
  if (IS_WINDOWS) {
20051
20781
  return {
20052
20782
  type: "stdio",
@@ -20147,6 +20877,8 @@ async function upsertCodexTomlConfig(filePath, params) {
20147
20877
  ` : "";
20148
20878
  const contextPackLine = `CONTEXTSTREAM_CONTEXT_PACK = "${params.contextPackEnabled === false ? "false" : "true"}"
20149
20879
  `;
20880
+ const showTimingLine = params.showTiming ? `CONTEXTSTREAM_SHOW_TIMING = "true"
20881
+ ` : "";
20150
20882
  const commandLine = IS_WINDOWS ? `command = "cmd"
20151
20883
  args = ["/c", "npx", "-y", "@contextstream/mcp-server"]
20152
20884
  ` : `command = "npx"
@@ -20160,7 +20892,7 @@ args = ["-y", "@contextstream/mcp-server"]
20160
20892
  [mcp_servers.contextstream.env]
20161
20893
  CONTEXTSTREAM_API_URL = "${params.apiUrl}"
20162
20894
  CONTEXTSTREAM_API_KEY = "${params.apiKey}"
20163
- ` + toolsetLine + contextPackLine;
20895
+ ` + toolsetLine + contextPackLine + showTimingLine;
20164
20896
  if (!exists) {
20165
20897
  await fs7.writeFile(filePath, block.trimStart(), "utf8");
20166
20898
  return "created";
@@ -20517,6 +21249,11 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
20517
21249
  console.log(" Uses more operations/credits; can be disabled in settings or via env.");
20518
21250
  const contextPackChoice = normalizeInput(await rl.question("Enable Context Pack? [Y/n]: "));
20519
21251
  const contextPackEnabled = !(contextPackChoice.toLowerCase() === "n" || contextPackChoice.toLowerCase() === "no");
21252
+ console.log("\nResponse Timing:");
21253
+ console.log(" Show response time for tool calls (e.g., '\u2713 3 results in 142ms').");
21254
+ console.log(" Useful for debugging performance; disabled by default.");
21255
+ const showTimingChoice = normalizeInput(await rl.question("Show response timing? [y/N]: "));
21256
+ const showTiming = showTimingChoice.toLowerCase() === "y" || showTimingChoice.toLowerCase() === "yes";
20520
21257
  const editors = [
20521
21258
  "codex",
20522
21259
  "claude",
@@ -20591,18 +21328,20 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
20591
21328
  )
20592
21329
  ) || mcpChoiceDefault;
20593
21330
  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 });
21331
+ const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, toolset, contextPackEnabled, showTiming });
20595
21332
  const mcpServerClaude = buildContextStreamMcpServer({
20596
21333
  apiUrl,
20597
21334
  apiKey,
20598
21335
  toolset,
20599
- contextPackEnabled
21336
+ contextPackEnabled,
21337
+ showTiming
20600
21338
  });
20601
21339
  const vsCodeServer = buildContextStreamVsCodeServer({
20602
21340
  apiUrl,
20603
21341
  apiKey,
20604
21342
  toolset,
20605
- contextPackEnabled
21343
+ contextPackEnabled,
21344
+ showTiming
20606
21345
  });
20607
21346
  const needsGlobalMcpConfig = mcpScope === "global" || mcpScope === "both" || mcpScope === "project" && hasCodex;
20608
21347
  if (needsGlobalMcpConfig) {
@@ -20621,7 +21360,8 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
20621
21360
  apiUrl,
20622
21361
  apiKey,
20623
21362
  toolset,
20624
- contextPackEnabled
21363
+ contextPackEnabled,
21364
+ showTiming
20625
21365
  });
20626
21366
  writeActions.push({ kind: "mcp-config", target: filePath, status });
20627
21367
  console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
@@ -20936,7 +21676,12 @@ Applying to ${projects.length} project(s)...`);
20936
21676
  folder_path: projectPath,
20937
21677
  workspace_id: workspaceId,
20938
21678
  workspace_name: workspaceName,
20939
- create_parent_mapping: createParentMapping
21679
+ create_parent_mapping: createParentMapping,
21680
+ // Include version and config info for desktop app compatibility
21681
+ version: VERSION,
21682
+ configured_editors: configuredEditors,
21683
+ context_pack: contextPackEnabled,
21684
+ api_url: apiUrl
20940
21685
  });
20941
21686
  writeActions.push({
20942
21687
  kind: "workspace-config",
@@ -21053,6 +21798,7 @@ Applying to ${projects.length} project(s)...`);
21053
21798
  console.log(`Toolset: ${toolset} (${toolsetDesc})`);
21054
21799
  console.log(`Token reduction: ~75% compared to previous versions.`);
21055
21800
  console.log(`Context Pack: ${contextPackEnabled ? "enabled" : "disabled"}`);
21801
+ console.log(`Response Timing: ${showTiming ? "enabled" : "disabled"}`);
21056
21802
  }
21057
21803
  console.log("\nNext steps:");
21058
21804
  console.log("- Restart your editor/CLI after changing MCP config or rules.");
@@ -21068,6 +21814,9 @@ Applying to ${projects.length} project(s)...`);
21068
21814
  console.log(
21069
21815
  "- Toggle Context Pack with CONTEXTSTREAM_CONTEXT_PACK=true|false (and in dashboard settings)."
21070
21816
  );
21817
+ console.log(
21818
+ "- Toggle Response Timing with CONTEXTSTREAM_SHOW_TIMING=true|false."
21819
+ );
21071
21820
  console.log("");
21072
21821
  console.log("You're set up! Now try these prompts in your AI tool:");
21073
21822
  console.log(' 1) "session summary"');
@@ -4082,7 +4082,8 @@ var configSchema = external_exports.object({
4082
4082
  defaultProjectId: external_exports.string().uuid().optional(),
4083
4083
  userAgent: external_exports.string().default(`contextstream-mcp/${VERSION}`),
4084
4084
  allowHeaderAuth: external_exports.boolean().optional(),
4085
- contextPackEnabled: external_exports.boolean().default(true)
4085
+ contextPackEnabled: external_exports.boolean().default(true),
4086
+ showTiming: external_exports.boolean().default(false)
4086
4087
  });
4087
4088
  var MISSING_CREDENTIALS_ERROR = "Set CONTEXTSTREAM_API_KEY or CONTEXTSTREAM_JWT for authentication (or CONTEXTSTREAM_ALLOW_HEADER_AUTH=true for header-based auth).";
4088
4089
  function loadConfig() {
@@ -4090,6 +4091,7 @@ function loadConfig() {
4090
4091
  const contextPackEnabled = parseBooleanEnv(
4091
4092
  process.env.CONTEXTSTREAM_CONTEXT_PACK ?? process.env.CONTEXTSTREAM_CONTEXT_PACK_ENABLED
4092
4093
  );
4094
+ const showTiming = parseBooleanEnv(process.env.CONTEXTSTREAM_SHOW_TIMING);
4093
4095
  const parsed = configSchema.safeParse({
4094
4096
  apiUrl: process.env.CONTEXTSTREAM_API_URL,
4095
4097
  apiKey: process.env.CONTEXTSTREAM_API_KEY,
@@ -4098,7 +4100,8 @@ function loadConfig() {
4098
4100
  defaultProjectId: process.env.CONTEXTSTREAM_PROJECT_ID,
4099
4101
  userAgent: process.env.CONTEXTSTREAM_USER_AGENT,
4100
4102
  allowHeaderAuth,
4101
- contextPackEnabled
4103
+ contextPackEnabled,
4104
+ showTiming
4102
4105
  });
4103
4106
  if (!parsed.success) {
4104
4107
  const missing = parsed.error.errors.map((e) => e.path.join(".")).join(", ");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@contextstream/mcp-server",
3
3
  "mcpName": "io.github.contextstreamio/mcp-server",
4
- "version": "0.4.37",
4
+ "version": "0.4.38",
5
5
  "description": "ContextStream MCP server - v0.4.x with consolidated domain tools (~11 tools, ~75% token reduction). Code context, memory, search, and AI tools.",
6
6
  "type": "module",
7
7
  "license": "MIT",