@contextstream/mcp-server 0.4.17 → 0.4.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +382 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7504,7 +7504,7 @@ var CONTEXTSTREAM_TOOL_NAMES = [
7504
7504
  ];
7505
7505
  function applyMcpToolPrefix(markdown, toolPrefix) {
7506
7506
  const toolPattern = CONTEXTSTREAM_TOOL_NAMES.join("|");
7507
- const toolRegex = new RegExp(`(?<!__)\\b(${toolPattern})\\b`, "g");
7507
+ const toolRegex = new RegExp(`(?<!__)\\b(${toolPattern})\\b(?=\\s*\\()`, "g");
7508
7508
  return markdown.replace(toolRegex, `${toolPrefix}$1`);
7509
7509
  }
7510
7510
  var CONTEXTSTREAM_RULES_FULL = `
@@ -7527,7 +7527,9 @@ Rules Version: ${RULES_VERSION}
7527
7527
 
7528
7528
  **NO EXCEPTIONS.** Do not skip even if you think you have enough context.
7529
7529
 
7530
- **Context Pack (Pro+):** If enabled, use \`context_smart(..., mode="pack", distill=true)\` for code/file queries. If unavailable, omit \`mode\` and use standard \`context_smart\`.
7530
+ **Context Pack (Pro+):** If enabled, use \`context_smart(..., mode="pack", distill=true)\` for code/file queries. If unavailable or disabled, omit \`mode\` and proceed with standard \`context_smart\` (the API will fall back).
7531
+
7532
+ **Tool naming:** Use the exact tool names exposed by your MCP client. Claude Code typically uses \`mcp__<server>__<tool>\` where \`<server>\` matches your MCP config (often \`contextstream\`). If a tool call fails with "No such tool available", refresh rules and match the tool list.
7531
7533
 
7532
7534
  ---
7533
7535
 
@@ -7619,9 +7621,9 @@ Only after this preflight, proceed with search/analysis below.
7619
7621
  2. \`search(mode="hybrid", query="...")\` or \`search(mode="keyword", query="<filename>")\`
7620
7622
  3. \`project(action="files")\` - file tree/list (only when needed)
7621
7623
  4. \`graph(action="dependencies", ...)\` - code structure
7622
- 5. Local repo scans (rg/ls/find) - only if ContextStream returns no results or is unavailable
7624
+ 5. Local repo scans (rg/ls/find) - only if ContextStream returns no results, errors, or the user explicitly asks
7623
7625
 
7624
- Use ContextStream results directly; only open files if you need exact code.
7626
+ If ContextStream returns results, stop and use them. Do NOT use local Search/Explore/Read unless you need exact code edits.
7625
7627
 
7626
7628
  **Code Analysis:**
7627
7629
  - Dependencies: \`graph(action="dependencies", file_path="...")\`
@@ -7713,7 +7715,9 @@ Rules Version: ${RULES_VERSION}
7713
7715
  | **Before risky work** | \`session(action="get_lessons", query="<topic>")\` |
7714
7716
  | **On user frustration** | \`session(action="capture_lesson", title="...", trigger="...", impact="...", prevention="...")\` |
7715
7717
 
7716
- **Context Pack (Pro+):** If enabled, use \`context_smart(..., mode="pack", distill=true)\` for code/file queries. If unavailable, omit \`mode\`.
7718
+ **Context Pack (Pro+):** If enabled, use \`context_smart(..., mode="pack", distill=true)\` for code/file queries. If unavailable or disabled, omit \`mode\` and proceed with standard \`context_smart\` (the API will fall back).
7719
+
7720
+ **Tool naming:** Use the exact tool names exposed by your MCP client. Claude Code typically uses \`mcp__<server>__<tool>\` where \`<server>\` matches your MCP config (often \`contextstream\`). If a tool call fails with "No such tool available", refresh rules and match the tool list.
7717
7721
 
7718
7722
  ### Quick Reference: Domain Tools
7719
7723
 
@@ -7735,6 +7739,7 @@ Rules Version: ${RULES_VERSION}
7735
7739
  - **Before searching files/code**: Check \`project(action="index_status")\`; if missing/stale run \`project(action="ingest_local", path="<cwd>")\` or \`project(action="index")\`, and use \`graph(action="ingest")\` if needed
7736
7740
  - **For discovery**: Use \`session(action="smart_search")\` or \`search(mode="hybrid")\` before any local repo scans
7737
7741
  - **For file/function/config lookups**: Use \`search\`/\`graph\` first; only fall back to rg/ls/find if ContextStream returns no results
7742
+ - **If ContextStream returns results**: Do NOT use local Search/Explore/Read; only open specific files when needed for exact edits
7738
7743
  - **For code analysis**: Use \`graph(action="dependencies")\` or \`graph(action="impact")\` for call/dependency analysis
7739
7744
  - **On [RULES_NOTICE]**: Use \`generate_editor_rules(folder_path="<cwd>")\` to update rules
7740
7745
  - **After completing work**: Always capture decisions/insights with \`session(action="capture")\`
@@ -8050,6 +8055,7 @@ var DEFAULT_PARAM_DESCRIPTIONS = {
8050
8055
  recurrence: "Recurrence pattern (daily, weekly, monthly).",
8051
8056
  keywords: "Keywords for matching.",
8052
8057
  overwrite: "Allow overwriting existing files on disk.",
8058
+ overwrite_existing: "Allow overwriting existing rule files (ContextStream block only).",
8053
8059
  write_to_disk: "Write ingested files to disk before indexing.",
8054
8060
  await_indexing: "Wait for indexing to finish before returning.",
8055
8061
  auto_index: "Automatically index on creation.",
@@ -8227,11 +8233,159 @@ function getRulesNotice(folderPath, clientName) {
8227
8233
  }
8228
8234
  var CONTEXTSTREAM_START_MARKER = "<!-- BEGIN ContextStream -->";
8229
8235
  var CONTEXTSTREAM_END_MARKER = "<!-- END ContextStream -->";
8236
+ var LEGACY_CONTEXTSTREAM_HINTS = [
8237
+ "contextstream integration",
8238
+ "contextstream v0.4",
8239
+ "contextstream v0.3",
8240
+ "contextstream (standard)",
8241
+ "contextstream (consolidated",
8242
+ "contextstream mcp",
8243
+ "contextstream tools"
8244
+ ];
8245
+ var LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS = [
8246
+ "contextstream",
8247
+ "tl;dr",
8248
+ "required every message",
8249
+ "quick reference",
8250
+ "tool catalog",
8251
+ "consolidated domain tools",
8252
+ "standalone tools",
8253
+ "domain tools",
8254
+ "why context_smart",
8255
+ "recommended token budgets",
8256
+ "rules update notices",
8257
+ "preferences & lessons",
8258
+ "index & graph preflight",
8259
+ "search & code intelligence",
8260
+ "distillation",
8261
+ "when to capture",
8262
+ "behavior rules",
8263
+ "plans & tasks",
8264
+ "complete action reference"
8265
+ ];
8266
+ var CONTEXTSTREAM_PREAMBLE_PATTERNS = [
8267
+ /^#\s+workspace:/i,
8268
+ /^#\s+project:/i,
8269
+ /^#\s+workspace id:/i,
8270
+ /^#\s+codex cli instructions$/i,
8271
+ /^#\s+claude code instructions$/i,
8272
+ /^#\s+cursor rules$/i,
8273
+ /^#\s+windsurf rules$/i,
8274
+ /^#\s+cline rules$/i,
8275
+ /^#\s+kilo code rules$/i,
8276
+ /^#\s+roo code rules$/i,
8277
+ /^#\s+aider configuration$/i
8278
+ ];
8230
8279
  function wrapWithMarkers(content) {
8231
8280
  return `${CONTEXTSTREAM_START_MARKER}
