@contextstream/mcp-server 0.3.27 → 0.3.29

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
@@ -487,8 +487,8 @@ function getErrorMap() {
487
487
 
488
488
  // node_modules/zod/v3/helpers/parseUtil.js
489
489
  var makeIssue = (params) => {
490
- const { data, path: path3, errorMaps, issueData } = params;
491
- const fullPath = [...path3, ...issueData.path || []];
490
+ const { data, path: path6, errorMaps, issueData } = params;
491
+ const fullPath = [...path6, ...issueData.path || []];
492
492
  const fullIssue = {
493
493
  ...issueData,
494
494
  path: fullPath
@@ -604,11 +604,11 @@ var errorUtil;
604
604
 
605
605
  // node_modules/zod/v3/types.js
606
606
  var ParseInputLazyPath = class {
607
- constructor(parent, value, path3, key) {
607
+ constructor(parent, value, path6, key) {
608
608
  this._cachedPath = [];
609
609
  this.parent = parent;
610
610
  this.data = value;
611
- this._path = path3;
611
+ this._path = path6;
612
612
  this._key = key;
613
613
  }
614
614
  get path() {
@@ -4082,6 +4082,7 @@ function loadConfig() {
4082
4082
 
4083
4083
  // src/client.ts
4084
4084
  import { randomUUID } from "node:crypto";
4085
+ import * as path3 from "node:path";
4085
4086
 
4086
4087
  // src/http.ts
4087
4088
  var HttpError = class extends Error {
@@ -4135,11 +4136,11 @@ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504])
4135
4136
  var MAX_RETRIES = 3;
4136
4137
  var BASE_DELAY = 1e3;
4137
4138
  async function sleep(ms) {
4138
- return new Promise((resolve) => setTimeout(resolve, ms));
4139
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4139
4140
  }
4140
- async function request(config, path3, options = {}) {
4141
+ async function request(config, path6, options = {}) {
4141
4142
  const { apiUrl, apiKey, jwt, userAgent } = config;
4142
- const apiPath = path3.startsWith("/api/") ? path3 : `/api/v1${path3}`;
4143
+ const apiPath = path6.startsWith("/api/") ? path6 : `/api/v1${path6}`;
4143
4144
  const url = `${apiUrl.replace(/\/$/, "")}${apiPath}`;
4144
4145
  const maxRetries = options.retries ?? MAX_RETRIES;
4145
4146
  const baseDelay = options.retryDelay ?? BASE_DELAY;
@@ -4408,8 +4409,8 @@ async function* readAllFilesInBatches(rootPath, options = {}) {
4408
4409
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
4409
4410
  if (!CODE_EXTENSIONS.has(ext)) continue;
4410
4411
  try {
4411
- const stat = await fs.promises.stat(fullPath);
4412
- if (stat.size > maxFileSize) continue;
4412
+ const stat2 = await fs.promises.stat(fullPath);
4413
+ if (stat2.size > maxFileSize) continue;
4413
4414
  const content = await fs.promises.readFile(fullPath, "utf-8");
4414
4415
  yield { path: relPath, content };
4415
4416
  } catch {
@@ -4486,21 +4487,23 @@ function writeGlobalMappings(mappings) {
4486
4487
  }
4487
4488
  }
4488
4489
  function addGlobalMapping(mapping) {
4490
+ const normalizedPattern = path2.normalize(mapping.pattern);
4489
4491
  const mappings = readGlobalMappings();
4490
- const filtered = mappings.filter((m) => m.pattern !== mapping.pattern);
4491
- filtered.push(mapping);
4492
+ const filtered = mappings.filter((m) => path2.normalize(m.pattern) !== normalizedPattern);
4493
+ filtered.push({ ...mapping, pattern: normalizedPattern });
4492
4494
  return writeGlobalMappings(filtered);
4493
4495
  }
4494
4496
  function findMatchingMapping(repoPath) {
4495
4497
  const mappings = readGlobalMappings();
4496
4498
  const normalizedRepo = path2.normalize(repoPath);
4497
4499
  for (const mapping of mappings) {
4498
- if (mapping.pattern.endsWith("/*")) {
4499
- const parentDir = mapping.pattern.slice(0, -2);
4500
+ const normalizedPattern = path2.normalize(mapping.pattern);
4501
+ if (normalizedPattern.endsWith(`${path2.sep}*`)) {
4502
+ const parentDir = normalizedPattern.slice(0, -2);
4500
4503
  if (normalizedRepo.startsWith(parentDir + path2.sep)) {
4501
4504
  return mapping;
4502
4505
  }
4503
- } else if (normalizedRepo === path2.normalize(mapping.pattern)) {
4506
+ } else if (normalizedRepo === normalizedPattern) {
4504
4507
  return mapping;
4505
4508
  }
4506
4509
  }
@@ -4530,6 +4533,7 @@ var MemoryCache = class {
4530
4533
  this.cache = /* @__PURE__ */ new Map();
4531
4534
  this.cleanupInterval = null;
4532
4535
  this.cleanupInterval = setInterval(() => this.cleanup(), cleanupIntervalMs);
4536
+ this.cleanupInterval.unref?.();
4533
4537
  }
4534
4538
  /**
4535
4539
  * Get a cached value if it exists and hasn't expired
@@ -4677,6 +4681,15 @@ var ContextStreamClient = class {
4677
4681
  me() {
4678
4682
  return request(this.config, "/auth/me");
4679
4683
  }
4684
+ startDeviceLogin() {
4685
+ return request(this.config, "/auth/device/start", { method: "POST" });
4686
+ }
4687
+ pollDeviceLogin(input) {
4688
+ return request(this.config, "/auth/device/token", { body: input });
4689
+ }
4690
+ createApiKey(input) {
4691
+ return request(this.config, "/auth/api-keys", { body: input });
4692
+ }
4680
4693
  // Credits / Billing (used for plan gating)
4681
4694
  async getCreditBalance() {
4682
4695
  const cacheKey = CacheKeys.creditBalance();
@@ -5024,7 +5037,7 @@ var ContextStreamClient = class {
5024
5037
  context.workspace_source = resolved.source;
5025
5038
  context.workspace_resolved_from = resolved.source === "local_config" ? `${rootPath}/.contextstream/config.json` : "parent_folder_mapping";
5026
5039
  } else {
5027
- const folderName = rootPath?.split("/").pop()?.toLowerCase() || "";
5040
+ const folderName = rootPath ? path3.basename(rootPath).toLowerCase() : "";
5028
5041
  try {
5029
5042
  const workspaces = await this.listWorkspaces({ page_size: 50 });
5030
5043
  if (workspaces.items && workspaces.items.length > 0) {
@@ -5079,13 +5092,13 @@ var ContextStreamClient = class {
5079
5092
  name: w.name,
5080
5093
  description: w.description
5081
5094
  }));
5082
- context.message = `New folder detected: "${rootPath?.split("/").pop()}". Please select which workspace this belongs to, or create a new one.`;
5095
+ context.message = `New folder detected: "${rootPath ? path3.basename(rootPath) : "this folder"}". Please select which workspace this belongs to, or create a new one.`;
5083
5096
  context.ide_roots = ideRoots;
5084
- context.folder_name = rootPath?.split("/").pop();
5097
+ context.folder_name = rootPath ? path3.basename(rootPath) : void 0;
5085
5098
  return context;
5086
5099
  }
5087
5100
  } else {
5088
- const folderDisplayName = rootPath?.split("/").pop() || "this folder";
5101
+ const folderDisplayName = rootPath ? path3.basename(rootPath) || "this folder" : "this folder";
5089
5102
  context.status = "requires_workspace_name";
5090
5103
  context.workspace_source = "none_found";
5091
5104
  context.ide_roots = ideRoots;
@@ -5113,7 +5126,7 @@ var ContextStreamClient = class {
5113
5126
  }
5114
5127
  }
5115
5128
  if (!workspaceId && !params.allow_no_workspace) {
5116
- const folderDisplayName = rootPath?.split("/").pop() || "this folder";
5129
+ const folderDisplayName = rootPath ? path3.basename(rootPath) || "this folder" : "this folder";
5117
5130
  context.ide_roots = ideRoots;
5118
5131
  context.folder_name = folderDisplayName;
5119
5132
  if (rootPath) {
@@ -5145,7 +5158,7 @@ var ContextStreamClient = class {
5145
5158
  }
5146
5159
  }
5147
5160
  if (!projectId && workspaceId && rootPath && params.auto_index !== false) {
5148
- const projectName = rootPath.split("/").pop() || "My Project";
5161
+ const projectName = path3.basename(rootPath) || "My Project";
5149
5162
  try {
5150
5163
  const projects = await this.listProjects({ workspace_id: workspaceId });
5151
5164
  const projectNameLower = projectName.toLowerCase();
@@ -5353,9 +5366,9 @@ var ContextStreamClient = class {
5353
5366
  associated_at: (/* @__PURE__ */ new Date()).toISOString()
5354
5367
  });
5355
5368
  if (create_parent_mapping) {
5356
- const parentDir = folder_path.split("/").slice(0, -1).join("/");
5369
+ const parentDir = path3.dirname(folder_path);
5357
5370
  addGlobalMapping({
5358
- pattern: `${parentDir}/*`,
5371
+ pattern: path3.join(parentDir, "*"),
5359
5372
  workspace_id,
5360
5373
  workspace_name: workspace_name || "Unknown"
5361
5374
  });
@@ -5829,9 +5842,9 @@ var ContextStreamClient = class {
5829
5842
  candidateParts.push("## Relevant Code\n");
5830
5843
  currentChars += 18;
5831
5844
  const codeEntries = code.results.map((c) => {
5832
- const path3 = c.file_path || "file";
5845
+ const path6 = c.file_path || "file";
5833
5846
  const content = c.content?.slice(0, 150) || "";
5834
- return { path: path3, entry: `\u2022 ${path3}: ${content}...
5847
+ return { path: path6, entry: `\u2022 ${path6}: ${content}...
5835
5848
  ` };
5836
5849
  });
5837
5850
  for (const c of codeEntries) {
@@ -6282,10 +6295,221 @@ W:${wsHint}
6282
6295
  }
6283
6296
  return matches / keywords.length;
6284
6297
  }
6298
+ // ============================================
6299
+ // Slack Integration Methods
6300
+ // ============================================
6301
+ /**
6302
+ * Get Slack integration statistics and overview
6303
+ */
6304
+ async slackStats(params) {
6305
+ const withDefaults = this.withDefaults(params || {});
6306
+ if (!withDefaults.workspace_id) {
6307
+ throw new Error("workspace_id is required for Slack stats");
6308
+ }
6309
+ const query = new URLSearchParams();
6310
+ if (params?.days) query.set("days", String(params.days));
6311
+ const suffix = query.toString() ? `?${query.toString()}` : "";
6312
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/slack/stats${suffix}`, { method: "GET" });
6313
+ }
6314
+ /**
6315
+ * Get Slack users for a workspace
6316
+ */
6317
+ async slackUsers(params) {
6318
+ const withDefaults = this.withDefaults(params || {});
6319
+ if (!withDefaults.workspace_id) {
6320
+ throw new Error("workspace_id is required for Slack users");
6321
+ }
6322
+ const query = new URLSearchParams();
6323
+ if (params?.page) query.set("page", String(params.page));
6324
+ if (params?.per_page) query.set("per_page", String(params.per_page));
6325
+ const suffix = query.toString() ? `?${query.toString()}` : "";
6326
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/slack/users${suffix}`, { method: "GET" });
6327
+ }
6328
+ /**
6329
+ * Get Slack channels with stats
6330
+ */
6331
+ async slackChannels(params) {
6332
+ const withDefaults = this.withDefaults(params || {});
6333
+ if (!withDefaults.workspace_id) {
6334
+ throw new Error("workspace_id is required for Slack channels");
6335
+ }
6336
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/slack/channels`, { method: "GET" });
6337
+ }
6338
+ /**
6339
+ * Get recent Slack activity feed
6340
+ */
6341
+ async slackActivity(params) {
6342
+ const withDefaults = this.withDefaults(params || {});
6343
+ if (!withDefaults.workspace_id) {
6344
+ throw new Error("workspace_id is required for Slack activity");
6345
+ }
6346
+ const query = new URLSearchParams();
6347
+ if (params?.limit) query.set("limit", String(params.limit));
6348
+ if (params?.offset) query.set("offset", String(params.offset));
6349
+ if (params?.channel_id) query.set("channel_id", params.channel_id);
6350
+ const suffix = query.toString() ? `?${query.toString()}` : "";
6351
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/slack/activity${suffix}`, { method: "GET" });
6352
+ }
6353
+ /**
6354
+ * Get high-engagement Slack discussions
6355
+ */
6356
+ async slackDiscussions(params) {
6357
+ const withDefaults = this.withDefaults(params || {});
6358
+ if (!withDefaults.workspace_id) {
6359
+ throw new Error("workspace_id is required for Slack discussions");
6360
+ }
6361
+ const query = new URLSearchParams();
6362
+ if (params?.limit) query.set("limit", String(params.limit));
6363
+ const suffix = query.toString() ? `?${query.toString()}` : "";
6364
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/slack/discussions${suffix}`, { method: "GET" });
6365
+ }
6366
+ /**
6367
+ * Get top Slack contributors
6368
+ */
6369
+ async slackContributors(params) {
6370
+ const withDefaults = this.withDefaults(params || {});
6371
+ if (!withDefaults.workspace_id) {
6372
+ throw new Error("workspace_id is required for Slack contributors");
6373
+ }
6374
+ const query = new URLSearchParams();
6375
+ if (params?.limit) query.set("limit", String(params.limit));
6376
+ const suffix = query.toString() ? `?${query.toString()}` : "";
6377
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/slack/contributors${suffix}`, { method: "GET" });
6378
+ }
6379
+ /**
6380
+ * Trigger a sync of Slack user profiles
6381
+ */
6382
+ async slackSyncUsers(params) {
6383
+ const withDefaults = this.withDefaults(params || {});
6384
+ if (!withDefaults.workspace_id) {
6385
+ throw new Error("workspace_id is required for syncing Slack users");
6386
+ }
6387
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/slack/sync-users`, { method: "POST" });
6388
+ }
6389
+ /**
6390
+ * Search Slack messages
6391
+ */
6392
+ async slackSearch(params) {
6393
+ const withDefaults = this.withDefaults(params || {});
6394
+ if (!withDefaults.workspace_id) {
6395
+ throw new Error("workspace_id is required for Slack search");
6396
+ }
6397
+ const query = new URLSearchParams();
6398
+ query.set("q", params.q);
6399
+ if (params?.limit) query.set("limit", String(params.limit));
6400
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/slack/search?${query.toString()}`, { method: "GET" });
6401
+ }
6402
+ // ============================================
6403
+ // GitHub Integration Methods
6404
+ // ============================================
6405
+ /**
6406
+ * Get GitHub integration statistics and overview
6407
+ */
6408
+ async githubStats(params) {
6409
+ const withDefaults = this.withDefaults(params || {});
6410
+ if (!withDefaults.workspace_id) {
6411
+ throw new Error("workspace_id is required for GitHub stats");
6412
+ }
6413
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/github/stats`, { method: "GET" });
6414
+ }
6415
+ /**
6416
+ * Get GitHub repository stats
6417
+ */
6418
+ async githubRepos(params) {
6419
+ const withDefaults = this.withDefaults(params || {});
6420
+ if (!withDefaults.workspace_id) {
6421
+ throw new Error("workspace_id is required for GitHub repos");
6422
+ }
6423
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/github/repos`, { method: "GET" });
6424
+ }
6425
+ /**
6426
+ * Get recent GitHub activity feed
6427
+ */
6428
+ async githubActivity(params) {
6429
+ const withDefaults = this.withDefaults(params || {});
6430
+ if (!withDefaults.workspace_id) {
6431
+ throw new Error("workspace_id is required for GitHub activity");
6432
+ }
6433
+ const query = new URLSearchParams();
6434
+ if (params?.limit) query.set("limit", String(params.limit));
6435
+ if (params?.offset) query.set("offset", String(params.offset));
6436
+ if (params?.repo) query.set("repo", params.repo);
6437
+ if (params?.type) query.set("type", params.type);
6438
+ const suffix = query.toString() ? `?${query.toString()}` : "";
6439
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/github/activity${suffix}`, { method: "GET" });
6440
+ }
6441
+ /**
6442
+ * Get GitHub issues and PRs
6443
+ */
6444
+ async githubIssues(params) {
6445
+ const withDefaults = this.withDefaults(params || {});
6446
+ if (!withDefaults.workspace_id) {
6447
+ throw new Error("workspace_id is required for GitHub issues");
6448
+ }
6449
+ const query = new URLSearchParams();
6450
+ if (params?.limit) query.set("limit", String(params.limit));
6451
+ if (params?.offset) query.set("offset", String(params.offset));
6452
+ if (params?.state) query.set("state", params.state);
6453
+ if (params?.repo) query.set("repo", params.repo);
6454
+ const suffix = query.toString() ? `?${query.toString()}` : "";
6455
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/github/issues${suffix}`, { method: "GET" });
6456
+ }
6457
+ /**
6458
+ * Get top GitHub contributors
6459
+ */
6460
+ async githubContributors(params) {
6461
+ const withDefaults = this.withDefaults(params || {});
6462
+ if (!withDefaults.workspace_id) {
6463
+ throw new Error("workspace_id is required for GitHub contributors");
6464
+ }
6465
+ const query = new URLSearchParams();
6466
+ if (params?.limit) query.set("limit", String(params.limit));
6467
+ const suffix = query.toString() ? `?${query.toString()}` : "";
6468
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/github/contributors${suffix}`, { method: "GET" });
6469
+ }
6470
+ /**
6471
+ * Search GitHub content
6472
+ */
6473
+ async githubSearch(params) {
6474
+ const withDefaults = this.withDefaults(params || {});
6475
+ if (!withDefaults.workspace_id) {
6476
+ throw new Error("workspace_id is required for GitHub search");
6477
+ }
6478
+ const query = new URLSearchParams();
6479
+ query.set("q", params.q);
6480
+ if (params?.limit) query.set("limit", String(params.limit));
6481
+ return request(this.config, `/workspaces/${withDefaults.workspace_id}/github/search?${query.toString()}`, { method: "GET" });
6482
+ }
6285
6483
  };
6286
6484
 
6485
+ // src/tools.ts
6486
+ import * as path4 from "node:path";
6487
+
6287
6488
  // src/rules-templates.ts
6288
- var CONTEXTSTREAM_RULES = `
6489
+ var DEFAULT_CLAUDE_MCP_SERVER_NAME = "contextstream";
6490
+ var CONTEXTSTREAM_TOOL_NAMES = [
6491
+ "session_init",
6492
+ "context_smart",
6493
+ "session_summary",
6494
+ "session_capture",
6495
+ "session_capture_lesson",
6496
+ "session_get_lessons",
6497
+ "session_recall",
6498
+ "session_remember",
6499
+ "session_get_user_context",
6500
+ "session_smart_search",
6501
+ "session_compress",
6502
+ "session_delta",
6503
+ "generate_editor_rules",
6504
+ "workspace_associate",
6505
+ "workspace_bootstrap"
6506
+ ];
6507
+ function applyMcpToolPrefix(markdown, toolPrefix) {
6508
+ const toolPattern = CONTEXTSTREAM_TOOL_NAMES.join("|");
6509
+ const toolRegex = new RegExp(`(?<!__)\\b(${toolPattern})\\b`, "g");
6510
+ return markdown.replace(toolRegex, `${toolPrefix}$1`);
6511
+ }
6512
+ var CONTEXTSTREAM_RULES_FULL = `
6289
6513
  ## ContextStream Integration
6290
6514
 
6291
6515
  You have access to ContextStream MCP tools for persistent memory and context.
@@ -6295,7 +6519,7 @@ You have access to ContextStream MCP tools for persistent memory and context.
6295
6519
  | Message | What to Call |
6296
6520
  |---------|--------------|
6297
6521
  | **1st message** | \`session_init(folder_path="...", context_hint="<user's message>")\` |
6298
- | **2nd+ messages** | \`context_smart(user_message="<user's message>")\` |
6522
+ | **2nd+ messages** | \`context_smart(user_message="<user's message>", max_tokens=400)\` |
6299
6523
  | **After completing task** | \`session_capture(...)\` - MUST capture decisions/insights |
6300
6524
  | **User frustration/correction** | \`session_capture_lesson(...)\` - MUST capture lessons |
6301
6525
  | **Command/tool error + fix** | \`session_capture_lesson(...)\` - MUST capture lessons |
@@ -6322,6 +6546,17 @@ You have access to ContextStream MCP tools for persistent memory and context.
6322
6546
 
6323
6547
  ---
6324
6548
 
6549
+ ### Recommended Token Budgets
6550
+
6551
+ - For trivial/local edits: \`context_smart(..., max_tokens=200)\`
6552
+ - Default: \`context_smart(..., max_tokens=400)\`
6553
+ - Deep debugging/architecture or heavy \u201Cwhat did we decide?\u201D: \`context_smart(..., max_tokens=800)\`
6554
+ - Keep \`format="minified"\` (default) unless you\u2019re actively debugging tool output
6555
+
6556
+ If context still feels missing, increase \`max_tokens\` and/or call \`session_recall\` for a focused deep lookup.
6557
+
6558
+ ---
6559
+
6325
6560
  ### When to Capture (MANDATORY)
6326
6561
 
6327
6562
  | When | Tool | Example |
@@ -6408,7 +6643,7 @@ session_init(folder_path="/path/to/project", context_hint="how should I implemen
6408
6643
  # Returns workspace info + semantically relevant auth decisions from ANY time
6409
6644
 
6410
6645
  # Second message - user asks about database
6411
- context_smart(user_message="what database should I use?")
6646
+ context_smart(user_message="what database should I use?", max_tokens=400)
6412
6647
  # Returns: W:Maker|P:myproject|D:Use PostgreSQL|D:No ORMs|M:DB schema at...
6413
6648
 
6414
6649
  # User says "Let's use Redis for caching"
@@ -6421,58 +6656,75 @@ session_capture(event_type="decision", title="Auth Implementation Complete", con
6421
6656
  session_recall(query="what did we decide about caching?")
6422
6657
  \`\`\`
6423
6658
  `.trim();
6659
+ var CONTEXTSTREAM_RULES_MINIMAL = `
6660
+ ## ContextStream (Minimal)
6661
+
6662
+ - First user message: \`session_init(folder_path="<cwd>", context_hint="<user_message>")\`, then answer.
6663
+ - Every user message: \`context_smart(user_message="<user_message>", format="minified", max_tokens=400)\` BEFORE answering.
6664
+ - Use \`max_tokens=200\` for trivial/local edits, \`max_tokens=800\` for deep debugging/architecture.
6665
+ - If prior context is missing or user asks past decisions: \`session_recall(query="<question>")\`, then answer.
6666
+ - After meaningful work/decisions/preferences: \`session_capture(event_type=decision|preference|task|insight, title="\u2026", content="\u2026")\`.
6667
+ - On frustration/corrections/tool mistakes: \`session_capture_lesson(...)\`.
6668
+ `.trim();
6424
6669
  var TEMPLATES = {
6670
+ codex: {
6671
+ filename: "AGENTS.md",
6672
+ description: "Codex CLI agent instructions",
6673
+ build: (rules) => `# Codex CLI Instructions
6674
+ ${rules}
6675
+ `
6676
+ },
6425
6677
  windsurf: {
6426
6678
  filename: ".windsurfrules",
6427
6679
  description: "Windsurf AI rules",
6428
- content: `# Windsurf Rules
6429
- ${CONTEXTSTREAM_RULES}
6680
+ build: (rules) => `# Windsurf Rules
6681
+ ${rules}
6430
6682
  `
6431
6683
  },
6432
6684
  cursor: {
6433
6685
  filename: ".cursorrules",
6434
6686
  description: "Cursor AI rules",
6435
- content: `# Cursor Rules
6436
- ${CONTEXTSTREAM_RULES}
6687
+ build: (rules) => `# Cursor Rules
6688
+ ${rules}
6437
6689
  `
6438
6690
  },
6439
6691
  cline: {
6440
6692
  filename: ".clinerules",
6441
6693
  description: "Cline AI rules",
6442
- content: `# Cline Rules
6443
- ${CONTEXTSTREAM_RULES}
6694
+ build: (rules) => `# Cline Rules
6695
+ ${rules}
6444
6696
  `
6445
6697
  },
6446
6698
  kilo: {
6447
6699
  filename: ".kilocode/rules/contextstream.md",
6448
6700
  description: "Kilo Code AI rules",
6449
- content: `# Kilo Code Rules
6450
- ${CONTEXTSTREAM_RULES}
6701
+ build: (rules) => `# Kilo Code Rules
6702
+ ${rules}
6451
6703
  `
6452
6704
  },
6453
6705
  roo: {
6454
6706
  filename: ".roo/rules/contextstream.md",
6455
6707
  description: "Roo Code AI rules",
6456
- content: `# Roo Code Rules
6457
- ${CONTEXTSTREAM_RULES}
6708
+ build: (rules) => `# Roo Code Rules
6709
+ ${rules}
6458
6710
  `
6459
6711
  },
6460
6712
  claude: {
6461
6713
  filename: "CLAUDE.md",
6462
6714
  description: "Claude Code instructions",
6463
- content: `# Claude Code Instructions
6464
- ${CONTEXTSTREAM_RULES}
6715
+ build: (rules) => `# Claude Code Instructions
6716
+ ${rules}
6465
6717
  `
6466
6718
  },
6467
6719
  aider: {
6468
6720
  filename: ".aider.conf.yml",
6469
6721
  description: "Aider configuration with system prompt",
6470
- content: `# Aider Configuration
6722
+ build: (rules) => `# Aider Configuration
6471
6723
  # Note: Aider uses different config format - this adds to the system prompt
6472
6724
 
6473
6725
  # Add ContextStream guidance to conventions
6474
6726
  conventions: |
6475
- ${CONTEXTSTREAM_RULES.split("\n").map((line) => " " + line).join("\n")}
6727
+ ${rules.split("\n").map((line) => " " + line).join("\n")}
6476
6728
  `
6477
6729
  }
6478
6730
  };
@@ -6485,7 +6737,9 @@ function getTemplate(editor) {
6485
6737
  function generateRuleContent(editor, options) {
6486
6738
  const template = getTemplate(editor);
6487
6739
  if (!template) return null;
6488
- let content = template.content;
6740
+ const mode = options?.mode || "minimal";
6741
+ const rules = mode === "full" ? CONTEXTSTREAM_RULES_FULL : CONTEXTSTREAM_RULES_MINIMAL;
6742
+ let content = template.build(rules);
6489
6743
  if (options?.workspaceName || options?.projectName) {
6490
6744
  const header = `
6491
6745
  # Workspace: ${options.workspaceName || "Unknown"}
@@ -6498,6 +6752,9 @@ ${options.workspaceId ? `# Workspace ID: ${options.workspaceId}` : ""}
6498
6752
  if (options?.additionalRules) {
6499
6753
  content += "\n\n## Project-Specific Rules\n\n" + options.additionalRules;
6500
6754
  }
6755
+ if (editor.toLowerCase() === "claude") {
6756
+ content = applyMcpToolPrefix(content, `mcp__${DEFAULT_CLAUDE_MCP_SERVER_NAME}__`);
6757
+ }
6501
6758
  return {
6502
6759
  filename: template.filename,
6503
6760
  content: content.trim() + "\n"
@@ -6537,7 +6794,22 @@ function registerTools(server, client, sessionManager) {
6537
6794
  "ai_context_budget",
6538
6795
  "ai_embeddings",
6539
6796
  "ai_plan",
6540
- "ai_tasks"
6797
+ "ai_tasks",
6798
+ // Slack integration tools
6799
+ "slack_stats",
6800
+ "slack_channels",
6801
+ "slack_contributors",
6802
+ "slack_activity",
6803
+ "slack_discussions",
6804
+ "slack_search",
6805
+ "slack_sync_users",
6806
+ // GitHub integration tools
6807
+ "github_stats",
6808
+ "github_repos",
6809
+ "github_contributors",
6810
+ "github_activity",
6811
+ "github_issues",
6812
+ "github_search"
6541
6813
  ]);
6542
6814
  const proTools = (() => {
6543
6815
  const raw = process.env.CONTEXTSTREAM_PRO_TOOLS;
@@ -7594,7 +7866,7 @@ This does semantic search on the first message. You only need context_smart on s
7594
7866
  formatContent(result)
7595
7867
  ].join("\n");
7596
7868
  } else if (status === "requires_workspace_selection") {
7597
- const folderName = typeof result.folder_name === "string" ? result.folder_name : typeof input.folder_path === "string" ? input.folder_path.split("/").pop() || "this folder" : "this folder";
7869
+ const folderName = typeof result.folder_name === "string" ? result.folder_name : typeof input.folder_path === "string" ? path4.basename(input.folder_path) || "this folder" : "this folder";
7598
7870
  const candidates = Array.isArray(result.workspace_candidates) ? result.workspace_candidates : [];
7599
7871
  const lines = [];
7600
7872
  lines.push(`Action required: select a workspace for "${folderName}" (or create a new one).`);
@@ -7664,26 +7936,26 @@ Optionally generates AI editor rules for automatic ContextStream usage.`,
7664
7936
  const result = await client.associateWorkspace(input);
7665
7937
  let rulesGenerated = [];
7666
7938
  if (input.generate_editor_rules) {
7667
- const fs3 = await import("fs");
7668
- const path3 = await import("path");
7939
+ const fs4 = await import("fs");
7940
+ const path6 = await import("path");
7669
7941
  for (const editor of getAvailableEditors()) {
7670
7942
  const rule = generateRuleContent(editor, {
7671
7943
  workspaceName: input.workspace_name,
7672
7944
  workspaceId: input.workspace_id
7673
7945
  });
7674
7946
  if (rule) {
7675
- const filePath = path3.join(input.folder_path, rule.filename);
7947
+ const filePath = path6.join(input.folder_path, rule.filename);
7676
7948
  try {
7677
7949
  let existingContent = "";
7678
7950
  try {
7679
- existingContent = fs3.readFileSync(filePath, "utf-8");
7951
+ existingContent = fs4.readFileSync(filePath, "utf-8");
7680
7952
  } catch {
7681
7953
  }
7682
7954
  if (!existingContent) {
7683
- fs3.writeFileSync(filePath, rule.content);
7955
+ fs4.writeFileSync(filePath, rule.content);
7684
7956
  rulesGenerated.push(rule.filename);
7685
7957
  } else if (!existingContent.includes("ContextStream Integration")) {
7686
- fs3.writeFileSync(filePath, existingContent + "\n\n" + rule.content);
7958
+ fs4.writeFileSync(filePath, existingContent + "\n\n" + rule.content);
7687
7959
  rulesGenerated.push(rule.filename + " (appended)");
7688
7960
  }
7689
7961
  } catch {
@@ -7737,7 +8009,7 @@ Behavior:
7737
8009
  if (!folderPath) {
7738
8010
  return errorResult("Error: folder_path is required. Provide folder_path or run from a project directory.");
7739
8011
  }
7740
- const folderName = folderPath.split("/").pop() || "My Project";
8012
+ const folderName = path4.basename(folderPath) || "My Project";
7741
8013
  let newWorkspace;
7742
8014
  try {
7743
8015
  newWorkspace = await client.createWorkspace({
@@ -7769,26 +8041,26 @@ Behavior:
7769
8041
  });
7770
8042
  let rulesGenerated = [];
7771
8043
  if (input.generate_editor_rules) {
7772
- const fs3 = await import("fs");
7773
- const path3 = await import("path");
8044
+ const fs4 = await import("fs");
8045
+ const path6 = await import("path");
7774
8046
  for (const editor of getAvailableEditors()) {
7775
8047
  const rule = generateRuleContent(editor, {
7776
8048
  workspaceName: newWorkspace.name || input.workspace_name,
7777
8049
  workspaceId: newWorkspace.id
7778
8050
  });
7779
8051
  if (!rule) continue;
7780
- const filePath = path3.join(folderPath, rule.filename);
8052
+ const filePath = path6.join(folderPath, rule.filename);
7781
8053
  try {
7782
8054
  let existingContent = "";
7783
8055
  try {
7784
- existingContent = fs3.readFileSync(filePath, "utf-8");
8056
+ existingContent = fs4.readFileSync(filePath, "utf-8");
7785
8057
  } catch {
7786
8058
  }
7787
8059
  if (!existingContent) {
7788
- fs3.writeFileSync(filePath, rule.content);
8060
+ fs4.writeFileSync(filePath, rule.content);
7789
8061
  rulesGenerated.push(rule.filename);
7790
8062
  } else if (!existingContent.includes("ContextStream Integration")) {
7791
- fs3.writeFileSync(filePath, existingContent + "\n\n" + rule.content);
8063
+ fs4.writeFileSync(filePath, existingContent + "\n\n" + rule.content);
7792
8064
  rulesGenerated.push(rule.filename + " (appended)");
7793
8065
  }
7794
8066
  } catch {
@@ -8178,17 +8450,18 @@ These rules instruct the AI to automatically use ContextStream for memory and co
8178
8450
  Supported editors: ${getAvailableEditors().join(", ")}`,
8179
8451
  inputSchema: external_exports.object({
8180
8452
  folder_path: external_exports.string().describe("Absolute path to the project folder"),
8181
- editors: external_exports.array(external_exports.enum(["windsurf", "cursor", "cline", "kilo", "roo", "claude", "aider", "all"])).optional().describe("Which editors to generate rules for. Defaults to all."),
8453
+ editors: external_exports.array(external_exports.enum(["codex", "windsurf", "cursor", "cline", "kilo", "roo", "claude", "aider", "all"])).optional().describe("Which editors to generate rules for. Defaults to all."),
8182
8454
  workspace_name: external_exports.string().optional().describe("Workspace name to include in rules"),
8183
8455
  workspace_id: external_exports.string().uuid().optional().describe("Workspace ID to include in rules"),
8184
8456
  project_name: external_exports.string().optional().describe("Project name to include in rules"),
8185
8457
  additional_rules: external_exports.string().optional().describe("Additional project-specific rules to append"),
8458
+ mode: external_exports.enum(["minimal", "full"]).optional().describe("Rule verbosity mode (default: minimal)"),
8186
8459
  dry_run: external_exports.boolean().optional().describe("If true, return content without writing files")
8187
8460
  })
8188
8461
  },
8189
8462
  async (input) => {
8190
- const fs3 = await import("fs");
8191
- const path3 = await import("path");
8463
+ const fs4 = await import("fs");
8464
+ const path6 = await import("path");
8192
8465
  const editors = input.editors?.includes("all") || !input.editors ? getAvailableEditors() : input.editors.filter((e) => e !== "all");
8193
8466
  const results = [];
8194
8467
  for (const editor of editors) {
@@ -8196,13 +8469,14 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
8196
8469
  workspaceName: input.workspace_name,
8197
8470
  workspaceId: input.workspace_id,
8198
8471
  projectName: input.project_name,
8199
- additionalRules: input.additional_rules
8472
+ additionalRules: input.additional_rules,
8473
+ mode: input.mode
8200
8474
  });
8201
8475
  if (!rule) {
8202
8476
  results.push({ editor, filename: "", status: "unknown editor" });
8203
8477
  continue;
8204
8478
  }
8205
- const filePath = path3.join(input.folder_path, rule.filename);
8479
+ const filePath = path6.join(input.folder_path, rule.filename);
8206
8480
  if (input.dry_run) {
8207
8481
  results.push({
8208
8482
  editor,
@@ -8214,15 +8488,15 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
8214
8488
  try {
8215
8489
  let existingContent = "";
8216
8490
  try {
8217
- existingContent = fs3.readFileSync(filePath, "utf-8");
8491
+ existingContent = fs4.readFileSync(filePath, "utf-8");
8218
8492
  } catch {
8219
8493
  }
8220
8494
  if (existingContent && !existingContent.includes("ContextStream Integration")) {
8221
8495
  const updatedContent = existingContent + "\n\n" + rule.content;
8222
- fs3.writeFileSync(filePath, updatedContent);
8496
+ fs4.writeFileSync(filePath, updatedContent);
8223
8497
  results.push({ editor, filename: rule.filename, status: "appended to existing" });
8224
8498
  } else {
8225
- fs3.writeFileSync(filePath, rule.content);
8499
+ fs4.writeFileSync(filePath, rule.content);
8226
8500
  results.push({ editor, filename: rule.filename, status: "created" });
8227
8501
  }
8228
8502
  } catch (err) {
@@ -8510,6 +8784,304 @@ This saves ~80% tokens compared to including full chat history.`,
8510
8784
  };
8511
8785
  }
8512
8786
  );
8787
+ registerTool(
8788
+ "slack_stats",
8789
+ {
8790
+ title: "Slack overview stats",
8791
+ description: `Get Slack integration statistics and overview for a workspace.
8792
+ Returns: total messages, threads, active users, channel stats, activity trends, and sync status.
8793
+ Use this to understand Slack activity and engagement patterns.`,
8794
+ inputSchema: external_exports.object({
8795
+ workspace_id: external_exports.string().uuid().optional(),
8796
+ days: external_exports.number().optional().describe("Number of days to include in stats (default: 30)")
8797
+ })
8798
+ },
8799
+ async (input) => {
8800
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8801
+ if (!workspaceId) {
8802
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8803
+ }
8804
+ const result = await client.slackStats({ workspace_id: workspaceId, days: input.days });
8805
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8806
+ }
8807
+ );
8808
+ registerTool(
8809
+ "slack_channels",
8810
+ {
8811
+ title: "List Slack channels",
8812
+ description: `Get synced Slack channels with statistics for a workspace.
8813
+ Returns: channel names, message counts, thread counts, and last activity timestamps.`,
8814
+ inputSchema: external_exports.object({
8815
+ workspace_id: external_exports.string().uuid().optional()
8816
+ })
8817
+ },
8818
+ async (input) => {
8819
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8820
+ if (!workspaceId) {
8821
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8822
+ }
8823
+ const result = await client.slackChannels({ workspace_id: workspaceId });
8824
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8825
+ }
8826
+ );
8827
+ registerTool(
8828
+ "slack_contributors",
8829
+ {
8830
+ title: "Slack top contributors",
8831
+ description: `Get top Slack contributors for a workspace.
8832
+ Returns: user profiles with message counts, sorted by activity level.`,
8833
+ inputSchema: external_exports.object({
8834
+ workspace_id: external_exports.string().uuid().optional(),
8835
+ limit: external_exports.number().optional().describe("Maximum contributors to return (default: 20)")
8836
+ })
8837
+ },
8838
+ async (input) => {
8839
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8840
+ if (!workspaceId) {
8841
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8842
+ }
8843
+ const result = await client.slackContributors({ workspace_id: workspaceId, limit: input.limit });
8844
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8845
+ }
8846
+ );
8847
+ registerTool(
8848
+ "slack_activity",
8849
+ {
8850
+ title: "Slack activity feed",
8851
+ description: `Get recent Slack activity feed for a workspace.
8852
+ Returns: messages with user info, reactions, replies, and timestamps.
8853
+ Can filter by channel.`,
8854
+ inputSchema: external_exports.object({
8855
+ workspace_id: external_exports.string().uuid().optional(),
8856
+ limit: external_exports.number().optional().describe("Maximum messages to return (default: 50)"),
8857
+ offset: external_exports.number().optional().describe("Pagination offset"),
8858
+ channel_id: external_exports.string().optional().describe("Filter by specific channel ID")
8859
+ })
8860
+ },
8861
+ async (input) => {
8862
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8863
+ if (!workspaceId) {
8864
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8865
+ }
8866
+ const result = await client.slackActivity({
8867
+ workspace_id: workspaceId,
8868
+ limit: input.limit,
8869
+ offset: input.offset,
8870
+ channel_id: input.channel_id
8871
+ });
8872
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8873
+ }
8874
+ );
8875
+ registerTool(
8876
+ "slack_discussions",
8877
+ {
8878
+ title: "Slack key discussions",
8879
+ description: `Get high-engagement Slack discussions/threads for a workspace.
8880
+ Returns: threads with high reply/reaction counts, sorted by engagement.
8881
+ Useful for finding important conversations and decisions.`,
8882
+ inputSchema: external_exports.object({
8883
+ workspace_id: external_exports.string().uuid().optional(),
8884
+ limit: external_exports.number().optional().describe("Maximum discussions to return (default: 20)")
8885
+ })
8886
+ },
8887
+ async (input) => {
8888
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8889
+ if (!workspaceId) {
8890
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8891
+ }
8892
+ const result = await client.slackDiscussions({ workspace_id: workspaceId, limit: input.limit });
8893
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8894
+ }
8895
+ );
8896
+ registerTool(
8897
+ "slack_search",
8898
+ {
8899
+ title: "Search Slack messages",
8900
+ description: `Search Slack messages for a workspace.
8901
+ Returns: matching messages with channel, user, and engagement info.
8902
+ Use this to find specific conversations or topics.`,
8903
+ inputSchema: external_exports.object({
8904
+ workspace_id: external_exports.string().uuid().optional(),
8905
+ q: external_exports.string().describe("Search query"),
8906
+ limit: external_exports.number().optional().describe("Maximum results (default: 50)")
8907
+ })
8908
+ },
8909
+ async (input) => {
8910
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8911
+ if (!workspaceId) {
8912
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8913
+ }
8914
+ const result = await client.slackSearch({ workspace_id: workspaceId, q: input.q, limit: input.limit });
8915
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8916
+ }
8917
+ );
8918
+ registerTool(
8919
+ "slack_sync_users",
8920
+ {
8921
+ title: "Sync Slack users",
8922
+ description: `Trigger a sync of Slack user profiles for a workspace.
8923
+ This fetches the latest user info from Slack and updates local profiles.
8924
+ Also auto-maps Slack users to ContextStream users by email.`,
8925
+ inputSchema: external_exports.object({
8926
+ workspace_id: external_exports.string().uuid().optional()
8927
+ })
8928
+ },
8929
+ async (input) => {
8930
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8931
+ if (!workspaceId) {
8932
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8933
+ }
8934
+ const result = await client.slackSyncUsers({ workspace_id: workspaceId });
8935
+ return {
8936
+ content: [{
8937
+ type: "text",
8938
+ text: `\u2705 Synced ${result.synced_users} Slack users, auto-mapped ${result.auto_mapped} by email.`
8939
+ }],
8940
+ structuredContent: toStructured(result)
8941
+ };
8942
+ }
8943
+ );
8944
+ registerTool(
8945
+ "github_stats",
8946
+ {
8947
+ title: "GitHub overview stats",
8948
+ description: `Get GitHub integration statistics and overview for a workspace.
8949
+ Returns: total issues, PRs, releases, comments, repository stats, activity trends, and sync status.
8950
+ Use this to understand GitHub activity and engagement patterns across synced repositories.`,
8951
+ inputSchema: external_exports.object({
8952
+ workspace_id: external_exports.string().uuid().optional()
8953
+ })
8954
+ },
8955
+ async (input) => {
8956
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8957
+ if (!workspaceId) {
8958
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8959
+ }
8960
+ const result = await client.githubStats({ workspace_id: workspaceId });
8961
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8962
+ }
8963
+ );
8964
+ registerTool(
8965
+ "github_repos",
8966
+ {
8967
+ title: "List GitHub repositories",
8968
+ description: `Get synced GitHub repositories with statistics for a workspace.
8969
+ Returns: repository names with issue, PR, release, and comment counts, plus last activity timestamps.`,
8970
+ inputSchema: external_exports.object({
8971
+ workspace_id: external_exports.string().uuid().optional()
8972
+ })
8973
+ },
8974
+ async (input) => {
8975
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8976
+ if (!workspaceId) {
8977
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8978
+ }
8979
+ const result = await client.githubRepos({ workspace_id: workspaceId });
8980
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8981
+ }
8982
+ );
8983
+ registerTool(
8984
+ "github_contributors",
8985
+ {
8986
+ title: "GitHub top contributors",
8987
+ description: `Get top GitHub contributors for a workspace.
8988
+ Returns: usernames with contribution counts, sorted by activity level.`,
8989
+ inputSchema: external_exports.object({
8990
+ workspace_id: external_exports.string().uuid().optional(),
8991
+ limit: external_exports.number().optional().describe("Maximum contributors to return (default: 20)")
8992
+ })
8993
+ },
8994
+ async (input) => {
8995
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
8996
+ if (!workspaceId) {
8997
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
8998
+ }
8999
+ const result = await client.githubContributors({ workspace_id: workspaceId, limit: input.limit });
9000
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
9001
+ }
9002
+ );
9003
+ registerTool(
9004
+ "github_activity",
9005
+ {
9006
+ title: "GitHub activity feed",
9007
+ description: `Get recent GitHub activity feed for a workspace.
9008
+ Returns: issues, PRs, releases, and comments with details like state, author, labels.
9009
+ Can filter by repository or type (issue, pull_request, release, comment).`,
9010
+ inputSchema: external_exports.object({
9011
+ workspace_id: external_exports.string().uuid().optional(),
9012
+ limit: external_exports.number().optional().describe("Maximum items to return (default: 50)"),
9013
+ offset: external_exports.number().optional().describe("Pagination offset"),
9014
+ repo: external_exports.string().optional().describe("Filter by repository name"),
9015
+ type: external_exports.enum(["issue", "pull_request", "release", "comment"]).optional().describe("Filter by item type")
9016
+ })
9017
+ },
9018
+ async (input) => {
9019
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
9020
+ if (!workspaceId) {
9021
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
9022
+ }
9023
+ const result = await client.githubActivity({
9024
+ workspace_id: workspaceId,
9025
+ limit: input.limit,
9026
+ offset: input.offset,
9027
+ repo: input.repo,
9028
+ type: input.type
9029
+ });
9030
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
9031
+ }
9032
+ );
9033
+ registerTool(
9034
+ "github_issues",
9035
+ {
9036
+ title: "GitHub issues and PRs",
9037
+ description: `Get GitHub issues and pull requests for a workspace.
9038
+ Returns: issues/PRs with title, state, author, labels, comment count.
9039
+ Can filter by state (open/closed) or repository.`,
9040
+ inputSchema: external_exports.object({
9041
+ workspace_id: external_exports.string().uuid().optional(),
9042
+ limit: external_exports.number().optional().describe("Maximum items to return (default: 50)"),
9043
+ offset: external_exports.number().optional().describe("Pagination offset"),
9044
+ state: external_exports.enum(["open", "closed"]).optional().describe("Filter by state"),
9045
+ repo: external_exports.string().optional().describe("Filter by repository name")
9046
+ })
9047
+ },
9048
+ async (input) => {
9049
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
9050
+ if (!workspaceId) {
9051
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
9052
+ }
9053
+ const result = await client.githubIssues({
9054
+ workspace_id: workspaceId,
9055
+ limit: input.limit,
9056
+ offset: input.offset,
9057
+ state: input.state,
9058
+ repo: input.repo
9059
+ });
9060
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
9061
+ }
9062
+ );
9063
+ registerTool(
9064
+ "github_search",
9065
+ {
9066
+ title: "Search GitHub content",
9067
+ description: `Search GitHub issues, PRs, and comments for a workspace.
9068
+ Returns: matching items with repository, title, state, and content preview.
9069
+ Use this to find specific issues, PRs, or discussions.`,
9070
+ inputSchema: external_exports.object({
9071
+ workspace_id: external_exports.string().uuid().optional(),
9072
+ q: external_exports.string().describe("Search query"),
9073
+ limit: external_exports.number().optional().describe("Maximum results (default: 50)")
9074
+ })
9075
+ },
9076
+ async (input) => {
9077
+ const workspaceId = resolveWorkspaceId(input.workspace_id);
9078
+ if (!workspaceId) {
9079
+ return errorResult("Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly.");
9080
+ }
9081
+ const result = await client.githubSearch({ workspace_id: workspaceId, q: input.q, limit: input.limit });
9082
+ return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
9083
+ }
9084
+ );
8513
9085
  }
8514
9086
 
8515
9087
  // src/resources.ts
@@ -8594,8 +9166,8 @@ var SessionManager = class {
8594
9166
  /**
8595
9167
  * Set the folder path hint (can be passed from tools that know the workspace path)
8596
9168
  */
8597
- setFolderPath(path3) {
8598
- this.folderPath = path3;
9169
+ setFolderPath(path6) {
9170
+ this.folderPath = path6;
8599
9171
  }
8600
9172
  /**
8601
9173
  * Mark that context_smart has been called in this session
@@ -8662,11 +9234,11 @@ var SessionManager = class {
8662
9234
  }
8663
9235
  if (this.ideRoots.length === 0) {
8664
9236
  const cwd = process.cwd();
8665
- const fs3 = await import("fs");
9237
+ const fs4 = await import("fs");
8666
9238
  const projectIndicators = [".git", "package.json", "Cargo.toml", "pyproject.toml", ".contextstream"];
8667
9239
  const hasProjectIndicator = projectIndicators.some((f) => {
8668
9240
  try {
8669
- return fs3.existsSync(`${cwd}/${f}`);
9241
+ return fs4.existsSync(`${cwd}/${f}`);
8670
9242
  } catch {
8671
9243
  return false;
8672
9244
  }
@@ -8844,11 +9416,716 @@ var SessionManager = class {
8844
9416
 
8845
9417
  // src/index.ts
8846
9418
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
8847
- import { homedir } from "os";
8848
- import { join as join3 } from "path";
9419
+ import { homedir as homedir2 } from "os";
9420
+ import { join as join5 } from "path";
9421
+
9422
+ // src/setup.ts
9423
+ import * as fs3 from "node:fs/promises";
9424
+ import * as path5 from "node:path";
9425
+ import { homedir } from "node:os";
9426
+ import { stdin, stdout } from "node:process";
9427
+ import { createInterface } from "node:readline/promises";
9428
+ var EDITOR_LABELS = {
9429
+ codex: "Codex CLI",
9430
+ claude: "Claude Code",
9431
+ cursor: "Cursor / VS Code",
9432
+ windsurf: "Windsurf",
9433
+ cline: "Cline",
9434
+ kilo: "Kilo Code",
9435
+ roo: "Roo Code",
9436
+ aider: "Aider"
9437
+ };
9438
+ function normalizeInput(value) {
9439
+ return value.trim();
9440
+ }
9441
+ function maskApiKey(apiKey) {
9442
+ const trimmed = apiKey.trim();
9443
+ if (trimmed.length <= 8) return "********";
9444
+ return `${trimmed.slice(0, 4)}\u2026${trimmed.slice(-4)}`;
9445
+ }
9446
+ function parseNumberList(input, max) {
9447
+ const cleaned = input.trim().toLowerCase();
9448
+ if (!cleaned) return [];
9449
+ if (cleaned === "all" || cleaned === "*") {
9450
+ return Array.from({ length: max }, (_, i) => i + 1);
9451
+ }
9452
+ const parts = cleaned.split(/[, ]+/).filter(Boolean);
9453
+ const out = /* @__PURE__ */ new Set();
9454
+ for (const part of parts) {
9455
+ const n = Number.parseInt(part, 10);
9456
+ if (Number.isFinite(n) && n >= 1 && n <= max) out.add(n);
9457
+ }
9458
+ return [...out].sort((a, b) => a - b);
9459
+ }
9460
+ async function fileExists(filePath) {
9461
+ try {
9462
+ await fs3.stat(filePath);
9463
+ return true;
9464
+ } catch {
9465
+ return false;
9466
+ }
9467
+ }
9468
+ async function upsertTextFile(filePath, content, marker) {
9469
+ await fs3.mkdir(path5.dirname(filePath), { recursive: true });
9470
+ const exists = await fileExists(filePath);
9471
+ if (!exists) {
9472
+ await fs3.writeFile(filePath, content, "utf8");
9473
+ return "created";
9474
+ }
9475
+ const existing = await fs3.readFile(filePath, "utf8").catch(() => "");
9476
+ if (existing.includes(marker)) return "skipped";
9477
+ const joined = existing.trimEnd() + "\n\n" + content.trim() + "\n";
9478
+ await fs3.writeFile(filePath, joined, "utf8");
9479
+ return "appended";
9480
+ }
9481
+ function globalRulesPathForEditor(editor) {
9482
+ const home = homedir();
9483
+ switch (editor) {
9484
+ case "codex":
9485
+ return path5.join(home, ".codex", "AGENTS.md");
9486
+ case "claude":
9487
+ return path5.join(home, ".claude", "CLAUDE.md");
9488
+ case "windsurf":
9489
+ return path5.join(home, ".codeium", "windsurf", "memories", "global_rules.md");
9490
+ case "cline":
9491
+ return path5.join(home, "Documents", "Cline", "Rules", "contextstream.md");
9492
+ case "kilo":
9493
+ return path5.join(home, ".kilocode", "rules", "contextstream.md");
9494
+ case "roo":
9495
+ return path5.join(home, ".roo", "rules", "contextstream.md");
9496
+ case "aider":
9497
+ return path5.join(home, ".aider.conf.yml");
9498
+ case "cursor":
9499
+ return null;
9500
+ default:
9501
+ return null;
9502
+ }
9503
+ }
9504
+ function buildContextStreamMcpServer(params) {
9505
+ return {
9506
+ command: "npx",
9507
+ args: ["-y", "@contextstream/mcp-server"],
9508
+ env: {
9509
+ CONTEXTSTREAM_API_URL: params.apiUrl,
9510
+ CONTEXTSTREAM_API_KEY: params.apiKey
9511
+ }
9512
+ };
9513
+ }
9514
+ function buildContextStreamVsCodeServer(params) {
9515
+ return {
9516
+ type: "stdio",
9517
+ command: "npx",
9518
+ args: ["-y", "@contextstream/mcp-server"],
9519
+ env: {
9520
+ CONTEXTSTREAM_API_URL: params.apiUrl,
9521
+ CONTEXTSTREAM_API_KEY: params.apiKey
9522
+ }
9523
+ };
9524
+ }
9525
+ function stripJsonComments(input) {
9526
+ return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
9527
+ }
9528
+ function tryParseJsonLike(raw) {
9529
+ const trimmed = raw.replace(/^\uFEFF/, "").trim();
9530
+ if (!trimmed) return { ok: true, value: {} };
9531
+ try {
9532
+ return { ok: true, value: JSON.parse(trimmed) };
9533
+ } catch {
9534
+ try {
9535
+ const noComments = stripJsonComments(trimmed);
9536
+ const noTrailingCommas = noComments.replace(/,(\s*[}\]])/g, "$1");
9537
+ return { ok: true, value: JSON.parse(noTrailingCommas) };
9538
+ } catch (err) {
9539
+ return { ok: false, error: err?.message || "Invalid JSON" };
9540
+ }
9541
+ }
9542
+ }
9543
+ async function upsertJsonMcpConfig(filePath, server) {
9544
+ await fs3.mkdir(path5.dirname(filePath), { recursive: true });
9545
+ const exists = await fileExists(filePath);
9546
+ let root = {};
9547
+ if (exists) {
9548
+ const raw = await fs3.readFile(filePath, "utf8").catch(() => "");
9549
+ const parsed = tryParseJsonLike(raw);
9550
+ if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
9551
+ root = parsed.value;
9552
+ }
9553
+ if (!root || typeof root !== "object" || Array.isArray(root)) root = {};
9554
+ if (!root.mcpServers || typeof root.mcpServers !== "object" || Array.isArray(root.mcpServers)) root.mcpServers = {};
9555
+ const before = JSON.stringify(root.mcpServers.contextstream ?? null);
9556
+ root.mcpServers.contextstream = server;
9557
+ const after = JSON.stringify(root.mcpServers.contextstream ?? null);
9558
+ await fs3.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
9559
+ if (!exists) return "created";
9560
+ return before === after ? "skipped" : "updated";
9561
+ }
9562
+ async function upsertJsonVsCodeMcpConfig(filePath, server) {
9563
+ await fs3.mkdir(path5.dirname(filePath), { recursive: true });
9564
+ const exists = await fileExists(filePath);
9565
+ let root = {};
9566
+ if (exists) {
9567
+ const raw = await fs3.readFile(filePath, "utf8").catch(() => "");
9568
+ const parsed = tryParseJsonLike(raw);
9569
+ if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
9570
+ root = parsed.value;
9571
+ }
9572
+ if (!root || typeof root !== "object" || Array.isArray(root)) root = {};
9573
+ if (!root.servers || typeof root.servers !== "object" || Array.isArray(root.servers)) root.servers = {};
9574
+ const before = JSON.stringify(root.servers.contextstream ?? null);
9575
+ root.servers.contextstream = server;
9576
+ const after = JSON.stringify(root.servers.contextstream ?? null);
9577
+ await fs3.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
9578
+ if (!exists) return "created";
9579
+ return before === after ? "skipped" : "updated";
9580
+ }
9581
+ function claudeDesktopConfigPath() {
9582
+ const home = homedir();
9583
+ if (process.platform === "darwin") {
9584
+ return path5.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
9585
+ }
9586
+ if (process.platform === "win32") {
9587
+ const appData = process.env.APPDATA || path5.join(home, "AppData", "Roaming");
9588
+ return path5.join(appData, "Claude", "claude_desktop_config.json");
9589
+ }
9590
+ return null;
9591
+ }
9592
+ async function upsertCodexTomlConfig(filePath, params) {
9593
+ await fs3.mkdir(path5.dirname(filePath), { recursive: true });
9594
+ const exists = await fileExists(filePath);
9595
+ const existing = exists ? await fs3.readFile(filePath, "utf8").catch(() => "") : "";
9596
+ const marker = "[mcp_servers.contextstream]";
9597
+ const envMarker = "[mcp_servers.contextstream.env]";
9598
+ const block = `
9599
+
9600
+ # ContextStream MCP server
9601
+ [mcp_servers.contextstream]
9602
+ command = "npx"
9603
+ args = ["-y", "@contextstream/mcp-server"]
9604
+
9605
+ [mcp_servers.contextstream.env]
9606
+ CONTEXTSTREAM_API_URL = "${params.apiUrl}"
9607
+ CONTEXTSTREAM_API_KEY = "${params.apiKey}"
9608
+ `;
9609
+ if (!exists) {
9610
+ await fs3.writeFile(filePath, block.trimStart(), "utf8");
9611
+ return "created";
9612
+ }
9613
+ if (!existing.includes(marker)) {
9614
+ await fs3.writeFile(filePath, existing.trimEnd() + block, "utf8");
9615
+ return "updated";
9616
+ }
9617
+ if (!existing.includes(envMarker)) {
9618
+ await fs3.writeFile(filePath, existing.trimEnd() + "\n\n" + envMarker + `
9619
+ CONTEXTSTREAM_API_URL = "${params.apiUrl}"
9620
+ CONTEXTSTREAM_API_KEY = "${params.apiKey}"
9621
+ `, "utf8");
9622
+ return "updated";
9623
+ }
9624
+ const lines = existing.split(/\r?\n/);
9625
+ const out = [];
9626
+ let inEnv = false;
9627
+ let sawUrl = false;
9628
+ let sawKey = false;
9629
+ for (const line of lines) {
9630
+ const trimmed = line.trim();
9631
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
9632
+ if (inEnv && trimmed !== envMarker) {
9633
+ if (!sawUrl) out.push(`CONTEXTSTREAM_API_URL = "${params.apiUrl}"`);
9634
+ if (!sawKey) out.push(`CONTEXTSTREAM_API_KEY = "${params.apiKey}"`);
9635
+ inEnv = false;
9636
+ }
9637
+ if (trimmed === envMarker) inEnv = true;
9638
+ out.push(line);
9639
+ continue;
9640
+ }
9641
+ if (inEnv && /^\s*CONTEXTSTREAM_API_URL\s*=/.test(line)) {
9642
+ out.push(`CONTEXTSTREAM_API_URL = "${params.apiUrl}"`);
9643
+ sawUrl = true;
9644
+ continue;
9645
+ }
9646
+ if (inEnv && /^\s*CONTEXTSTREAM_API_KEY\s*=/.test(line)) {
9647
+ out.push(`CONTEXTSTREAM_API_KEY = "${params.apiKey}"`);
9648
+ sawKey = true;
9649
+ continue;
9650
+ }
9651
+ out.push(line);
9652
+ }
9653
+ if (inEnv) {
9654
+ if (!sawUrl) out.push(`CONTEXTSTREAM_API_URL = "${params.apiUrl}"`);
9655
+ if (!sawKey) out.push(`CONTEXTSTREAM_API_KEY = "${params.apiKey}"`);
9656
+ }
9657
+ const updated = out.join("\n");
9658
+ if (updated === existing) return "skipped";
9659
+ await fs3.writeFile(filePath, updated, "utf8");
9660
+ return "updated";
9661
+ }
9662
+ async function discoverProjectsUnderFolder(parentFolder) {
9663
+ const entries = await fs3.readdir(parentFolder, { withFileTypes: true });
9664
+ const candidates = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => path5.join(parentFolder, e.name));
9665
+ const projects = [];
9666
+ for (const dir of candidates) {
9667
+ const hasGit = await fileExists(path5.join(dir, ".git"));
9668
+ const hasPkg = await fileExists(path5.join(dir, "package.json"));
9669
+ const hasCargo = await fileExists(path5.join(dir, "Cargo.toml"));
9670
+ const hasPyProject = await fileExists(path5.join(dir, "pyproject.toml"));
9671
+ if (hasGit || hasPkg || hasCargo || hasPyProject) projects.push(dir);
9672
+ }
9673
+ return projects;
9674
+ }
9675
+ function buildClientConfig(params) {
9676
+ return {
9677
+ apiUrl: params.apiUrl,
9678
+ apiKey: params.apiKey,
9679
+ jwt: params.jwt,
9680
+ defaultWorkspaceId: void 0,
9681
+ defaultProjectId: void 0,
9682
+ userAgent: `contextstream-mcp/setup/${VERSION}`
9683
+ };
9684
+ }
9685
+ async function runSetupWizard(args) {
9686
+ const dryRun = args.includes("--dry-run");
9687
+ const rl = createInterface({ input: stdin, output: stdout });
9688
+ const writeActions = [];
9689
+ try {
9690
+ console.log(`ContextStream Setup Wizard (v${VERSION})`);
9691
+ console.log("This configures ContextStream MCP + rules for your AI editor(s).");
9692
+ if (dryRun) console.log("DRY RUN: no files will be written.\n");
9693
+ else console.log("");
9694
+ const apiUrlDefault = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
9695
+ const apiUrl = normalizeInput(
9696
+ await rl.question(`ContextStream API URL [${apiUrlDefault}]: `)
9697
+ ) || apiUrlDefault;
9698
+ let apiKey = normalizeInput(process.env.CONTEXTSTREAM_API_KEY || "");
9699
+ if (apiKey) {
9700
+ const confirm = normalizeInput(
9701
+ await rl.question(`Use CONTEXTSTREAM_API_KEY from environment (${maskApiKey(apiKey)})? [Y/n]: `)
9702
+ );
9703
+ if (confirm.toLowerCase() === "n" || confirm.toLowerCase() === "no") apiKey = "";
9704
+ }
9705
+ if (!apiKey) {
9706
+ console.log("\nAuthentication:");
9707
+ console.log(" 1) Browser login (recommended)");
9708
+ console.log(" 2) Paste an API key");
9709
+ const authChoice = normalizeInput(await rl.question("Choose [1/2] (default 1): ")) || "1";
9710
+ if (authChoice === "2") {
9711
+ console.log("\nYou need a ContextStream API key to continue.");
9712
+ console.log("Create one here (then paste it): https://app.contextstream.io/settings/api-keys\n");
9713
+ apiKey = normalizeInput(await rl.question("CONTEXTSTREAM_API_KEY: "));
9714
+ } else {
9715
+ const anonClient = new ContextStreamClient(buildClientConfig({ apiUrl }));
9716
+ let device;
9717
+ try {
9718
+ device = await anonClient.startDeviceLogin();
9719
+ } catch (err) {
9720
+ const message = err instanceof HttpError ? `${err.status} ${err.code}: ${err.message}` : err?.message || String(err);
9721
+ throw new Error(
9722
+ `Browser login is not available on this API. Please use an API key instead. (${message})`
9723
+ );
9724
+ }
9725
+ const verificationUrl = typeof device?.verification_uri_complete === "string" ? device.verification_uri_complete : typeof device?.verification_uri === "string" && typeof device?.user_code === "string" ? `${device.verification_uri}?user_code=${device.user_code}` : void 0;
9726
+ if (!verificationUrl || typeof device?.device_code !== "string" || typeof device?.expires_in !== "number") {
9727
+ throw new Error("Browser login returned an unexpected response.");
9728
+ }
9729
+ console.log("\nOpen this URL to sign in and approve the setup wizard:");
9730
+ console.log(verificationUrl);
9731
+ if (typeof device?.user_code === "string") {
9732
+ console.log(`
9733
+ Code: ${device.user_code}`);
9734
+ }
9735
+ console.log("\nWaiting for approval...");
9736
+ const startedAt = Date.now();
9737
+ const expiresMs = device.expires_in * 1e3;
9738
+ const deviceCode = device.device_code;
9739
+ let accessToken;
9740
+ while (Date.now() - startedAt < expiresMs) {
9741
+ let poll;
9742
+ try {
9743
+ poll = await anonClient.pollDeviceLogin({ device_code: deviceCode });
9744
+ } catch (err) {
9745
+ const message = err instanceof HttpError ? `${err.status} ${err.code}: ${err.message}` : err?.message || String(err);
9746
+ throw new Error(`Browser login failed while polling. (${message})`);
9747
+ }
9748
+ if (poll && poll.status === "authorized" && typeof poll.access_token === "string") {
9749
+ accessToken = poll.access_token;
9750
+ break;
9751
+ }
9752
+ if (poll && poll.status === "pending") {
9753
+ const intervalSeconds = typeof poll.interval === "number" ? poll.interval : 5;
9754
+ const waitMs = Math.max(1, intervalSeconds) * 1e3;
9755
+ await new Promise((resolve2) => setTimeout(resolve2, waitMs));
9756
+ continue;
9757
+ }
9758
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
9759
+ }
9760
+ if (!accessToken) {
9761
+ throw new Error("Browser login expired or was not approved in time. Please run setup again.");
9762
+ }
9763
+ const jwtClient = new ContextStreamClient(buildClientConfig({ apiUrl, jwt: accessToken }));
9764
+ const keyName = `setup-wizard-${Date.now()}`;
9765
+ let createdKey;
9766
+ try {
9767
+ createdKey = await jwtClient.createApiKey({ name: keyName });
9768
+ } catch (err) {
9769
+ const message = err instanceof HttpError ? `${err.status} ${err.code}: ${err.message}` : err?.message || String(err);
9770
+ throw new Error(`Login succeeded but API key creation failed. (${message})`);
9771
+ }
9772
+ if (typeof createdKey?.secret_key !== "string" || !createdKey.secret_key.trim()) {
9773
+ throw new Error("API key creation returned an unexpected response.");
9774
+ }
9775
+ apiKey = createdKey.secret_key.trim();
9776
+ console.log(`
9777
+ Created API key: ${maskApiKey(apiKey)}
9778
+ `);
9779
+ }
9780
+ }
9781
+ const client = new ContextStreamClient(buildClientConfig({ apiUrl, apiKey }));
9782
+ let me;
9783
+ try {
9784
+ me = await client.me();
9785
+ } catch (err) {
9786
+ const message = err instanceof HttpError ? `${err.status} ${err.code}: ${err.message}` : err?.message || String(err);
9787
+ throw new Error(`Authentication failed. Check your API key. (${message})`);
9788
+ }
9789
+ const email = typeof me?.data?.email === "string" ? me.data.email : typeof me?.email === "string" ? me.email : void 0;
9790
+ console.log(`Authenticated as: ${email || "unknown user"} (${maskApiKey(apiKey)})
9791
+ `);
9792
+ let workspaceId;
9793
+ let workspaceName;
9794
+ console.log("Workspace setup:");
9795
+ console.log(" 1) Create a new workspace");
9796
+ console.log(" 2) Select an existing workspace");
9797
+ console.log(" 3) Skip (rules only, no workspace mapping)");
9798
+ const wsChoice = normalizeInput(await rl.question("Choose [1/2/3] (default 2): ")) || "2";
9799
+ if (wsChoice === "1") {
9800
+ const name = normalizeInput(await rl.question("Workspace name: "));
9801
+ if (!name) throw new Error("Workspace name is required.");
9802
+ const description = normalizeInput(await rl.question("Workspace description (optional): "));
9803
+ let visibility = "private";
9804
+ while (true) {
9805
+ const raw = normalizeInput(await rl.question("Visibility [private/team/org] (default private): ")) || "private";
9806
+ const normalized = raw.trim().toLowerCase() === "public" ? "org" : raw.trim().toLowerCase();
9807
+ if (normalized === "private" || normalized === "team" || normalized === "org") {
9808
+ visibility = normalized;
9809
+ break;
9810
+ }
9811
+ console.log("Invalid visibility. Choose: private, team, org.");
9812
+ }
9813
+ if (!dryRun) {
9814
+ const created = await client.createWorkspace({ name, description: description || void 0, visibility });
9815
+ workspaceId = typeof created?.id === "string" ? created.id : void 0;
9816
+ workspaceName = typeof created?.name === "string" ? created.name : name;
9817
+ } else {
9818
+ workspaceId = "dry-run";
9819
+ workspaceName = name;
9820
+ }
9821
+ console.log(`Workspace: ${workspaceName}${workspaceId ? ` (${workspaceId})` : ""}
9822
+ `);
9823
+ } else if (wsChoice === "2") {
9824
+ const list = await client.listWorkspaces({ page_size: 50 });
9825
+ const items = Array.isArray(list?.items) ? list.items : Array.isArray(list?.data?.items) ? list.data.items : [];
9826
+ if (items.length === 0) {
9827
+ console.log("No workspaces found. Creating a new one is recommended.\n");
9828
+ } else {
9829
+ items.slice(0, 20).forEach((w, i) => {
9830
+ console.log(` ${i + 1}) ${w.name || "Untitled"}${w.id ? ` (${w.id})` : ""}`);
9831
+ });
9832
+ const idxRaw = normalizeInput(await rl.question("Select workspace number (or blank to skip): "));
9833
+ if (idxRaw) {
9834
+ const idx = Number.parseInt(idxRaw, 10);
9835
+ const selected = Number.isFinite(idx) ? items[idx - 1] : void 0;
9836
+ if (selected?.id) {
9837
+ workspaceId = selected.id;
9838
+ workspaceName = selected.name;
9839
+ }
9840
+ }
9841
+ }
9842
+ }
9843
+ console.log("Rule verbosity:");
9844
+ console.log(" 1) Minimal (recommended)");
9845
+ console.log(" 2) Full (more context and guidance, more tokens)");
9846
+ const modeChoice = normalizeInput(await rl.question("Choose [1/2] (default 1): ")) || "1";
9847
+ const mode = modeChoice === "2" ? "full" : "minimal";
9848
+ const editors = ["codex", "claude", "cursor", "windsurf", "cline", "kilo", "roo", "aider"];
9849
+ console.log('\nSelect editors to configure (comma-separated numbers, or "all"):');
9850
+ editors.forEach((e, i) => console.log(` ${i + 1}) ${EDITOR_LABELS[e]}`));
9851
+ const selectedRaw = normalizeInput(await rl.question("Editors [all]: ")) || "all";
9852
+ const selectedNums = parseNumberList(selectedRaw, editors.length);
9853
+ const selectedEditors = selectedNums.length ? selectedNums.map((n) => editors[n - 1]) : editors;
9854
+ console.log("\nInstall rules as:");
9855
+ console.log(" 1) Global");
9856
+ console.log(" 2) Project");
9857
+ console.log(" 3) Both");
9858
+ const scopeChoice = normalizeInput(await rl.question("Choose [1/2/3] (default 3): ")) || "3";
9859
+ const scope = scopeChoice === "1" ? "global" : scopeChoice === "2" ? "project" : "both";
9860
+ console.log("\nInstall MCP server config as:");
9861
+ console.log(" 1) Global");
9862
+ console.log(" 2) Project");
9863
+ console.log(" 3) Both");
9864
+ console.log(" 4) Skip (rules only)");
9865
+ const mcpChoice = normalizeInput(await rl.question("Choose [1/2/3/4] (default 3): ")) || "3";
9866
+ const mcpScope = mcpChoice === "4" ? "skip" : mcpChoice === "1" ? "global" : mcpChoice === "2" ? "project" : "both";
9867
+ const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey });
9868
+ const vsCodeServer = buildContextStreamVsCodeServer({ apiUrl, apiKey });
9869
+ if (mcpScope === "global" || mcpScope === "both") {
9870
+ console.log("\nInstalling global MCP config...");
9871
+ for (const editor of selectedEditors) {
9872
+ try {
9873
+ if (editor === "codex") {
9874
+ const filePath = path5.join(homedir(), ".codex", "config.toml");
9875
+ if (dryRun) {
9876
+ writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
9877
+ console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
9878
+ continue;
9879
+ }
9880
+ const status = await upsertCodexTomlConfig(filePath, { apiUrl, apiKey });
9881
+ writeActions.push({ kind: "mcp-config", target: filePath, status });
9882
+ console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
9883
+ continue;
9884
+ }
9885
+ if (editor === "windsurf") {
9886
+ const filePath = path5.join(homedir(), ".codeium", "windsurf", "mcp_config.json");
9887
+ if (dryRun) {
9888
+ writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
9889
+ console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
9890
+ continue;
9891
+ }
9892
+ const status = await upsertJsonMcpConfig(filePath, mcpServer);
9893
+ writeActions.push({ kind: "mcp-config", target: filePath, status });
9894
+ console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
9895
+ continue;
9896
+ }
9897
+ if (editor === "claude") {
9898
+ const desktopPath = claudeDesktopConfigPath();
9899
+ if (desktopPath) {
9900
+ const useDesktop = normalizeInput(await rl.question("Also configure Claude Desktop (GUI app)? [y/N]: ")).toLowerCase() === "y";
9901
+ if (useDesktop) {
9902
+ if (dryRun) {
9903
+ writeActions.push({ kind: "mcp-config", target: desktopPath, status: "dry-run" });
9904
+ console.log(`- Claude Desktop: would update ${desktopPath}`);
9905
+ } else {
9906
+ const status = await upsertJsonMcpConfig(desktopPath, mcpServer);
9907
+ writeActions.push({ kind: "mcp-config", target: desktopPath, status });
9908
+ console.log(`- Claude Desktop: ${status} ${desktopPath}`);
9909
+ }
9910
+ }
9911
+ }
9912
+ console.log("- Claude Code: global MCP config is best done via `claude mcp add --transport stdio ...` (see docs).");
9913
+ console.log(" macOS/Linux: claude mcp add --transport stdio contextstream --scope user --env CONTEXTSTREAM_API_URL=... --env CONTEXTSTREAM_API_KEY=... -- npx -y @contextstream/mcp-server");
9914
+ console.log(" Windows (native): use `cmd /c npx -y @contextstream/mcp-server` after `--` if `npx` is not found.");
9915
+ continue;
9916
+ }
9917
+ if (editor === "cursor") {
9918
+ const filePath = path5.join(homedir(), ".cursor", "mcp.json");
9919
+ if (dryRun) {
9920
+ writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
9921
+ console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
9922
+ continue;
9923
+ }
9924
+ const status = await upsertJsonMcpConfig(filePath, mcpServer);
9925
+ writeActions.push({ kind: "mcp-config", target: filePath, status });
9926
+ console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
9927
+ continue;
9928
+ }
9929
+ if (editor === "cline") {
9930
+ console.log(`- ${EDITOR_LABELS[editor]}: MCP config is managed via the extension UI (skipping global).`);
9931
+ continue;
9932
+ }
9933
+ if (editor === "kilo" || editor === "roo") {
9934
+ console.log(`- ${EDITOR_LABELS[editor]}: project MCP config supported via file; global is managed via the app UI.`);
9935
+ continue;
9936
+ }
9937
+ if (editor === "aider") {
9938
+ console.log(`- ${EDITOR_LABELS[editor]}: no MCP config file to write (rules only).`);
9939
+ continue;
9940
+ }
9941
+ } catch (err) {
9942
+ const message = err instanceof Error ? err.message : String(err);
9943
+ console.log(`- ${EDITOR_LABELS[editor]}: failed to write MCP config: ${message}`);
9944
+ }
9945
+ }
9946
+ }
9947
+ if (scope === "global" || scope === "both") {
9948
+ console.log("\nInstalling global rules...");
9949
+ for (const editor of selectedEditors) {
9950
+ const filePath = globalRulesPathForEditor(editor);
9951
+ if (!filePath) {
9952
+ console.log(`- ${EDITOR_LABELS[editor]}: global rules need manual setup (project rules supported).`);
9953
+ continue;
9954
+ }
9955
+ const rule = generateRuleContent(editor, {
9956
+ workspaceName,
9957
+ workspaceId: workspaceId && workspaceId !== "dry-run" ? workspaceId : void 0,
9958
+ mode
9959
+ });
9960
+ if (!rule) continue;
9961
+ if (dryRun) {
9962
+ writeActions.push({ kind: "rules", target: filePath, status: "dry-run" });
9963
+ console.log(`- ${EDITOR_LABELS[editor]}: would write ${filePath}`);
9964
+ continue;
9965
+ }
9966
+ const status = await upsertTextFile(filePath, rule.content, "ContextStream");
9967
+ writeActions.push({ kind: "rules", target: filePath, status });
9968
+ console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
9969
+ }
9970
+ }
9971
+ const projectPaths = /* @__PURE__ */ new Set();
9972
+ const needsProjects = scope === "project" || scope === "both" || mcpScope === "project" || mcpScope === "both";
9973
+ if (needsProjects) {
9974
+ console.log("\nProject setup...");
9975
+ const addCwd = normalizeInput(await rl.question(`Add current folder as a project? [Y/n] (${process.cwd()}): `));
9976
+ if (addCwd.toLowerCase() !== "n" && addCwd.toLowerCase() !== "no") {
9977
+ projectPaths.add(path5.resolve(process.cwd()));
9978
+ }
9979
+ while (true) {
9980
+ console.log("\n 1) Add another project path");
9981
+ console.log(" 2) Add all projects under a folder");
9982
+ console.log(" 3) Continue");
9983
+ const choice = normalizeInput(await rl.question("Choose [1/2/3] (default 3): ")) || "3";
9984
+ if (choice === "3") break;
9985
+ if (choice === "1") {
9986
+ const p = normalizeInput(await rl.question("Project folder path: "));
9987
+ if (p) projectPaths.add(path5.resolve(p));
9988
+ continue;
9989
+ }
9990
+ if (choice === "2") {
9991
+ const parent = normalizeInput(await rl.question("Parent folder path: "));
9992
+ if (!parent) continue;
9993
+ const parentAbs = path5.resolve(parent);
9994
+ const projects2 = await discoverProjectsUnderFolder(parentAbs);
9995
+ if (projects2.length === 0) {
9996
+ console.log(`No projects detected under ${parentAbs} (looked for .git/package.json/Cargo.toml/pyproject.toml).`);
9997
+ continue;
9998
+ }
9999
+ console.log(`Found ${projects2.length} project(s):`);
10000
+ projects2.slice(0, 25).forEach((p) => console.log(`- ${p}`));
10001
+ if (projects2.length > 25) console.log(`\u2026and ${projects2.length - 25} more`);
10002
+ const confirm = normalizeInput(await rl.question("Add these projects? [Y/n]: "));
10003
+ if (confirm.toLowerCase() === "n" || confirm.toLowerCase() === "no") continue;
10004
+ projects2.forEach((p) => projectPaths.add(p));
10005
+ }
10006
+ }
10007
+ }
10008
+ const projects = [...projectPaths];
10009
+ if (projects.length && needsProjects) {
10010
+ console.log(`
10011
+ Applying to ${projects.length} project(s)...`);
10012
+ }
10013
+ const createParentMapping = !!workspaceId && workspaceId !== "dry-run" && projects.length > 1 && normalizeInput(await rl.question("Also create a parent folder mapping for auto-detection? [y/N]: ")).toLowerCase() === "y";
10014
+ for (const projectPath of projects) {
10015
+ if (workspaceId && workspaceId !== "dry-run" && workspaceName && !dryRun) {
10016
+ try {
10017
+ await client.associateWorkspace({
10018
+ folder_path: projectPath,
10019
+ workspace_id: workspaceId,
10020
+ workspace_name: workspaceName,
10021
+ create_parent_mapping: createParentMapping
10022
+ });
10023
+ writeActions.push({ kind: "workspace-config", target: path5.join(projectPath, ".contextstream", "config.json"), status: "created" });
10024
+ console.log(`- Linked workspace in ${projectPath}`);
10025
+ } catch (err) {
10026
+ const message = err instanceof Error ? err.message : String(err);
10027
+ console.log(`- Failed to link workspace in ${projectPath}: ${message}`);
10028
+ }
10029
+ } else if (workspaceId && workspaceId !== "dry-run" && workspaceName && dryRun) {
10030
+ writeActions.push({ kind: "workspace-config", target: path5.join(projectPath, ".contextstream", "config.json"), status: "dry-run" });
10031
+ }
10032
+ if (mcpScope === "project" || mcpScope === "both") {
10033
+ for (const editor of selectedEditors) {
10034
+ try {
10035
+ if (editor === "cursor") {
10036
+ const cursorPath = path5.join(projectPath, ".cursor", "mcp.json");
10037
+ const vscodePath = path5.join(projectPath, ".vscode", "mcp.json");
10038
+ if (dryRun) {
10039
+ writeActions.push({ kind: "mcp-config", target: cursorPath, status: "dry-run" });
10040
+ writeActions.push({ kind: "mcp-config", target: vscodePath, status: "dry-run" });
10041
+ } else {
10042
+ const status1 = await upsertJsonMcpConfig(cursorPath, mcpServer);
10043
+ const status2 = await upsertJsonVsCodeMcpConfig(vscodePath, vsCodeServer);
10044
+ writeActions.push({ kind: "mcp-config", target: cursorPath, status: status1 });
10045
+ writeActions.push({ kind: "mcp-config", target: vscodePath, status: status2 });
10046
+ }
10047
+ continue;
10048
+ }
10049
+ if (editor === "claude") {
10050
+ const mcpPath = path5.join(projectPath, ".mcp.json");
10051
+ if (dryRun) {
10052
+ writeActions.push({ kind: "mcp-config", target: mcpPath, status: "dry-run" });
10053
+ } else {
10054
+ const status = await upsertJsonMcpConfig(mcpPath, mcpServer);
10055
+ writeActions.push({ kind: "mcp-config", target: mcpPath, status });
10056
+ }
10057
+ continue;
10058
+ }
10059
+ if (editor === "kilo") {
10060
+ const kiloPath = path5.join(projectPath, ".kilocode", "mcp.json");
10061
+ if (dryRun) {
10062
+ writeActions.push({ kind: "mcp-config", target: kiloPath, status: "dry-run" });
10063
+ } else {
10064
+ const status = await upsertJsonMcpConfig(kiloPath, mcpServer);
10065
+ writeActions.push({ kind: "mcp-config", target: kiloPath, status });
10066
+ }
10067
+ continue;
10068
+ }
10069
+ if (editor === "roo") {
10070
+ const rooPath = path5.join(projectPath, ".roo", "mcp.json");
10071
+ if (dryRun) {
10072
+ writeActions.push({ kind: "mcp-config", target: rooPath, status: "dry-run" });
10073
+ } else {
10074
+ const status = await upsertJsonMcpConfig(rooPath, mcpServer);
10075
+ writeActions.push({ kind: "mcp-config", target: rooPath, status });
10076
+ }
10077
+ continue;
10078
+ }
10079
+ } catch (err) {
10080
+ const message = err instanceof Error ? err.message : String(err);
10081
+ console.log(`- Failed to write MCP config for ${EDITOR_LABELS[editor]} in ${projectPath}: ${message}`);
10082
+ }
10083
+ }
10084
+ }
10085
+ for (const editor of selectedEditors) {
10086
+ if (scope !== "project" && scope !== "both") continue;
10087
+ const rule = generateRuleContent(editor, {
10088
+ workspaceName,
10089
+ workspaceId: workspaceId && workspaceId !== "dry-run" ? workspaceId : void 0,
10090
+ projectName: path5.basename(projectPath),
10091
+ mode
10092
+ });
10093
+ if (!rule) continue;
10094
+ const filePath = path5.join(projectPath, rule.filename);
10095
+ if (dryRun) {
10096
+ writeActions.push({ kind: "rules", target: filePath, status: "dry-run" });
10097
+ continue;
10098
+ }
10099
+ try {
10100
+ const status = await upsertTextFile(filePath, rule.content, "ContextStream");
10101
+ writeActions.push({ kind: "rules", target: filePath, status });
10102
+ } catch (err) {
10103
+ const message = err instanceof Error ? err.message : String(err);
10104
+ writeActions.push({ kind: "rules", target: filePath, status: `error: ${message}` });
10105
+ }
10106
+ }
10107
+ }
10108
+ console.log("\nDone.");
10109
+ if (writeActions.length) {
10110
+ const created = writeActions.filter((a) => a.status === "created").length;
10111
+ const appended = writeActions.filter((a) => a.status === "appended").length;
10112
+ const updated = writeActions.filter((a) => a.status === "updated").length;
10113
+ const skipped = writeActions.filter((a) => a.status === "skipped").length;
10114
+ const dry = writeActions.filter((a) => a.status === "dry-run").length;
10115
+ console.log(`Summary: ${created} created, ${updated} updated, ${appended} appended, ${skipped} skipped, ${dry} dry-run.`);
10116
+ }
10117
+ console.log("\nNext steps:");
10118
+ console.log("- Restart your editor/CLI after changing MCP config or rules.");
10119
+ console.log("- If any tools require UI-based MCP setup (e.g. Cline/Kilo/Roo global), follow https://contextstream.io/docs/mcp.");
10120
+ } finally {
10121
+ rl.close();
10122
+ }
10123
+ }
10124
+
10125
+ // src/index.ts
8849
10126
  function showFirstRunMessage() {
8850
- const configDir = join3(homedir(), ".contextstream");
8851
- const starShownFile = join3(configDir, ".star-shown");
10127
+ const configDir = join5(homedir2(), ".contextstream");
10128
+ const starShownFile = join5(configDir, ".star-shown");
8852
10129
  if (existsSync2(starShownFile)) {
8853
10130
  return;
8854
10131
  }
@@ -8877,6 +10154,10 @@ function printHelp() {
8877
10154
  Usage:
8878
10155
  npx -y @contextstream/mcp-server
8879
10156
  contextstream-mcp
10157
+ contextstream-mcp setup
10158
+
10159
+ Commands:
10160
+ setup Interactive onboarding wizard (rules + workspace mapping)
8880
10161
 
8881
10162
  Environment variables:
8882
10163
  CONTEXTSTREAM_API_URL Base API URL (e.g. https://api.contextstream.io)
@@ -8892,6 +10173,9 @@ Examples:
8892
10173
  CONTEXTSTREAM_API_KEY="your_api_key" \\
8893
10174
  npx -y @contextstream/mcp-server
8894
10175
 
10176
+ Setup wizard:
10177
+ npx -y @contextstream/mcp-server setup
10178
+
8895
10179
  Notes:
8896
10180
  - When used from an MCP client (e.g. Codex, Cursor, VS Code),
8897
10181
  set these env vars in the client's MCP server configuration.
@@ -8907,6 +10191,10 @@ async function main() {
8907
10191
  console.log(`contextstream-mcp v${VERSION}`);
8908
10192
  return;
8909
10193
  }
10194
+ if (args[0] === "setup") {
10195
+ await runSetupWizard(args.slice(1));
10196
+ return;
10197
+ }
8910
10198
  const config = loadConfig();
8911
10199
  const client = new ContextStreamClient(config);
8912
10200
  const server = new McpServer({