🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@contextstream/mcp-server

Package Overview
Dependencies
Maintainers
1
Versions
148
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@contextstream/mcp-server - npm Package Compare versions

Comparing version
0.4.81
to
0.4.82
+703
-28
dist/hooks/post-write.js
#!/usr/bin/env node
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
try {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
} catch (e) {
throw mod = 0, e;
}
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// node_modules/ignore/index.js
var require_ignore = __commonJS({
"node_modules/ignore/index.js"(exports, module) {
function makeArray(subject) {
return Array.isArray(subject) ? subject : [subject];
}
var UNDEFINED = void 0;
var EMPTY = "";
var SPACE = " ";
var ESCAPE = "\\";
var REGEX_TEST_BLANK_LINE = /^\s+$/;
var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
var REGEX_SPLITALL_CRLF = /\r?\n/g;
var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
var REGEX_TEST_TRAILING_SLASH = /\/$/;
var SLASH = "/";
var TMP_KEY_IGNORE = "node-ignore";
if (typeof Symbol !== "undefined") {
TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
}
var KEY_IGNORE = TMP_KEY_IGNORE;
var define = (object, key, value) => {
Object.defineProperty(object, key, { value });
return value;
};
var REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
var RETURN_FALSE = () => false;
var sanitizeRange = (range) => range.replace(
REGEX_REGEXP_RANGE,
(match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match : EMPTY
);
var cleanRangeBackSlash = (slashes) => {
const { length } = slashes;
return slashes.slice(0, length - length % 2);
};
var REPLACERS = [
[
// Remove BOM
// TODO:
// Other similar zero-width characters?
/^\uFEFF/,
() => EMPTY
],
// > Trailing spaces are ignored unless they are quoted with backslash ("\")
[
// (a\ ) -> (a )
// (a ) -> (a)
// (a ) -> (a)
// (a \ ) -> (a )
/((?:\\\\)*?)(\\?\s+)$/,
(_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY)
],
// Replace (\ ) with ' '
// (\ ) -> ' '
// (\\ ) -> '\\ '
// (\\\ ) -> '\\ '
[
/(\\+?)\s/g,
(_, m1) => {
const { length } = m1;
return m1.slice(0, length - length % 2) + SPACE;
}
],
// Escape metacharacters
// which is written down by users but means special for regular expressions.
// > There are 12 characters with special meanings:
// > - the backslash \,
// > - the caret ^,
// > - the dollar sign $,
// > - the period or dot .,
// > - the vertical bar or pipe symbol |,
// > - the question mark ?,
// > - the asterisk or star *,
// > - the plus sign +,
// > - the opening parenthesis (,
// > - the closing parenthesis ),
// > - and the opening square bracket [,
// > - the opening curly brace {,
// > These special characters are often called "metacharacters".
[
/[\\$.|*+(){^]/g,
(match) => `\\${match}`
],
[
// > a question mark (?) matches a single character
/(?!\\)\?/g,
() => "[^/]"
],
// leading slash
[
// > A leading slash matches the beginning of the pathname.
// > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
// A leading slash matches the beginning of the pathname
/^\//,
() => "^"
],
// replace special metacharacter slash after the leading slash
[
/\//g,
() => "\\/"
],
[
// > A leading "**" followed by a slash means match in all directories.
// > For example, "**/foo" matches file or directory "foo" anywhere,
// > the same as pattern "foo".
// > "**/foo/bar" matches file or directory "bar" anywhere that is directly
// > under directory "foo".
// Notice that the '*'s have been replaced as '\\*'
/^\^*\\\*\\\*\\\//,
// '**/foo' <-> 'foo'
() => "^(?:.*\\/)?"
],
// starting
[
// there will be no leading '/'
// (which has been replaced by section "leading slash")
// If starts with '**', adding a '^' to the regular expression also works
/^(?=[^^])/,
function startingReplacer() {
return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^";
}
],
// two globstars
[
// Use lookahead assertions so that we could match more than one `'/**'`
/\\\/\\\*\\\*(?=\\\/|$)/g,
// Zero, one or several directories
// should not use '*', or it will be replaced by the next replacer
// Check if it is not the last `'/**'`
(_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
],
// normal intermediate wildcards
[
// Never replace escaped '*'
// ignore rule '\*' will match the path '*'
// 'abc.*/' -> go
// 'abc.*' -> skip this rule,
// coz trailing single wildcard will be handed by [trailing wildcard]
/(^|[^\\]+)(\\\*)+(?=.+)/g,
// '*.js' matches '.js'
// '*.js' doesn't match 'abc'
(_, p1, p2) => {
const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
return p1 + unescaped;
}
],
[
// unescape, revert step 3 except for back slash
// For example, if a user escape a '\\*',
// after step 3, the result will be '\\\\\\*'
/\\\\\\(?=[$.|*+(){^])/g,
() => ESCAPE
],
[
// '\\\\' -> '\\'
/\\\\/g,
() => ESCAPE
],
[
// > The range notation, e.g. [a-zA-Z],
// > can be used to match one of the characters in a range.
// `\` is escaped by step 3
/(\\)?\[([^\]/]*?)(\\*)($|\])/g,
(match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]"
],
// ending
[
// 'js' will not match 'js.'
// 'ab' will not match 'abc'
/(?:[^*])$/,
// WTF!
// https://git-scm.com/docs/gitignore
// changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
// which re-fixes #24, #38
// > If there is a separator at the end of the pattern then the pattern
// > will only match directories, otherwise the pattern can match both
// > files and directories.
// 'js*' will not match 'a.js'
// 'js/' will not match 'a.js'
// 'js' will match 'a.js' and 'a.js/'
(match) => /\/$/.test(match) ? `${match}$` : `${match}(?=$|\\/$)`
]
];
var REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/;
var MODE_IGNORE = "regex";
var MODE_CHECK_IGNORE = "checkRegex";
var UNDERSCORE = "_";
var TRAILING_WILD_CARD_REPLACERS = {
[MODE_IGNORE](_, p1) {
const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
return `${prefix}(?=$|\\/$)`;
},
[MODE_CHECK_IGNORE](_, p1) {
const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
return `${prefix}(?=$|\\/$)`;
}
};
var makeRegexPrefix = (pattern) => REPLACERS.reduce(
(prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)),
pattern
);
var isString = (subject) => typeof subject === "string";
var checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0;
var splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean);
var IgnoreRule = class {
constructor(pattern, mark, body, ignoreCase, negative, prefix) {
this.pattern = pattern;
this.mark = mark;
this.negative = negative;
define(this, "body", body);
define(this, "ignoreCase", ignoreCase);
define(this, "regexPrefix", prefix);
}
get regex() {
const key = UNDERSCORE + MODE_IGNORE;
if (this[key]) {
return this[key];
}
return this._make(MODE_IGNORE, key);
}
get checkRegex() {
const key = UNDERSCORE + MODE_CHECK_IGNORE;
if (this[key]) {
return this[key];
}
return this._make(MODE_CHECK_IGNORE, key);
}
_make(mode, key) {
const str = this.regexPrefix.replace(
REGEX_REPLACE_TRAILING_WILDCARD,
// It does not need to bind pattern
TRAILING_WILD_CARD_REPLACERS[mode]
);
const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
return define(this, key, regex);
}
};
var createRule = ({
pattern,
mark
}, ignoreCase) => {
let negative = false;
let body = pattern;
if (body.indexOf("!") === 0) {
negative = true;
body = body.substr(1);
}
body = body.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
const regexPrefix = makeRegexPrefix(body);
return new IgnoreRule(
pattern,
mark,
body,
ignoreCase,
negative,
regexPrefix
);
};
var RuleManager = class {
constructor(ignoreCase) {
this._ignoreCase = ignoreCase;
this._rules = [];
}
_add(pattern) {
if (pattern && pattern[KEY_IGNORE]) {
this._rules = this._rules.concat(pattern._rules._rules);
this._added = true;
return;
}
if (isString(pattern)) {
pattern = {
pattern
};
}
if (checkPattern(pattern.pattern)) {
const rule = createRule(pattern, this._ignoreCase);
this._added = true;
this._rules.push(rule);
}
}
// @param {Array<string> | string | Ignore} pattern
add(pattern) {
this._added = false;
makeArray(
isString(pattern) ? splitPattern(pattern) : pattern
).forEach(this._add, this);
return this._added;
}
// Test one single path without recursively checking parent directories
//
// - checkUnignored `boolean` whether should check if the path is unignored,
// setting `checkUnignored` to `false` could reduce additional
// path matching.
// - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
// @returns {TestResult} true if a file is ignored
test(path3, checkUnignored, mode) {
let ignored = false;
let unignored = false;
let matchedRule;
this._rules.forEach((rule) => {
const { negative } = rule;
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
return;
}
const matched = rule[mode].test(path3);
if (!matched) {
return;
}
ignored = !negative;
unignored = negative;
matchedRule = negative ? UNDEFINED : rule;
});
const ret = {
ignored,
unignored
};
if (matchedRule) {
ret.rule = matchedRule;
}
return ret;
}
};
var throwError = (message, Ctor) => {
throw new Ctor(message);
};
var checkPath = (path3, originalPath, doThrow) => {
if (!isString(path3)) {
return doThrow(
`path must be a string, but got \`${originalPath}\``,
TypeError
);
}
if (!path3) {
return doThrow(`path must not be empty`, TypeError);
}
if (checkPath.isNotRelative(path3)) {
const r = "`path.relative()`d";
return doThrow(
`path should be a ${r} string, but got "${originalPath}"`,
RangeError
);
}
return true;
};
var isNotRelative = (path3) => REGEX_TEST_INVALID_PATH.test(path3);
checkPath.isNotRelative = isNotRelative;
checkPath.convert = (p) => p;
var Ignore2 = class {
constructor({
ignorecase = true,
ignoreCase = ignorecase,
allowRelativePaths = false
} = {}) {
define(this, KEY_IGNORE, true);
this._rules = new RuleManager(ignoreCase);
this._strictPathCheck = !allowRelativePaths;
this._initCache();
}
_initCache() {
this._ignoreCache = /* @__PURE__ */ Object.create(null);
this._testCache = /* @__PURE__ */ Object.create(null);
}
add(pattern) {
if (this._rules.add(pattern)) {
this._initCache();
}
return this;
}
// legacy
addPattern(pattern) {
return this.add(pattern);
}
// @returns {TestResult}
_test(originalPath, cache, checkUnignored, slices) {
const path3 = originalPath && checkPath.convert(originalPath);
checkPath(
path3,
originalPath,
this._strictPathCheck ? throwError : RETURN_FALSE
);
return this._t(path3, cache, checkUnignored, slices);
}
checkIgnore(path3) {
if (!REGEX_TEST_TRAILING_SLASH.test(path3)) {
return this.test(path3);
}
const slices = path3.split(SLASH).filter(Boolean);
slices.pop();
if (slices.length) {
const parent = this._t(
slices.join(SLASH) + SLASH,
this._testCache,
true,
slices
);
if (parent.ignored) {
return parent;
}
}
return this._rules.test(path3, false, MODE_CHECK_IGNORE);
}
_t(path3, cache, checkUnignored, slices) {
if (path3 in cache) {
return cache[path3];
}
if (!slices) {
slices = path3.split(SLASH).filter(Boolean);
}
slices.pop();
if (!slices.length) {
return cache[path3] = this._rules.test(path3, checkUnignored, MODE_IGNORE);
}
const parent = this._t(
slices.join(SLASH) + SLASH,
cache,
checkUnignored,
slices
);
return cache[path3] = parent.ignored ? parent : this._rules.test(path3, checkUnignored, MODE_IGNORE);
}
ignores(path3) {
return this._test(path3, this._ignoreCache, false).ignored;
}
createFilter() {
return (path3) => !this.ignores(path3);
}
filter(paths) {
return makeArray(paths).filter(this.createFilter());
}
// @returns {TestResult}
test(path3) {
return this._test(path3, this._testCache, true);
}
};
var factory = (options) => new Ignore2(options);
var isPathValid = (path3) => checkPath(path3 && checkPath.convert(path3), path3, RETURN_FALSE);
var setupWindows = () => {
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
checkPath.convert = makePosix;
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
checkPath.isNotRelative = (path3) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path3) || isNotRelative(path3);
};
if (
// Detect `process` so that it can run in browsers.
typeof process !== "undefined" && process.platform === "win32"
) {
setupWindows();
}
module.exports = factory;
factory.default = factory;
module.exports.isPathValid = isPathValid;
define(module.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
}
});
// src/hooks/post-write.ts
import * as fs from "node:fs";
import * as path from "node:path";
import * as fs2 from "node:fs";
import * as path2 from "node:path";
import { homedir } from "node:os";
import { execFileSync } from "node:child_process";
// src/ignore.ts
var import_ignore = __toESM(require_ignore(), 1);
import * as fs from "fs";
import * as path from "path";
var IGNORE_FILENAMES = [".gitignore", ".contextignore", ".contextstream/ignore"];
var INVARIANT_IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
".contextstream",
".claude",
".codex",
".cursor",
".windsurf",
".roo",
".kilocode",
".ssh",
".aws",
".gnupg",
".kube",
".docker"
]);
var INVARIANT_SECRET_FILENAMES = /* @__PURE__ */ new Set([
".npmrc",
".pypirc",
".netrc",
".pgpass",
".git-credentials",
"id_rsa",
"id_ed25519",
"id_ecdsa",
"known_hosts",
"credentials.json",
"serviceaccountkey.json",
"auth.json"
]);
var INVARIANT_SECRET_SUFFIXES = [".pem", ".key", ".p12", ".pfx", ".jks"];
var ENV_TEMPLATE_ALLOWLIST = /* @__PURE__ */ new Set([
".env.example",
".env.sample",
".env.template",
".env.dist",
".env.defaults"
]);
function isInvariantIgnoredPath(pathname) {
const normalized = pathname.replace(/\\/g, "/").replace(/^\.\//, "");
const segments = normalized.split("/").filter(Boolean);
if (segments.some((segment) => INVARIANT_IGNORED_DIRECTORIES.has(segment.toLowerCase()))) {
return true;
}
const basename2 = (segments.at(-1) || "").toLowerCase();
if (INVARIANT_SECRET_FILENAMES.has(basename2)) return true;
if (INVARIANT_SECRET_SUFFIXES.some((suffix) => basename2.endsWith(suffix))) return true;
return (basename2 === ".env" || basename2.startsWith(".env.")) && !ENV_TEMPLATE_ALLOWLIST.has(basename2);
}
var DEFAULT_IGNORE_PATTERNS = [
// Version control
".git/",
".svn/",
".hg/",
// ContextStream and AI editor/agent state. These directories can contain
// credentials, generated rules, caches, and nested worktrees.
".contextstream/",
".claude/",
".codex/",
".cursor/",
".windsurf/",
".roo/",
".kilocode/",
// Package managers / dependencies
"node_modules/",
"vendor/",
".pnpm/",
// Build outputs
"target/",
"dist/",
"build/",
"out/",
".next/",
".nuxt/",
".svelte-kit/",
".parcel-cache/",
".turbo/",
".gradle/",
".cache/",
"bin/",
"obj/",
// Python
"__pycache__/",
".pytest_cache/",
".mypy_cache/",
"venv/",
".venv/",
"env/",
".env/",
// IDE
".idea/",
".vscode/",
".vs/",
// Credential stores
".ssh/",
".aws/",
".gnupg/",
".kube/",
".docker/",
// Coverage
"coverage/",
".coverage/",
// Lock files
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"Cargo.lock",
"poetry.lock",
"Gemfile.lock",
"composer.lock",
"*.min.js",
"*.min.css",
"*.pem",
"*.key",
"*.p12",
"*.pfx",
"*.jks",
".npmrc",
".pypirc",
".netrc",
".pgpass",
".git-credentials",
"id_rsa",
"id_ed25519",
"id_ecdsa",
"credentials.json",
"serviceAccountKey.json",
"auth.json",
// OS files
".DS_Store",
"Thumbs.db"
];
function loadIgnorePatternsSync(projectRoot) {
const ig = (0, import_ignore.default)();
const patterns = [...DEFAULT_IGNORE_PATTERNS];
ig.add(DEFAULT_IGNORE_PATTERNS);
let hasUserPatterns = false;
for (const filename of IGNORE_FILENAMES) {
try {
const content = fs.readFileSync(path.join(projectRoot, filename), "utf-8");
const userPatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
if (userPatterns.length > 0) {
ig.add(userPatterns);
patterns.push(...userPatterns);
hasUserPatterns = true;
}
} catch {
}
}
return {
ignores: (pathname) => isInvariantIgnoredPath(pathname) || ig.ignores(pathname),
patterns,
hasUserPatterns
};
}
// src/hooks/post-write.ts
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";

@@ -102,8 +750,8 @@ var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";

function findLocalConfig(startDir) {
let currentDir = path.resolve(startDir);
let currentDir = path2.resolve(startDir);
for (let i = 0; i < 10; i++) {
const configPath = path.join(currentDir, ".contextstream", "config.json");
if (fs.existsSync(configPath)) {
const configPath = path2.join(currentDir, ".contextstream", "config.json");
if (fs2.existsSync(configPath)) {
try {
const content = fs.readFileSync(configPath, "utf-8");
const content = fs2.readFileSync(configPath, "utf-8");
return JSON.parse(content);

@@ -113,3 +761,3 @@ } catch {

}
const parentDir = path.dirname(currentDir);
const parentDir = path2.dirname(currentDir);
if (parentDir === currentDir) break;

@@ -126,8 +774,8 @@ currentDir = parentDir;

}
let currentDir = path.resolve(startDir);
let currentDir = path2.resolve(startDir);
for (let i = 0; i < 10; i++) {
const mcpPath = path.join(currentDir, ".mcp.json");
if (fs.existsSync(mcpPath)) {
const mcpPath = path2.join(currentDir, ".mcp.json");
if (fs2.existsSync(mcpPath)) {
try {
const content = fs.readFileSync(mcpPath, "utf-8");
const content = fs2.readFileSync(mcpPath, "utf-8");
const config = JSON.parse(content);

@@ -145,3 +793,3 @@ const csEnv = config.mcpServers?.contextstream?.env;

}
const parentDir = path.dirname(currentDir);
const parentDir = path2.dirname(currentDir);
if (parentDir === currentDir) break;

@@ -151,6 +799,6 @@ currentDir = parentDir;

if (!apiKey) {
const homeMcpPath = path.join(homedir(), ".mcp.json");
if (fs.existsSync(homeMcpPath)) {
const homeMcpPath = path2.join(homedir(), ".mcp.json");
if (fs2.existsSync(homeMcpPath)) {
try {
const content = fs.readFileSync(homeMcpPath, "utf-8");
const content = fs2.readFileSync(homeMcpPath, "utf-8");
const config = JSON.parse(content);

@@ -171,5 +819,5 @@ const csEnv = config.mcpServers?.contextstream?.env;

function shouldIndexFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
const ext = path2.extname(filePath).toLowerCase();
if (!INDEXABLE_EXTENSIONS.has(ext)) {
const basename2 = path.basename(filePath).toLowerCase();
const basename2 = path2.basename(filePath).toLowerCase();
if (!["dockerfile", "makefile", "rakefile", "gemfile", "procfile"].includes(basename2)) {

@@ -180,3 +828,3 @@ return false;

try {
const stats = fs.statSync(filePath);
const stats = fs2.statSync(filePath);
if (stats.size > MAX_FILE_SIZE) {

@@ -190,4 +838,27 @@ return false;

}
function shouldIgnoreHookPath(projectRoot, filePath) {
const root = path2.resolve(projectRoot);
const absolutePath = path2.resolve(filePath);
if (isInvariantIgnoredPath(absolutePath)) {
return true;
}
const relativePath = path2.relative(root, absolutePath).replace(/\\/g, "/");
if (!relativePath || relativePath === ".." || relativePath.startsWith("../")) {
return true;
}
if (loadIgnorePatternsSync(root).ignores(relativePath)) {
return true;
}
try {
execFileSync("git", ["-C", root, "check-ignore", "-q", "--", relativePath], {
stdio: "ignore",
timeout: 2e3
});
return true;
} catch {
return false;
}
}
function detectLanguage(filePath) {
const ext = path.extname(filePath).toLowerCase();
const ext = path2.extname(filePath).toLowerCase();
const langMap = {

@@ -261,4 +932,4 @@ ".ts": "typescript",

async function indexFile(filePath, projectId, apiUrl, apiKey, projectRoot) {
const content = fs.readFileSync(filePath, "utf-8");
const relativePath = path.relative(projectRoot, filePath);
const content = fs2.readFileSync(filePath, "utf-8");
const relativePath = path2.relative(projectRoot, filePath);
const payload = {

@@ -295,9 +966,9 @@ files: [

function findProjectRoot(filePath) {
let currentDir = path.dirname(path.resolve(filePath));
let currentDir = path2.dirname(path2.resolve(filePath));
for (let i = 0; i < 10; i++) {
const configPath = path.join(currentDir, ".contextstream", "config.json");
if (fs.existsSync(configPath)) {
const configPath = path2.join(currentDir, ".contextstream", "config.json");
if (fs2.existsSync(configPath)) {
return currentDir;
}
const parentDir = path.dirname(currentDir);
const parentDir = path2.dirname(currentDir);
if (parentDir === currentDir) break;

@@ -330,4 +1001,4 @@ currentDir = parentDir;

const cwd = extractCwd(input);
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
if (!fs.existsSync(absolutePath) || !shouldIndexFile(absolutePath)) {
const absolutePath = path2.isAbsolute(filePath) ? filePath : path2.resolve(cwd, filePath);
if (!fs2.existsSync(absolutePath) || !shouldIndexFile(absolutePath)) {
process.exit(0);

@@ -339,2 +1010,5 @@ }

}
if (shouldIgnoreHookPath(projectRoot, absolutePath)) {
process.exit(0);
}
const localConfig = findLocalConfig(projectRoot);

@@ -359,3 +1033,4 @@ if (!localConfig?.project_id) {

export {
runPostWriteHook
runPostWriteHook,
shouldIgnoreHookPath
};
+11
-4

@@ -287,5 +287,6 @@ #!/usr/bin/env node

var PROJECT_ID = null;
var REMINDER = `[CONTEXTSTREAM] On the first message in every session call mcp__contextstream__init(...), then call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>") FIRST before any other tool. On subsequent messages, default to context first. Narrow bypass is allowed only for immediate read-only ContextStream calls when prior context is fresh and no state-changing tool has run. Response contains dynamic rules, lessons, preferences.
var REMINDER = `[CONTEXTSTREAM] On the first message in every session call mcp__contextstream__init(...), then call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>") FIRST before any other tool. On subsequent messages, default to context first. The fresh-context direct-read lane is limited to workspace list/get; memory list_*; help version/tools/auth; project list/get/index_status; and reminder list/active. Recall, decisions, searches, specific reads, writes, and indexing require context first. Fresh [GROUNDING] is the first continuation retrieval; do not immediately duplicate it with recall. Response contains dynamic rules, lessons, preferences.
SEARCH-FIRST: Use mcp__contextstream__search(mode="auto") before Glob/Grep/Read/Explore/Task/EnterPlanMode. In planning, never use EnterPlanMode or Task(Explore) for file-by-file discovery.
COMMON MEMORY CALLS: list docs via memory(action="list_docs"), list lessons via session(action="get_lessons"), list plans via session(action="list_plans"), list tasks/todos via memory(action="list_tasks"|"list_todos").
HANDOFFS: A generic handoff must create entity(kind="handoff", action="create", body={title,summary,scope,next_steps}); add capsule only for a requested portable bundle/share link. HANDOFF.md or prose alone is not the canonical handoff.
[END]`;

@@ -299,3 +300,4 @@ var FULL_REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]

- Use a consistent session_id for the entire conversation (generate once on first message)
- On subsequent messages, default to context() first. Narrow bypass: immediate read-only ContextStream calls when context is fresh and no state-changing tool has run.
- On subsequent messages, default to context() first. Direct-read bypass is limited to workspace list/get; memory list_*; help version/tools/auth; project list/get/index_status; and reminder list/active when context is fresh and no state-changing tool has run.
- Fresh [GROUNDING] is the first continuation retrieval. Do not immediately duplicate sufficient grounding with session(action="recall").

@@ -318,3 +320,8 @@ 2. FOR CODE SEARCH: Check index status, then search appropriately

5. CHECK THESE from context() response:
5. FOR HANDOFFS: Create the canonical durable handoff entity
\u2705 mcp__contextstream__entity(kind="handoff", action="create", body={title,summary,scope,next_steps})
\u2705 Add a capsule only when a portable bundle or share link is requested
\u274C HANDOFF.md, a scratch prompt, or prose alone is not the canonical handoff
6. CHECK THESE from context() response:
- Lessons: Past mistakes to avoid (shown as warnings)

@@ -325,3 +332,3 @@ - Reminders: Active reminders for this project

6. SKIP CONTEXTSTREAM: If user preference says "skip contextstream", use local tools instead
7. SKIP CONTEXTSTREAM: If user preference says "skip contextstream", use local tools instead
[END]`;

@@ -328,0 +335,0 @@ var ENHANCED_REMINDER_HEADER = `\u2B21 ContextStream \u2014 Smart Context & Memory

{
"name": "@contextstream/mcp-server",
"mcpName": "io.github.contextstreamio/mcp-server",
"version": "0.4.81",
"version": "0.4.82",
"description": "MCP server that gives AI coding assistants persistent memory, semantic code search, a dependency graph, grounded Q&A, and team context (GitHub/Slack/Notion) — works with Claude Code, Cursor, VS Code Copilot, Windsurf, Cline, and any Model Context Protocol client.",

@@ -37,3 +37,3 @@ "type": "module",

"dependencies": {
"@modelcontextprotocol/sdk": ">=1.25.1 <1.28.0",
"@modelcontextprotocol/sdk": ">=1.30.0 <1.31.0",
"ignore": "^7.0.5",

@@ -47,3 +47,3 @@ "zod": "^3.23.8"

"@typescript-eslint/parser": "^8.52.0",
"esbuild": "^0.27.0",
"esbuild": "^0.28.1",
"eslint": "^9.39.2",

@@ -57,3 +57,3 @@ "prettier": "^3.7.4",

"engines": {
"node": ">=18"
"node": ">=20"
},

@@ -94,4 +94,5 @@ "keywords": [

"overrides": {
"qs": "6.14.2"
"@hono/node-server": "^2.1.0",
"qs": "6.15.3"
}
}
+40
-15

@@ -46,3 +46,3 @@ <p align="center">

<p align="center">
<img src="compare1.gif" alt="Side-by-side comparison: an AI coding assistant with ContextStream memory and semantic search vs. without" width="700" />
<img src="contextstream-mcp-comparison.gif" alt="Animated comparison of an AI coding assistant working without ContextStream versus with ContextStream project memory, search, and graph context" width="600" />
</p>

@@ -76,2 +76,17 @@

## Which ContextStream runtime am I using?
ContextStream has independent release lines. A `0.5.x` hosted version and a `0.4.x` npm version are different runtimes, not evidence that either updater is broken.
| Runtime | How to identify it | Version line | Canonical release metadata and notes |
|---|---|---|---|
| **Hosted MCP** | Your editor config uses `https://mcp.contextstream.io/mcp`. `help(action="version")` reports `runtime_type: rust-mcp`. | Rust MCP `0.5.x` | `help(action="version")` returns release notes inline when published and links the [machine-readable R2 manifest](https://pub-68429b9f7857416c9484b75bf1887b96.r2.dev/mcp/latest/version.json). |
| **Installed Rust MCP** | Your editor launches a `contextstream-mcp` binary installed by `https://contextstream.io/scripts/mcp.sh`; run `contextstream-mcp --version`. | Rust MCP `0.5.x` | The same [machine-readable R2 manifest](https://pub-68429b9f7857416c9484b75bf1887b96.r2.dev/mcp/latest/version.json); `help(action="version")` maps this runtime to that manifest. |
| **Legacy npm MCP (this repository)** | Your editor launches `npx ... @contextstream/mcp-server` or a global npm install. `help(action="version")` reports `runtime_type: legacy-typescript-mcp`; `npm list -g @contextstream/mcp-server` shows the installed package. | TypeScript MCP `0.4.x` | [GitHub releases](https://github.com/contextstream/mcp-server/releases) and this repository's [CHANGELOG.md](CHANGELOG.md). |
| **ContextStream Desktop** | Check the app's About/update UI. Desktop may run the local sync bridge, but that does not change the MCP runtime configured in your editor. | Desktop `0.3.x` (independent) | The in-app updater and the public [Desktop version JSON](https://api.contextstream.io/api/v1/desktop/version), which includes `release_notes` and platform downloads. |
The `version` reported by `help(action="version")` is always the MCP process serving that tool call. Desktop's version is separate, even when Desktop added or indexed the local repository.
---
## Why do AI coding assistants forget everything?

@@ -135,20 +150,30 @@

## The tools your AI gets
## Tools
36 tools in the default surface, organized as consolidated domains so they cost ~75% fewer tokens than individual registrations:
36 tools in the default surface, organized as consolidated domains so they cost ~75% fewer tokens than individual registrations. The tools your AI gets:
```
init / context → workspace state + the right context on every message
search → semantic, hybrid, keyword, pattern, exhaustive, refactor modes
memory → events, decisions, docs, runbooks, tasks, todos, diagrams, transcripts
session → capture decisions & lessons, recall past sessions, plans, retroactive capture
qa → grounded Q&A over your workspace knowledge base, with citations
graph → dependencies, impact analysis, circular deps, unused code
capsule → portable, shareable context snapshots for agent handoffs
entity → tickets, incidents, releases, sprints, OKRs, risks
project / workspace / skill / media / vcs / reminder / integration / help
```
- **init** / **context** — workspace state + the right context on every message
- **search** — semantic, hybrid, keyword, pattern, exhaustive, refactor modes
- **memory** — events, decisions, docs, runbooks, tasks, todos, diagrams, transcripts
- **session** — capture decisions & lessons, recall past sessions, plans, retroactive capture
- **qa** — grounded Q&A over your workspace knowledge base, with citations
- **graph** — dependencies, impact analysis, circular deps, unused code
- **capsule** — portable, shareable context snapshots for agent handoffs
- **entity** — tickets, incidents, releases, sprints, OKRs, risks
- **project** / **workspace** — project indexing, scope, and workspace management
- **skill** — reusable instruction + action bundles, portable across tools
- **media** — index and search images, video, audio, and documents
- **vcs** / **reminder** / **integration** / **help** — repo links, reminders, integrations, diagnostics
Plus focused write tools (`capture_plan`, `memory_create_doc`, …) so agents that display tool names show *what* they're doing. Your AI uses all of this automatically — you just code.
Plus focused write tools (`capture_plan`, `memory_create_doc`, `session_capture_lesson`, …) so agents that display tool names show *what* they're doing. Your AI uses all of this automatically — you just code.
### Daily Recaps
Daily Recaps are generated around **23:00 in your configured timezone** when there is enough activity. They are not triggered by closing an editor, changing an MCP `session_id`, or starting a new chat, so long-lived VS Code/Copilot connections do not suppress the nightly job.
- `session(action="list_recaps", workspace_id="<uuid>", limit=30)` lists completed recaps newest-first with `recap_date` and `generated_at` timestamps.
- `session(action="trigger_recap", workspace_id="<uuid>")` queues an asynchronous manual recap. Call `list_recaps` afterward to verify completion.
These actions use the same recap history and generation service as the dashboard.
---

@@ -155,0 +180,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display