8232
8281
  ${content.trim()}
8233
8282
  ${CONTEXTSTREAM_END_MARKER}`;
8234
8283
  }
8284
+ function isLegacyContextStreamRules(content) {
8285
+ const lower = content.toLowerCase();
8286
+ if (!lower.includes("contextstream")) return false;
8287
+ if (!LEGACY_CONTEXTSTREAM_HINTS.some((hint) => lower.includes(hint))) return false;
8288
+ const headingRegex = /^#{1,6}\s+(.+)$/gm;
8289
+ let hasHeading = false;
8290
+ let match;
8291
+ while ((match = headingRegex.exec(content)) !== null) {
8292
+ hasHeading = true;
8293
+ const heading = match[1].trim().toLowerCase();
8294
+ const allowed = LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS.some((prefix) => heading.startsWith(prefix));
8295
+ if (!allowed) return false;
8296
+ }
8297
+ return hasHeading;
8298
+ }
8299
+ function isContextStreamPreamble(line) {
8300
+ const trimmed = line.trim();
8301
+ if (!trimmed) return false;
8302
+ return CONTEXTSTREAM_PREAMBLE_PATTERNS.some((pattern) => pattern.test(trimmed));
8303
+ }
8304
+ function findContextStreamHeading(lines) {
8305
+ const headingRegex = /^(#{1,6})\s+(.+)$/;
8306
+ for (let i = 0; i < lines.length; i += 1) {
8307
+ const match = headingRegex.exec(lines[i]);
8308
+ if (!match) continue;
8309
+ if (match[2].toLowerCase().includes("contextstream")) {
8310
+ return { index: i, level: match[1].length };
8311
+ }
8312
+ }
8313
+ return null;
8314
+ }
8315
+ function findSectionEnd(lines, startLine, level) {
8316
+ const headingRegex = /^(#{1,6})\s+(.+)$/;
8317
+ for (let i = startLine + 1; i < lines.length; i += 1) {
8318
+ const match = headingRegex.exec(lines[i]);
8319
+ if (!match) continue;
8320
+ if (match[1].length <= level) return i;
8321
+ }
8322
+ return lines.length;
8323
+ }
8324
+ function extractContextStreamBlock(content) {
8325
+ const lines = content.split(/\r?\n/);
8326
+ const heading = findContextStreamHeading(lines);
8327
+ if (!heading) return content.trim();
8328
+ const endLine = findSectionEnd(lines, heading.index, heading.level);
8329
+ return lines.slice(heading.index, endLine).join("\n").trim();
8330
+ }
8331
+ function findLegacyContextStreamSection(content) {
8332
+ const lines = content.split(/\r?\n/);
8333
+ const heading = findContextStreamHeading(lines);
8334
+ if (!heading) return null;
8335
+ let startLine = heading.index;
8336
+ for (let i = heading.index - 1; i >= 0; i -= 1) {
8337
+ const line = lines[i];
8338
+ if (!line.trim()) {
8339
+ startLine = i;
8340
+ continue;
8341
+ }
8342
+ if (isContextStreamPreamble(line)) {
8343
+ startLine = i;
8344
+ continue;
8345
+ }
8346
+ break;
8347
+ }
8348
+ const endLine = findSectionEnd(lines, heading.index, heading.level);
8349
+ return { startLine, endLine, contextLine: heading.index };
8350
+ }
8351
+ function blockHasPreamble(block) {
8352
+ const lines = block.split(/\r?\n/);
8353
+ for (const line of lines) {
8354
+ const trimmed = line.trim();
8355
+ if (!trimmed) continue;
8356
+ if (isContextStreamPreamble(trimmed)) return true;
8357
+ if (/^#{1,6}\s+/.test(trimmed)) return false;
8358
+ }
8359
+ return false;
8360
+ }
8361
+ function replaceContextStreamBlock(existing, content) {
8362
+ const fullWrapped = wrapWithMarkers(content);
8363
+ const blockWrapped = wrapWithMarkers(extractContextStreamBlock(content));
8364
+ const startIdx = existing.indexOf(CONTEXTSTREAM_START_MARKER);
8365
+ const endIdx = existing.indexOf(CONTEXTSTREAM_END_MARKER);
8366
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
8367
+ const existingBlock = existing.slice(startIdx + CONTEXTSTREAM_START_MARKER.length, endIdx);
8368
+ const replacement = blockHasPreamble(existingBlock) ? fullWrapped : blockWrapped;
8369
+ const before = existing.substring(0, startIdx).trimEnd();
8370
+ const after = existing.substring(endIdx + CONTEXTSTREAM_END_MARKER.length).trimStart();
8371
+ const merged = [before, replacement, after].filter((part) => part.length > 0).join("\n\n");
8372
+ return { content: merged.trim() + "\n", status: "updated" };
8373
+ }
8374
+ const legacy = findLegacyContextStreamSection(existing);
8375
+ if (legacy) {
8376
+ const lines = existing.split(/\r?\n/);
8377
+ const before = lines.slice(0, legacy.startLine).join("\n").trimEnd();
8378
+ const after = lines.slice(legacy.endLine).join("\n").trimStart();
8379
+ const replacement = legacy.startLine < legacy.contextLine ? fullWrapped : blockWrapped;
8380
+ const merged = [before, replacement, after].filter((part) => part.length > 0).join("\n\n");
8381
+ return { content: merged.trim() + "\n", status: "updated" };
8382
+ }
8383
+ if (isLegacyContextStreamRules(existing)) {
8384
+ return { content: fullWrapped + "\n", status: "updated" };
8385
+ }
8386
+ const appended = existing.trimEnd() + "\n\n" + blockWrapped + "\n";
8387
+ return { content: appended, status: "appended" };
8388
+ }
8235
8389
  async function upsertRuleFile(filePath, content) {
8236
8390
  await fs3.promises.mkdir(path4.dirname(filePath), { recursive: true });
8237
8391
  const wrappedContent = wrapWithMarkers(content);
@@ -8244,18 +8398,13 @@ async function upsertRuleFile(filePath, content) {
8244
8398
  await fs3.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
8245
8399
  return "created";
8246
8400
  }
8247
- const startIdx = existing.indexOf(CONTEXTSTREAM_START_MARKER);
8248
- const endIdx = existing.indexOf(CONTEXTSTREAM_END_MARKER);
8249
- if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
8250
- const before = existing.substring(0, startIdx);
8251
- const after = existing.substring(endIdx + CONTEXTSTREAM_END_MARKER.length);
8252
- const updated = before.trimEnd() + "\n\n" + wrappedContent + "\n" + after.trimStart();
8253
- await fs3.promises.writeFile(filePath, updated.trim() + "\n", "utf8");
8401
+ if (!existing.trim()) {
8402
+ await fs3.promises.writeFile(filePath, wrappedContent + "\n", "utf8");
8254
8403
  return "updated";
8255
8404
  }
8256
- const joined = existing.trimEnd() + "\n\n" + wrappedContent + "\n";
8257
- await fs3.promises.writeFile(filePath, joined, "utf8");
8258
- return "appended";
8405
+ const replaced = replaceContextStreamBlock(existing, content);
8406
+ await fs3.promises.writeFile(filePath, replaced.content, "utf8");
8407
+ return replaced.status;
8259
8408
  }
8260
8409
  async function writeEditorRules(options) {
8261
8410
  const editors = options.editors && options.editors.length > 0 ? options.editors : getAvailableEditors();
@@ -8273,6 +8422,10 @@ async function writeEditorRules(options) {
8273
8422
  continue;
8274
8423
  }
8275
8424
  const filePath = path4.join(options.folderPath, rule.filename);
8425
+ if (fs3.existsSync(filePath) && !options.overwriteExisting) {
8426
+ results.push({ editor, filename: rule.filename, status: "skipped (exists)" });
8427
+ continue;
8428
+ }
8276
8429
  try {
8277
8430
  const status = await upsertRuleFile(filePath, rule.content);
8278
8431
  results.push({ editor, filename: rule.filename, status });
@@ -9986,7 +10139,8 @@ Access: Free`,
9986
10139
  description: external_exports.string().optional().describe("Project description"),
