@contextstream/mcp-server 0.3.28 → 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 +97 -2
- package/dist/index.js +862 -72
- 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) {
|
|
@@ -6469,8 +6482,34 @@ W:${wsHint}
|
|
|
6469
6482
|
}
|
|
6470
6483
|
};
|
|
6471
6484
|
|
|
6485
|
+
// src/tools.ts
|
|
6486
|
+
import * as path4 from "node:path";
|
|
6487
|
+
|
|
6472
6488
|
// src/rules-templates.ts
|
|
6473
|
-
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 = `
|
|
6474
6513
|
## ContextStream Integration
|
|
6475
6514
|
|
|
6476
6515
|
You have access to ContextStream MCP tools for persistent memory and context.
|
|
@@ -6480,7 +6519,7 @@ You have access to ContextStream MCP tools for persistent memory and context.
|
|
|
6480
6519
|
| Message | What to Call |
|
|
6481
6520
|
|---------|--------------|
|
|
6482
6521
|
| **1st message** | \`session_init(folder_path="...", context_hint="<user's message>")\` |
|
|
6483
|
-
| **2nd+ messages** | \`context_smart(user_message="<user's message>")\` |
|
|
6522
|
+
| **2nd+ messages** | \`context_smart(user_message="<user's message>", max_tokens=400)\` |
|
|
6484
6523
|
| **After completing task** | \`session_capture(...)\` - MUST capture decisions/insights |
|
|
6485
6524
|
| **User frustration/correction** | \`session_capture_lesson(...)\` - MUST capture lessons |
|
|
6486
6525
|
| **Command/tool error + fix** | \`session_capture_lesson(...)\` - MUST capture lessons |
|
|
@@ -6507,6 +6546,17 @@ You have access to ContextStream MCP tools for persistent memory and context.
|
|
|
6507
6546
|
|
|
6508
6547
|
---
|
|
6509
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
|
+
|
|
6510
6560
|
### When to Capture (MANDATORY)
|
|
6511
6561
|
|
|
6512
6562
|
| When | Tool | Example |
|
|
@@ -6593,7 +6643,7 @@ session_init(folder_path="/path/to/project", context_hint="how should I implemen
|
|
|
6593
6643
|
# Returns workspace info + semantically relevant auth decisions from ANY time
|
|
6594
6644
|
|
|
6595
6645
|
# Second message - user asks about database
|
|
6596
|
-
context_smart(user_message="what database should I use?")
|
|
6646
|
+
context_smart(user_message="what database should I use?", max_tokens=400)
|
|
6597
6647
|
# Returns: W:Maker|P:myproject|D:Use PostgreSQL|D:No ORMs|M:DB schema at...
|
|
6598
6648
|
|
|
6599
6649
|
# User says "Let's use Redis for caching"
|
|
@@ -6606,58 +6656,75 @@ session_capture(event_type="decision", title="Auth Implementation Complete", con
|
|
|
6606
6656
|
session_recall(query="what did we decide about caching?")
|
|
6607
6657
|
\`\`\`
|
|
6608
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();
|
|
6609
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
|
+
},
|
|
6610
6677
|
windsurf: {
|
|
6611
6678
|
filename: ".windsurfrules",
|
|
6612
6679
|
description: "Windsurf AI rules",
|
|
6613
|
-
|
|
6614
|
-
${
|
|
6680
|
+
build: (rules) => `# Windsurf Rules
|
|
6681
|
+
${rules}
|
|
6615
6682
|
`
|
|
6616
6683
|
},
|
|
6617
6684
|
cursor: {
|
|
6618
6685
|
filename: ".cursorrules",
|
|
6619
6686
|
description: "Cursor AI rules",
|
|
6620
|
-
|
|
6621
|
-
${
|
|
6687
|
+
build: (rules) => `# Cursor Rules
|
|
6688
|
+
${rules}
|
|
6622
6689
|
`
|
|
6623
6690
|
},
|
|
6624
6691
|
cline: {
|
|
6625
6692
|
filename: ".clinerules",
|
|
6626
6693
|
description: "Cline AI rules",
|
|
6627
|
-
|
|
6628
|
-
${
|
|
6694
|
+
build: (rules) => `# Cline Rules
|
|
6695
|
+
${rules}
|
|
6629
6696
|
`
|
|
6630
6697
|
},
|
|
6631
6698
|
kilo: {
|
|
6632
6699
|
filename: ".kilocode/rules/contextstream.md",
|
|
6633
6700
|
description: "Kilo Code AI rules",
|
|
6634
|
-
|
|
6635
|
-
${
|
|
6701
|
+
build: (rules) => `# Kilo Code Rules
|
|
6702
|
+
${rules}
|
|
6636
6703
|
`
|
|
6637
6704
|
},
|
|
6638
6705
|
roo: {
|
|
6639
6706
|
filename: ".roo/rules/contextstream.md",
|
|
6640
6707
|
description: "Roo Code AI rules",
|
|
6641
|
-
|
|
6642
|
-
${
|
|
6708
|
+
build: (rules) => `# Roo Code Rules
|
|
6709
|
+
${rules}
|
|
6643
6710
|
`
|
|
6644
6711
|
},
|
|
6645
6712
|
claude: {
|
|
6646
6713
|
filename: "CLAUDE.md",
|
|
6647
6714
|
description: "Claude Code instructions",
|
|
6648
|
-
|
|
6649
|
-
${
|
|
6715
|
+
build: (rules) => `# Claude Code Instructions
|
|
6716
|
+
${rules}
|
|
6650
6717
|
`
|
|
6651
6718
|
},
|
|
6652
6719
|
aider: {
|
|
6653
6720
|
filename: ".aider.conf.yml",
|
|
6654
6721
|
description: "Aider configuration with system prompt",
|
|
6655
|
-
|
|
6722
|
+
build: (rules) => `# Aider Configuration
|
|
6656
6723
|
# Note: Aider uses different config format - this adds to the system prompt
|
|
6657
6724
|
|
|
6658
6725
|
# Add ContextStream guidance to conventions
|
|
6659
6726
|
conventions: |
|
|
6660
|
-
${
|
|
6727
|
+
${rules.split("\n").map((line) => " " + line).join("\n")}
|
|
6661
6728
|
`
|
|
6662
6729
|
}
|
|
6663
6730
|
};
|
|
@@ -6670,7 +6737,9 @@ function getTemplate(editor) {
|
|
|
6670
6737
|
function generateRuleContent(editor, options) {
|
|
6671
6738
|
const template = getTemplate(editor);
|
|
6672
6739
|
if (!template) return null;
|
|
6673
|
-
|
|
6740
|
+
const mode = options?.mode || "minimal";
|
|
6741
|
+
const rules = mode === "full" ? CONTEXTSTREAM_RULES_FULL : CONTEXTSTREAM_RULES_MINIMAL;
|
|
6742
|
+
let content = template.build(rules);
|
|
6674
6743
|
if (options?.workspaceName || options?.projectName) {
|
|
6675
6744
|
const header = `
|
|
6676
6745
|
# Workspace: ${options.workspaceName || "Unknown"}
|
|
@@ -6683,6 +6752,9 @@ ${options.workspaceId ? `# Workspace ID: ${options.workspaceId}` : ""}
|
|
|
6683
6752
|
if (options?.additionalRules) {
|
|
6684
6753
|
content += "\n\n## Project-Specific Rules\n\n" + options.additionalRules;
|
|
6685
6754
|
}
|
|
6755
|
+
if (editor.toLowerCase() === "claude") {
|
|
6756
|
+
content = applyMcpToolPrefix(content, `mcp__${DEFAULT_CLAUDE_MCP_SERVER_NAME}__`);
|
|
6757
|
+
}
|
|
6686
6758
|
return {
|
|
6687
6759
|
filename: template.filename,
|
|
6688
6760
|
content: content.trim() + "\n"
|
|
@@ -7794,7 +7866,7 @@ This does semantic search on the first message. You only need context_smart on s
|
|
|
7794
7866
|
formatContent(result)
|
|
7795
7867
|
].join("\n");
|
|
7796
7868
|
} else if (status === "requires_workspace_selection") {
|
|
7797
|
-
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";
|
|
7798
7870
|
const candidates = Array.isArray(result.workspace_candidates) ? result.workspace_candidates : [];
|
|
7799
7871
|
const lines = [];
|
|
7800
7872
|
lines.push(`Action required: select a workspace for "${folderName}" (or create a new one).`);
|
|
@@ -7864,26 +7936,26 @@ Optionally generates AI editor rules for automatic ContextStream usage.`,
|
|
|
7864
7936
|
const result = await client.associateWorkspace(input);
|
|
7865
7937
|
let rulesGenerated = [];
|
|
7866
7938
|
if (input.generate_editor_rules) {
|
|
7867
|
-
const
|
|
7868
|
-
const
|
|
7939
|
+
const fs4 = await import("fs");
|
|
7940
|
+
const path6 = await import("path");
|
|
7869
7941
|
for (const editor of getAvailableEditors()) {
|
|
7870
7942
|
const rule = generateRuleContent(editor, {
|
|
7871
7943
|
workspaceName: input.workspace_name,
|
|
7872
7944
|
workspaceId: input.workspace_id
|
|
7873
7945
|
});
|
|
7874
7946
|
if (rule) {
|
|
7875
|
-
const filePath =
|
|
7947
|
+
const filePath = path6.join(input.folder_path, rule.filename);
|
|
7876
7948
|
try {
|
|
7877
7949
|
let existingContent = "";
|
|
7878
7950
|
try {
|
|
7879
|
-
existingContent =
|
|
7951
|
+
existingContent = fs4.readFileSync(filePath, "utf-8");
|
|
7880
7952
|
} catch {
|
|
7881
7953
|
}
|
|
7882
7954
|
if (!existingContent) {
|
|
7883
|
-
|
|
7955
|
+
fs4.writeFileSync(filePath, rule.content);
|
|
7884
7956
|
rulesGenerated.push(rule.filename);
|
|
7885
7957
|
} else if (!existingContent.includes("ContextStream Integration")) {
|
|
7886
|
-
|
|
7958
|
+
fs4.writeFileSync(filePath, existingContent + "\n\n" + rule.content);
|
|
7887
7959
|
rulesGenerated.push(rule.filename + " (appended)");
|
|
7888
7960
|
}
|
|
7889
7961
|
} catch {
|
|
@@ -7937,7 +8009,7 @@ Behavior:
|
|
|
7937
8009
|
if (!folderPath) {
|
|
7938
8010
|
return errorResult("Error: folder_path is required. Provide folder_path or run from a project directory.");
|
|
7939
8011
|
}
|
|
7940
|
-
const folderName =
|
|
8012
|
+
const folderName = path4.basename(folderPath) || "My Project";
|
|
7941
8013
|
let newWorkspace;
|
|
7942
8014
|
try {
|
|
7943
8015
|
newWorkspace = await client.createWorkspace({
|
|
@@ -7969,26 +8041,26 @@ Behavior:
|
|
|
7969
8041
|
});
|
|
7970
8042
|
let rulesGenerated = [];
|
|
7971
8043
|
if (input.generate_editor_rules) {
|
|
7972
|
-
const
|
|
7973
|
-
const
|
|
8044
|
+
const fs4 = await import("fs");
|
|
8045
|
+
const path6 = await import("path");
|
|
7974
8046
|
for (const editor of getAvailableEditors()) {
|
|
7975
8047
|
const rule = generateRuleContent(editor, {
|
|
7976
8048
|
workspaceName: newWorkspace.name || input.workspace_name,
|
|
7977
8049
|
workspaceId: newWorkspace.id
|
|
7978
8050
|
});
|
|
7979
8051
|
if (!rule) continue;
|
|
7980
|
-
const filePath =
|
|
8052
|
+
const filePath = path6.join(folderPath, rule.filename);
|
|
7981
8053
|
try {
|
|
7982
8054
|
let existingContent = "";
|
|
7983
8055
|
try {
|
|
7984
|
-
existingContent =
|
|
8056
|
+
existingContent = fs4.readFileSync(filePath, "utf-8");
|
|
7985
8057
|
} catch {
|
|
7986
8058
|
}
|
|
7987
8059
|
if (!existingContent) {
|
|
7988
|
-
|
|
8060
|
+
fs4.writeFileSync(filePath, rule.content);
|
|
7989
8061
|
rulesGenerated.push(rule.filename);
|
|
7990
8062
|
} else if (!existingContent.includes("ContextStream Integration")) {
|
|
7991
|
-
|
|
8063
|
+
fs4.writeFileSync(filePath, existingContent + "\n\n" + rule.content);
|
|
7992
8064
|
rulesGenerated.push(rule.filename + " (appended)");
|
|
7993
8065
|
}
|
|
7994
8066
|
} catch {
|
|
@@ -8378,17 +8450,18 @@ These rules instruct the AI to automatically use ContextStream for memory and co
|
|
|
8378
8450
|
Supported editors: ${getAvailableEditors().join(", ")}`,
|
|
8379
8451
|
inputSchema: external_exports.object({
|
|
8380
8452
|
folder_path: external_exports.string().describe("Absolute path to the project folder"),
|
|
8381
|
-
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."),
|
|
8382
8454
|
workspace_name: external_exports.string().optional().describe("Workspace name to include in rules"),
|
|
8383
8455
|
workspace_id: external_exports.string().uuid().optional().describe("Workspace ID to include in rules"),
|
|
8384
8456
|
project_name: external_exports.string().optional().describe("Project name to include in rules"),
|
|
8385
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)"),
|
|
8386
8459
|
dry_run: external_exports.boolean().optional().describe("If true, return content without writing files")
|
|
8387
8460
|
})
|
|
8388
8461
|
},
|
|
8389
8462
|
async (input) => {
|
|
8390
|
-
const
|
|
8391
|
-
const
|
|
8463
|
+
const fs4 = await import("fs");
|
|
8464
|
+
const path6 = await import("path");
|
|
8392
8465
|
const editors = input.editors?.includes("all") || !input.editors ? getAvailableEditors() : input.editors.filter((e) => e !== "all");
|
|
8393
8466
|
const results = [];
|
|
8394
8467
|
for (const editor of editors) {
|
|
@@ -8396,13 +8469,14 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
|
|
|
8396
8469
|
workspaceName: input.workspace_name,
|
|
8397
8470
|
workspaceId: input.workspace_id,
|
|
8398
8471
|
projectName: input.project_name,
|
|
8399
|
-
additionalRules: input.additional_rules
|
|
8472
|
+
additionalRules: input.additional_rules,
|
|
8473
|
+
mode: input.mode
|
|
8400
8474
|
});
|
|
8401
8475
|
if (!rule) {
|
|
8402
8476
|
results.push({ editor, filename: "", status: "unknown editor" });
|
|
8403
8477
|
continue;
|
|
8404
8478
|
}
|
|
8405
|
-
const filePath =
|
|
8479
|
+
const filePath = path6.join(input.folder_path, rule.filename);
|
|
8406
8480
|
if (input.dry_run) {
|
|
8407
8481
|
results.push({
|
|
8408
8482
|
editor,
|
|
@@ -8414,15 +8488,15 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
|
|
|
8414
8488
|
try {
|
|
8415
8489
|
let existingContent = "";
|
|
8416
8490
|
try {
|
|
8417
|
-
existingContent =
|
|
8491
|
+
existingContent = fs4.readFileSync(filePath, "utf-8");
|
|
8418
8492
|
} catch {
|
|
8419
8493
|
}
|
|
8420
8494
|
if (existingContent && !existingContent.includes("ContextStream Integration")) {
|
|
8421
8495
|
const updatedContent = existingContent + "\n\n" + rule.content;
|
|
8422
|
-
|
|
8496
|
+
fs4.writeFileSync(filePath, updatedContent);
|
|
8423
8497
|
results.push({ editor, filename: rule.filename, status: "appended to existing" });
|
|
8424
8498
|
} else {
|
|
8425
|
-
|
|
8499
|
+
fs4.writeFileSync(filePath, rule.content);
|
|
8426
8500
|
results.push({ editor, filename: rule.filename, status: "created" });
|
|
8427
8501
|
}
|
|
8428
8502
|
} catch (err) {
|
|
@@ -9092,8 +9166,8 @@ var SessionManager = class {
|
|
|
9092
9166
|
/**
|
|
9093
9167
|
* Set the folder path hint (can be passed from tools that know the workspace path)
|
|
9094
9168
|
*/
|
|
9095
|
-
setFolderPath(
|
|
9096
|
-
this.folderPath =
|
|
9169
|
+
setFolderPath(path6) {
|
|
9170
|
+
this.folderPath = path6;
|
|
9097
9171
|
}
|
|
9098
9172
|
/**
|
|
9099
9173
|
* Mark that context_smart has been called in this session
|
|
@@ -9160,11 +9234,11 @@ var SessionManager = class {
|
|
|
9160
9234
|
}
|
|
9161
9235
|
if (this.ideRoots.length === 0) {
|
|
9162
9236
|
const cwd = process.cwd();
|
|
9163
|
-
const
|
|
9237
|
+
const fs4 = await import("fs");
|
|
9164
9238
|
const projectIndicators = [".git", "package.json", "Cargo.toml", "pyproject.toml", ".contextstream"];
|
|
9165
9239
|
const hasProjectIndicator = projectIndicators.some((f) => {
|
|
9166
9240
|
try {
|
|
9167
|
-
return
|
|
9241
|
+
return fs4.existsSync(`${cwd}/${f}`);
|
|
9168
9242
|
} catch {
|
|
9169
9243
|
return false;
|
|
9170
9244
|
}
|
|
@@ -9342,11 +9416,716 @@ var SessionManager = class {
|
|
|
9342
9416
|
|
|
9343
9417
|
// src/index.ts
|
|
9344
9418
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
9345
|
-
import { homedir } from "os";
|
|
9346
|
-
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
|
|
9347
10126
|
function showFirstRunMessage() {
|
|
9348
|
-
const configDir =
|
|
9349
|
-
const starShownFile =
|
|
10127
|
+
const configDir = join5(homedir2(), ".contextstream");
|
|
10128
|
+
const starShownFile = join5(configDir, ".star-shown");
|
|
9350
10129
|
if (existsSync2(starShownFile)) {
|
|
9351
10130
|
return;
|
|
9352
10131
|
}
|
|
@@ -9375,6 +10154,10 @@ function printHelp() {
|
|
|
9375
10154
|
Usage:
|
|
9376
10155
|
npx -y @contextstream/mcp-server
|
|
9377
10156
|
contextstream-mcp
|
|
10157
|
+
contextstream-mcp setup
|
|
10158
|
+
|
|
10159
|
+
Commands:
|
|
10160
|
+
setup Interactive onboarding wizard (rules + workspace mapping)
|
|
9378
10161
|
|
|
9379
10162
|
Environment variables:
|
|
9380
10163
|
CONTEXTSTREAM_API_URL Base API URL (e.g. https://api.contextstream.io)
|
|
@@ -9390,6 +10173,9 @@ Examples:
|
|
|
9390
10173
|
CONTEXTSTREAM_API_KEY="your_api_key" \\
|
|
9391
10174
|
npx -y @contextstream/mcp-server
|
|
9392
10175
|
|
|
10176
|
+
Setup wizard:
|
|
10177
|
+
npx -y @contextstream/mcp-server setup
|
|
10178
|
+
|
|
9393
10179
|
Notes:
|
|
9394
10180
|
- When used from an MCP client (e.g. Codex, Cursor, VS Code),
|
|
9395
10181
|
set these env vars in the client's MCP server configuration.
|
|
@@ -9405,6 +10191,10 @@ async function main() {
|
|
|
9405
10191
|
console.log(`contextstream-mcp v${VERSION}`);
|
|
9406
10192
|
return;
|
|
9407
10193
|
}
|
|
10194
|
+
if (args[0] === "setup") {
|
|
10195
|
+
await runSetupWizard(args.slice(1));
|
|
10196
|
+
return;
|
|
10197
|
+
}
|
|
9408
10198
|
const config = loadConfig();
|
|
9409
10199
|
const client = new ContextStreamClient(config);
|
|
9410
10200
|
const server = new McpServer({
|