@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/README.md +98 -3
- package/dist/index.js +1361 -73
- package/dist/test-server.js +4328 -0
- package/package.json +4 -2
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:
|
|
491
|
-
const fullPath = [...
|
|
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,
|
|
607
|
+
constructor(parent, value, path6, key) {
|
|
608
608
|
this._cachedPath = [];
|
|
609
609
|
this.parent = parent;
|
|
610
610
|
this.data = value;
|
|
611
|
-
this._path =
|
|
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((
|
|
4139
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4139
4140
|
}
|
|
4140
|
-
async function request(config,
|
|
4141
|
+
async function request(config, path6, options = {}) {
|
|
4141
4142
|
const { apiUrl, apiKey, jwt, userAgent } = config;
|
|
4142
|
-
const apiPath =
|
|
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
|
|
4412
|
-
if (
|
|
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 !==
|
|
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
|
-
|
|
4499
|
-
|
|
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 ===
|
|
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
|
|
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
|
|
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
|
|
5097
|
+
context.folder_name = rootPath ? path3.basename(rootPath) : void 0;
|
|
5085
5098
|
return context;
|
|
5086
5099
|
}
|
|
5087
5100
|
} else {
|
|
5088
|
-
const folderDisplayName = rootPath
|
|
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
|
|
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 =
|
|
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 =
|
|
5369
|
+
const parentDir = path3.dirname(folder_path);
|
|
5357
5370
|
addGlobalMapping({
|
|
5358
|
-
pattern:
|
|
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
|
|
5845
|
+
const path6 = c.file_path || "file";
|
|
5833
5846
|
const content = c.content?.slice(0, 150) || "";
|
|
5834
|
-
return { path:
|
|
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
|
|
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
|
-
|
|
6429
|
-
${
|
|
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
|
-
|
|
6436
|
-
${
|
|
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
|
-
|
|
6443
|
-
${
|
|
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
|
-
|
|
6450
|
-
${
|
|
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
|
-
|
|
6457
|
-
${
|
|
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
|
-
|
|
6464
|
-
${
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
|
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
|
|
7668
|
-
const
|
|
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 =
|
|
7947
|
+
const filePath = path6.join(input.folder_path, rule.filename);
|
|
7676
7948
|
try {
|
|
7677
7949
|
let existingContent = "";
|
|
7678
7950
|
try {
|
|
7679
|
-
existingContent =
|
|
7951
|
+
existingContent = fs4.readFileSync(filePath, "utf-8");
|
|
7680
7952
|
} catch {
|
|
7681
7953
|
}
|
|
7682
7954
|
if (!existingContent) {
|
|
7683
|
-
|
|
7955
|
+
fs4.writeFileSync(filePath, rule.content);
|
|
7684
7956
|
rulesGenerated.push(rule.filename);
|
|
7685
7957
|
} else if (!existingContent.includes("ContextStream Integration")) {
|
|
7686
|
-
|
|
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 =
|
|
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
|
|
7773
|
-
const
|
|
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 =
|
|
8052
|
+
const filePath = path6.join(folderPath, rule.filename);
|
|
7781
8053
|
try {
|
|
7782
8054
|
let existingContent = "";
|
|
7783
8055
|
try {
|
|
7784
|
-
existingContent =
|
|
8056
|
+
existingContent = fs4.readFileSync(filePath, "utf-8");
|
|
7785
8057
|
} catch {
|
|
7786
8058
|
}
|
|
7787
8059
|
if (!existingContent) {
|
|
7788
|
-
|
|
8060
|
+
fs4.writeFileSync(filePath, rule.content);
|
|
7789
8061
|
rulesGenerated.push(rule.filename);
|
|
7790
8062
|
} else if (!existingContent.includes("ContextStream Integration")) {
|
|
7791
|
-
|
|
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
|
|
8191
|
-
const
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
8496
|
+
fs4.writeFileSync(filePath, updatedContent);
|
|
8223
8497
|
results.push({ editor, filename: rule.filename, status: "appended to existing" });
|
|
8224
8498
|
} else {
|
|
8225
|
-
|
|
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(
|
|
8598
|
-
this.folderPath =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
8851
|
-
const starShownFile =
|
|
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({
|