9987
10140
  workspace_id: external_exports.string().uuid().optional().describe("Workspace ID (uses current session workspace if not provided)"),
9988
10141
  folder_path: external_exports.string().optional().describe("Optional: Local folder path to associate with this project"),
9989
- generate_editor_rules: external_exports.boolean().optional().describe("Generate AI editor rules in folder_path (requires folder_path)")
10142
+ generate_editor_rules: external_exports.boolean().optional().describe("Generate AI editor rules in folder_path (requires folder_path)"),
10143
+ overwrite_existing: external_exports.boolean().optional().describe("Allow overwriting existing rule files when generating editor rules")
9990
10144
  })
9991
10145
  },
9992
10146
  async (input) => {
@@ -10001,6 +10155,7 @@ Access: Free`,
10001
10155
  });
10002
10156
  const projectData = result;
10003
10157
  let rulesGenerated = [];
10158
+ let rulesSkipped = [];
10004
10159
  if (input.folder_path && projectData.id) {
10005
10160
  try {
10006
10161
  const configDir = path4.join(input.folder_path, ".contextstream");
@@ -10019,9 +10174,11 @@ Access: Free`,
10019
10174
  folderPath: input.folder_path,
10020
10175
  editors: getAvailableEditors(),
10021
10176
  workspaceId,
10022
- projectName: input.name
10177
+ projectName: input.name,
10178
+ overwriteExisting: input.overwrite_existing
10023
10179
  });
10024
10180
  rulesGenerated = ruleResults.filter((r) => r.status === "created" || r.status === "updated" || r.status === "appended").map((r) => r.status === "created" ? r.filename : `${r.filename} (${r.status})`);
10181
+ rulesSkipped = ruleResults.filter((r) => r.status.startsWith("skipped")).map((r) => r.filename);
10025
10182
  }
10026
10183
  } catch (err) {
10027
10184
  console.error("[ContextStream] Failed to write project config:", err);
@@ -10031,7 +10188,8 @@ Access: Free`,
10031
10188
  ...result && typeof result === "object" ? result : {},
10032
10189
  folder_path: input.folder_path,
10033
10190
  config_written: input.folder_path ? true : void 0,
10034
- editor_rules_generated: rulesGenerated.length > 0 ? rulesGenerated : void 0
10191
+ editor_rules_generated: rulesGenerated.length > 0 ? rulesGenerated : void 0,
10192
+ editor_rules_skipped: rulesSkipped.length > 0 ? rulesSkipped : void 0
10035
10193
  };
10036
10194
  return { content: [{ type: "text", text: formatContent(response) }], structuredContent: toStructured(response) };
10037
10195
  }
@@ -11155,24 +11313,29 @@ Optionally generates AI editor rules for automatic ContextStream usage.`,
11155
11313
  workspace_id: external_exports.string().uuid().describe("Workspace ID to associate with"),
11156
11314
  workspace_name: external_exports.string().optional().describe("Workspace name for reference"),
11157
11315
  create_parent_mapping: external_exports.boolean().optional().describe("Also create a parent folder mapping (e.g., /dev/maker/* -> workspace)"),
11158
- generate_editor_rules: external_exports.boolean().optional().describe("Generate AI editor rules for Windsurf, Cursor, Cline, Kilo Code, Roo Code, Claude Code, and Aider")
11316
+ generate_editor_rules: external_exports.boolean().optional().describe("Generate AI editor rules for Windsurf, Cursor, Cline, Kilo Code, Roo Code, Claude Code, and Aider"),
11317
+ overwrite_existing: external_exports.boolean().optional().describe("Allow overwriting existing rule files when generating editor rules")
11159
11318
  })
11160
11319
  },
11161
11320
  async (input) => {
11162
11321
  const result = await client.associateWorkspace(input);
11163
11322
  let rulesGenerated = [];
11323
+ let rulesSkipped = [];
11164
11324
  if (input.generate_editor_rules) {
11165
11325
  const ruleResults = await writeEditorRules({
11166
11326
  folderPath: input.folder_path,
11167
11327
  editors: getAvailableEditors(),
11168
11328
  workspaceName: input.workspace_name,
11169
- workspaceId: input.workspace_id
11329
+ workspaceId: input.workspace_id,
11330
+ overwriteExisting: input.overwrite_existing
11170
11331
  });
11171
11332
  rulesGenerated = ruleResults.filter((r) => r.status === "created" || r.status === "updated" || r.status === "appended").map((r) => r.status === "created" ? r.filename : `${r.filename} (${r.status})`);
11333
+ rulesSkipped = ruleResults.filter((r) => r.status.startsWith("skipped")).map((r) => r.filename);
11172
11334
  }
11173
11335
  const response = {
11174
11336
  ...result,
11175
- editor_rules_generated: rulesGenerated.length > 0 ? rulesGenerated : void 0
11337
+ editor_rules_generated: rulesGenerated.length > 0 ? rulesGenerated : void 0,
11338
+ editor_rules_skipped: rulesSkipped.length > 0 ? rulesSkipped : void 0
11176
11339
  };
11177
11340
  return { content: [{ type: "text", text: formatContent(response) }], structuredContent: toStructured(response) };
11178
11341
  }
@@ -11195,6 +11358,7 @@ Behavior:
11195
11358
  visibility: external_exports.enum(["private", "public"]).optional().describe("Workspace visibility (default: private)"),
11196
11359
  create_parent_mapping: external_exports.boolean().optional().describe("Also create a parent folder mapping (e.g., /dev/company/* -> workspace)"),
11197
11360
  generate_editor_rules: external_exports.boolean().optional().describe("Generate AI editor rules in the folder for automatic ContextStream usage"),
11361
+ overwrite_existing: external_exports.boolean().optional().describe("Allow overwriting existing rule files when generating editor rules"),
11198
11362
  context_hint: external_exports.string().optional().describe("Optional context hint for session initialization"),
11199
11363
  auto_index: external_exports.boolean().optional().describe("Automatically create and index project from folder (default: true)")
11200
11364
  })
@@ -11247,14 +11411,17 @@ Behavior:
11247
11411
  create_parent_mapping: input.create_parent_mapping
11248
11412
  });
11249
11413
  let rulesGenerated = [];
11414
+ let rulesSkipped = [];
11250
11415
  if (input.generate_editor_rules) {
11251
11416
  const ruleResults = await writeEditorRules({
11252
11417
  folderPath,
11253
11418
  editors: getAvailableEditors(),
11254
11419
  workspaceName: newWorkspace.name || input.workspace_name,
11255
- workspaceId: newWorkspace.id
11420
+ workspaceId: newWorkspace.id,
11421
+ overwriteExisting: input.overwrite_existing
11256
11422
  });
11257
11423
  rulesGenerated = ruleResults.filter((r) => r.status === "created" || r.status === "updated" || r.status === "appended").map((r) => r.status === "created" ? r.filename : `${r.filename} (${r.status})`);
11424
+ rulesSkipped = ruleResults.filter((r) => r.status.startsWith("skipped")).map((r) => r.filename);
11258
11425
  }
11259
11426
  const session = await client.initSession(
11260
11427
  {
@@ -11279,7 +11446,8 @@ Behavior:
11279
11446
  name: newWorkspace.name || input.workspace_name
11280
11447
  },
11281
11448
  association: associateResult,
11282
- editor_rules_generated: rulesGenerated.length > 0 ? rulesGenerated : void 0
11449
+ editor_rules_generated: rulesGenerated.length > 0 ? rulesGenerated : void 0,
11450
+ editor_rules_skipped: rulesSkipped.length > 0 ? rulesSkipped : void 0
11283
11451
  }
11284
11452
  };
11285
11453
  return { content: [{ type: "text", text: formatContent(response) }], structuredContent: toStructured(response) };
@@ -11666,6 +11834,7 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
11666
11834
  project_name: external_exports.string().optional().describe("Project name to include in rules"),
11667
11835
  additional_rules: external_exports.string().optional().describe("Additional project-specific rules to append"),
11668
11836
  mode: external_exports.enum(["minimal", "full"]).optional().describe("Rule verbosity mode (default: minimal)"),
11837
+ overwrite_existing: external_exports.boolean().optional().describe("Allow overwriting existing rule files (ContextStream block only)"),
11669
11838
  dry_run: external_exports.boolean().optional().describe("If true, return content without writing files")
11670
11839
  })
11671
11840
  },
@@ -11704,14 +11873,17 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
11704
11873
  workspaceId: input.workspace_id,
11705
11874
  projectName: input.project_name,
11706
11875
  additionalRules: input.additional_rules,
11707
- mode: input.mode
11876
+ mode: input.mode,
11877
+ overwriteExisting: input.overwrite_existing
11708
11878
  });
11709
11879
  results.push(...writeResults);
11710
11880
  }
11881
+ const createdCount = results.filter((r) => r.status === "created" || r.status === "updated" || r.status === "appended").length;
11882
+ const skippedCount = results.filter((r) => r.status.startsWith("skipped")).length;
11711
11883
  const summary = {
11712
11884
  folder: folderPath,
11713
11885
  results,
11714
- message: input.dry_run ? "Dry run complete. Use dry_run: false to write files." : `Generated ${results.filter((r) => r.status === "created" || r.status === "updated" || r.status === "appended").length} rule files.`
11886
+ message: input.dry_run ? "Dry run complete. Use dry_run: false to write files." : skippedCount > 0 ? `Generated ${createdCount} rule files. ${skippedCount} skipped (existing files). Re-run with overwrite_existing: true to replace ContextStream blocks.` : `Generated ${createdCount} rule files.`
11715
11887
  };
11716
11888
  return { content: [{ type: "text", text: formatContent(summary) }], structuredContent: toStructured(summary) };
11717
11889
  }
@@ -15738,11 +15910,159 @@ async function fileExists(filePath) {
15738
15910
  }
15739
15911
  var CONTEXTSTREAM_START_MARKER2 = "<!-- BEGIN ContextStream -->";
15740
15912
  var CONTEXTSTREAM_END_MARKER2 = "<!-- END ContextStream -->";
15913
+ var LEGACY_CONTEXTSTREAM_HINTS2 = [
15914
+ "contextstream integration",
15915
+ "contextstream v0.4",
15916
+ "contextstream v0.3",
15917
+ "contextstream (standard)",
15918
+ "contextstream (consolidated",
15919
+ "contextstream mcp",
15920
+ "contextstream tools"
15921
+ ];
15922
+ var LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS2 = [
15923
+ "contextstream",
15924
+ "tl;dr",
15925
+ "required every message",
15926
+ "quick reference",
15927
+ "tool catalog",
15928
+ "consolidated domain tools",
15929
+ "standalone tools",
15930
+ "domain tools",
15931
+ "why context_smart",
15932
+ "recommended token budgets",
15933
+ "rules update notices",
15934
+ "preferences & lessons",
15935
+ "index & graph preflight",
15936
+ "search & code intelligence",
15937
+ "distillation",
15938
+ "when to capture",
15939
+ "behavior rules",
15940
+ "plans & tasks",
15941
+ "complete action reference"
15942
+ ];
15943
+ var CONTEXTSTREAM_PREAMBLE_PATTERNS2 = [
15944
+ /^#\s+workspace:/i,
15945
+ /^#\s+project:/i,
15946
+ /^#\s+workspace id:/i,
15947
+ /^#\s+codex cli instructions$/i,
15948
+ /^#\s+claude code instructions$/i,
15949
+ /^#\s+cursor rules$/i,
15950
+ /^#\s+windsurf rules$/i,
15951
+ /^#\s+cline rules$/i,
15952
+ /^#\s+kilo code rules$/i,
15953
+ /^#\s+roo code rules$/i,
15954
+ /^#\s+aider configuration$/i
15955
+ ];
15741
15956
  function wrapWithMarkers2(content) {
15742
15957
  return `${CONTEXTSTREAM_START_MARKER2}
15743
15958
  ${content.trim()}
15744
15959
  ${CONTEXTSTREAM_END_MARKER2}`;
15745
15960
  }
15961
+ function isLegacyContextStreamRules2(content) {
15962
+ const lower = content.toLowerCase();
15963
+ if (!lower.includes("contextstream")) return false;
15964
+ if (!LEGACY_CONTEXTSTREAM_HINTS2.some((hint) => lower.includes(hint))) return false;
15965
+ const headingRegex = /^#{1,6}\s+(.+)$/gm;
15966
+ let hasHeading = false;
15967
+ let match;
15968
+ while ((match = headingRegex.exec(content)) !== null) {
15969
+ hasHeading = true;
15970
+ const heading = match[1].trim().toLowerCase();
15971
+ const allowed = LEGACY_CONTEXTSTREAM_ALLOWED_HEADINGS2.some((prefix) => heading.startsWith(prefix));
15972
+ if (!allowed) return false;
15973
+ }
15974
+ return hasHeading;
15975
+ }
15976
+ function isContextStreamPreamble2(line) {
15977
+ const trimmed = line.trim();
15978
+ if (!trimmed) return false;
15979
+ return CONTEXTSTREAM_PREAMBLE_PATTERNS2.some((pattern) => pattern.test(trimmed));
15980
+ }
15981
+ function findContextStreamHeading2(lines) {
15982
+ const headingRegex = /^(#{1,6})\s+(.+)$/;
15983
+ for (let i = 0; i < lines.length; i += 1) {
15984
+ const match = headingRegex.exec(lines[i]);
15985
+ if (!match) continue;
15986
+ if (match[2].toLowerCase().includes("contextstream")) {
15987
+ return { index: i, level: match[1].length };
15988
+ }
15989
+ }
15990
+ return null;
15991
+ }
15992
+ function findSectionEnd2(lines, startLine, level) {
15993
+ const headingRegex = /^(#{1,6})\s+(.+)$/;
15994
+ for (let i = startLine + 1; i < lines.length; i += 1) {
15995
+ const match = headingRegex.exec(lines[i]);
15996
+ if (!match) continue;
15997
+ if (match[1].length <= level) return i;
15998
+ }
15999
+ return lines.length;
16000
+ }
16001
+ function extractContextStreamBlock2(content) {
16002
+ const lines = content.split(/\r?\n/);
16003
+ const heading = findContextStreamHeading2(lines);
16004
+ if (!heading) return content.trim();
16005
+ const endLine = findSectionEnd2(lines, heading.index, heading.level);
16006
+ return lines.slice(heading.index, endLine).join("\n").trim();
16007
+ }
16008
+ function findLegacyContextStreamSection2(content) {
16009
+ const lines = content.split(/\r?\n/);
16010
+ const heading = findContextStreamHeading2(lines);
16011
+ if (!heading) return null;
16012
+ let startLine = heading.index;
16013
+ for (let i = heading.index - 1; i >= 0; i -= 1) {
16014
+ const line = lines[i];
16015
+ if (!line.trim()) {
16016
+ startLine = i;
16017
+ continue;
16018
+ }
16019
+ if (isContextStreamPreamble2(line)) {
16020
+ startLine = i;
16021
+ continue;
16022
+ }
16023
+ break;
16024
+ }
16025
+ const endLine = findSectionEnd2(lines, heading.index, heading.level);
16026
+ return { startLine, endLine, contextLine: heading.index };
16027
+ }
16028
+ function blockHasPreamble2(block) {
16029
+ const lines = block.split(/\r?\n/);
16030
+ for (const line of lines) {
16031
+ const trimmed = line.trim();
16032
+ if (!trimmed) continue;
16033
+ if (isContextStreamPreamble2(trimmed)) return true;
16034
+ if (/^#{1,6}\s+/.test(trimmed)) return false;
16035
+ }
16036
+ return false;
16037
+ }
16038
+ function replaceContextStreamBlock2(existing, content) {
16039
+ const fullWrapped = wrapWithMarkers2(content);
16040
+ const blockWrapped = wrapWithMarkers2(extractContextStreamBlock2(content));
16041
+ const startIdx = existing.indexOf(CONTEXTSTREAM_START_MARKER2);
16042
+ const endIdx = existing.indexOf(CONTEXTSTREAM_END_MARKER2);
16043
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
16044
+ const existingBlock = existing.slice(startIdx + CONTEXTSTREAM_START_MARKER2.length, endIdx);
16045
+ const replacement = blockHasPreamble2(existingBlock) ? fullWrapped : blockWrapped;
16046
+ const before = existing.substring(0, startIdx).trimEnd();
16047
+ const after = existing.substring(endIdx + CONTEXTSTREAM_END_MARKER2.length).trimStart();
16048
+ const merged = [before, replacement, after].filter((part) => part.length > 0).join("\n\n");
16049
+ return { content: merged.trim() + "\n", status: "updated" };
16050
+ }
16051
+ const legacy = findLegacyContextStreamSection2(existing);
16052
+ if (legacy) {
16053
+ const lines = existing.split(/\r?\n/);
16054
+ const before = lines.slice(0, legacy.startLine).join("\n").trimEnd();
16055
+ const after = lines.slice(legacy.endLine).join("\n").trimStart();
16056
+ const replacement = legacy.startLine < legacy.contextLine ? fullWrapped : blockWrapped;
16057
+ const merged = [before, replacement, after].filter((part) => part.length > 0).join("\n\n");
16058
+ return { content: merged.trim() + "\n", status: "updated" };
16059
+ }
16060
+ if (isLegacyContextStreamRules2(existing)) {
16061
+ return { content: fullWrapped + "\n", status: "updated" };
16062
+ }
16063
+ const appended = existing.trimEnd() + "\n\n" + blockWrapped + "\n";
16064
+ return { content: appended, status: "appended" };
16065
+ }
15746
16066
  async function upsertTextFile(filePath, content, _marker) {
15747
16067
  await fs5.mkdir(path6.dirname(filePath), { recursive: true });
15748
16068
  const exists = await fileExists(filePath);
@@ -15752,23 +16072,13 @@ async function upsertTextFile(filePath, content, _marker) {
15752
16072
  return "created";
15753
16073
  }
15754
16074
  const existing = await fs5.readFile(filePath, "utf8").catch(() => "");
15755
- const startIdx = existing.indexOf(CONTEXTSTREAM_START_MARKER2);
15756
- const endIdx = existing.indexOf(CONTEXTSTREAM_END_MARKER2);
15757
- if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
15758
- const before = existing.substring(0, startIdx);
15759
- const after = existing.substring(endIdx + CONTEXTSTREAM_END_MARKER2.length);
15760
- const updated = before.trimEnd() + "\n\n" + wrappedContent + "\n" + after.trimStart();
15761
- await fs5.writeFile(filePath, updated.trim() + "\n", "utf8");
15762
- return "updated";
15763
- }
15764
- if (existing.includes("ContextStream")) {
15765
- const joined2 = existing.trimEnd() + "\n\n" + wrappedContent + "\n";
15766
- await fs5.writeFile(filePath, joined2, "utf8");
16075
+ if (!existing.trim()) {
16076
+ await fs5.writeFile(filePath, wrappedContent + "\n", "utf8");
15767
16077
  return "updated";
15768
16078
  }
15769
- const joined = existing.trimEnd() + "\n\n" + wrappedContent + "\n";
15770
- await fs5.writeFile(filePath, joined, "utf8");
15771
- return "appended";
16079
+ const replaced = replaceContextStreamBlock2(existing, content);
16080
+ await fs5.writeFile(filePath, replaced.content, "utf8");
16081
+ return replaced.status;
15772
16082
  }
15773
16083
  function globalRulesPathForEditor(editor) {
15774
16084
  const home = homedir4();
@@ -16153,6 +16463,27 @@ async function runSetupWizard(args) {
16153
16463
  const dryRun = args.includes("--dry-run");
16154
16464
  const rl = createInterface({ input: stdin, output: stdout });
16155
16465
  const writeActions = [];
16466
+ let overwriteAllRules = null;
16467
+ let skipAllRules = false;
16468
+ const confirmOverwriteRules = async (filePath) => {
16469
+ if (dryRun) return true;
16470
+ if (skipAllRules) return false;
16471
+ if (overwriteAllRules) return true;
16472
+ const exists = await fileExists(filePath);
16473
+ if (!exists) return true;
16474
+ const answer = normalizeInput(
16475
+ await rl.question(`Rules file already exists at ${filePath}. Replace ContextStream block? [y/N/a/s]: `)
16476
+ ).toLowerCase();
16477
+ if (answer === "a" || answer === "all") {
16478
+ overwriteAllRules = true;
16479
+ return true;
16480
+ }
16481
+ if (answer === "s" || answer === "skip-all" || answer === "none") {
16482
+ skipAllRules = true;
16483
+ return false;
16484
+ }
16485
+ return answer === "y" || answer === "yes";
16486
+ };
16156
16487
  try {
16157
16488
  console.log(`ContextStream Setup Wizard (v${VERSION})`);
16158
16489
  console.log("This configures ContextStream MCP + rules for your AI editor(s).");
@@ -16525,6 +16856,12 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
16525
16856
  console.log(`- ${EDITOR_LABELS[editor]}: would write ${filePath}`);
16526
16857
  continue;
16527
16858
  }
16859
+ const allowOverwrite = await confirmOverwriteRules(filePath);
16860
+ if (!allowOverwrite) {
16861
+ writeActions.push({ kind: "rules", target: filePath, status: "skipped" });
16862
+ console.log(`- ${EDITOR_LABELS[editor]}: skipped ${filePath}`);
16863
+ continue;
16864
+ }
16528
16865
  const status = await upsertTextFile(filePath, rule.content, "ContextStream");
16529
16866
  writeActions.push({ kind: "rules", target: filePath, status });
16530
16867
  console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
@@ -16660,6 +16997,11 @@ Applying to ${projects.length} project(s)...`);
16660
16997
  continue;
16661
16998
  }
16662
16999
  try {
17000
+ const allowOverwrite = await confirmOverwriteRules(filePath);
17001
+ if (!allowOverwrite) {
17002
+ writeActions.push({ kind: "rules", target: filePath, status: "skipped" });
17003
+ continue;
17004
+ }
16663
17005
  const status = await upsertTextFile(filePath, rule.content, "ContextStream");
16664
17006
  writeActions.push({ kind: "rules", target: filePath, status });
16665
17007
  } catch (err) {
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.17",
4
+ "version": "0.4.20",
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",