@contextstream/mcp-server 0.4.63 → 0.4.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/auto-rules.js +102 -23
- package/dist/hooks/notification.js +159 -0
- package/dist/hooks/permission-request.js +171 -0
- package/dist/hooks/post-tool-use-failure.js +210 -0
- package/dist/hooks/pre-tool-use.js +14 -5
- package/dist/hooks/runner.js +2300 -5155
- package/dist/hooks/session-init.js +32 -9
- package/dist/hooks/stop.js +168 -0
- package/dist/hooks/subagent-start.js +192 -0
- package/dist/hooks/subagent-stop.js +298 -0
- package/dist/hooks/task-completed.js +257 -0
- package/dist/hooks/teammate-idle.js +197 -0
- package/dist/hooks/user-prompt-submit.js +6 -4
- package/dist/index.js +2780 -1519
- package/dist/test-server.js +4 -2
- package/package.json +2 -2
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/common.ts
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
var DEFAULT_API_URL = "https://api.contextstream.io";
|
|
8
|
+
function readHookInput() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function writeHookOutput(output) {
|
|
16
|
+
const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
|
|
17
|
+
hookSpecificOutput: output.additionalContext ? {
|
|
18
|
+
hookEventName: output.hookEventName,
|
|
19
|
+
additionalContext: output.additionalContext
|
|
20
|
+
} : void 0,
|
|
21
|
+
additionalContext: output.additionalContext,
|
|
22
|
+
blocked: output.blocked,
|
|
23
|
+
reason: output.reason
|
|
24
|
+
} : {};
|
|
25
|
+
console.log(JSON.stringify(payload));
|
|
26
|
+
}
|
|
27
|
+
function extractCwd(input) {
|
|
28
|
+
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
|
|
29
|
+
return cwd;
|
|
30
|
+
}
|
|
31
|
+
function loadHookConfig(cwd) {
|
|
32
|
+
let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
|
|
33
|
+
let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
34
|
+
let jwt = process.env.CONTEXTSTREAM_JWT || "";
|
|
35
|
+
let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
|
|
36
|
+
let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
|
|
37
|
+
let searchDir = path.resolve(cwd);
|
|
38
|
+
for (let i = 0; i < 6; i++) {
|
|
39
|
+
if (!apiKey && !jwt) {
|
|
40
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
41
|
+
if (fs.existsSync(mcpPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
44
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
45
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
46
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
47
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
48
|
+
if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
|
|
49
|
+
if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!workspaceId || !projectId) {
|
|
55
|
+
const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
56
|
+
if (fs.existsSync(localConfigPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
|
|
59
|
+
if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
|
|
60
|
+
if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const parentDir = path.dirname(searchDir);
|
|
66
|
+
if (parentDir === searchDir) break;
|
|
67
|
+
searchDir = parentDir;
|
|
68
|
+
}
|
|
69
|
+
if (!apiKey && !jwt) {
|
|
70
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
71
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
|
|
74
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
75
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
76
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
77
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { apiUrl, apiKey, jwt, workspaceId, projectId };
|
|
83
|
+
}
|
|
84
|
+
function isConfigured(config) {
|
|
85
|
+
return Boolean(config.apiKey || config.jwt);
|
|
86
|
+
}
|
|
87
|
+
function authHeaders(config) {
|
|
88
|
+
if (config.apiKey) {
|
|
89
|
+
return { "X-API-Key": config.apiKey };
|
|
90
|
+
}
|
|
91
|
+
if (config.jwt) {
|
|
92
|
+
return { Authorization: `Bearer ${config.jwt}` };
|
|
93
|
+
}
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
async function apiRequest(config, apiPath, init = {}) {
|
|
97
|
+
const response = await fetch(`${config.apiUrl}${apiPath}`, {
|
|
98
|
+
method: init.method || "GET",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
...authHeaders(config)
|
|
102
|
+
},
|
|
103
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
const json = await response.json();
|
|
109
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
110
|
+
return json.data;
|
|
111
|
+
}
|
|
112
|
+
return json;
|
|
113
|
+
}
|
|
114
|
+
async function postMemoryEvent(config, title, content, tags, eventType = "operation") {
|
|
115
|
+
if (!isConfigured(config) || !config.workspaceId) return;
|
|
116
|
+
await apiRequest(config, "/memory/events", {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: {
|
|
119
|
+
workspace_id: config.workspaceId,
|
|
120
|
+
project_id: config.projectId || void 0,
|
|
121
|
+
event_type: eventType,
|
|
122
|
+
title,
|
|
123
|
+
content: typeof content === "string" ? content : JSON.stringify(content),
|
|
124
|
+
metadata: {
|
|
125
|
+
tags,
|
|
126
|
+
source: "mcp_hook",
|
|
127
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async function listPendingTasks(config, limit = 5) {
|
|
133
|
+
if (!isConfigured(config) || !config.workspaceId) return [];
|
|
134
|
+
try {
|
|
135
|
+
const params = new URLSearchParams();
|
|
136
|
+
params.set("workspace_id", config.workspaceId);
|
|
137
|
+
params.set("status", "pending");
|
|
138
|
+
params.set("limit", String(limit));
|
|
139
|
+
if (config.projectId) params.set("project_id", config.projectId);
|
|
140
|
+
const result = await apiRequest(config, `/tasks?${params.toString()}`);
|
|
141
|
+
if (Array.isArray(result)) return result;
|
|
142
|
+
if (Array.isArray(result?.items)) return result.items;
|
|
143
|
+
if (Array.isArray(result?.tasks)) return result.tasks;
|
|
144
|
+
return [];
|
|
145
|
+
} catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/hooks/teammate-idle.ts
|
|
151
|
+
function firstString(input, keys) {
|
|
152
|
+
for (const key of keys) {
|
|
153
|
+
const value = input[key];
|
|
154
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
155
|
+
}
|
|
156
|
+
return void 0;
|
|
157
|
+
}
|
|
158
|
+
async function runTeammateIdleHook() {
|
|
159
|
+
const input = readHookInput();
|
|
160
|
+
const cwd = extractCwd(input);
|
|
161
|
+
const config = loadHookConfig(cwd);
|
|
162
|
+
const teammateName = firstString(input, ["teammate_name", "teammateName", "agent_name"]) || "teammate";
|
|
163
|
+
const teamName = firstString(input, ["team_name", "teamName"]) || "team";
|
|
164
|
+
if (!isConfigured(config)) {
|
|
165
|
+
writeHookOutput();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const pendingTasks = await listPendingTasks(config, 5);
|
|
169
|
+
await postMemoryEvent(
|
|
170
|
+
config,
|
|
171
|
+
"Teammate idle",
|
|
172
|
+
{
|
|
173
|
+
teammate_name: teammateName,
|
|
174
|
+
team_name: teamName,
|
|
175
|
+
pending_tasks: pendingTasks.slice(0, 5),
|
|
176
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
177
|
+
},
|
|
178
|
+
["hook", "teammate", "idle"]
|
|
179
|
+
).catch(() => {
|
|
180
|
+
});
|
|
181
|
+
const shouldRedirect = process.env.CONTEXTSTREAM_TEAMMATE_IDLE_REDIRECT !== "false";
|
|
182
|
+
if (shouldRedirect && pendingTasks.length > 0) {
|
|
183
|
+
const firstTask = pendingTasks[0];
|
|
184
|
+
const taskTitle = typeof firstTask.title === "string" && firstTask.title || typeof firstTask.subject === "string" && firstTask.subject || typeof firstTask.name === "string" && firstTask.name || "pending task";
|
|
185
|
+
const message = `Pending ContextStream task available: ${taskTitle}. Continue and complete this task before idling.`;
|
|
186
|
+
writeHookOutput({ additionalContext: message, blocked: true, reason: message });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
writeHookOutput();
|
|
190
|
+
}
|
|
191
|
+
var isDirectRun = process.argv[1]?.includes("teammate-idle") || process.argv[2] === "teammate-idle";
|
|
192
|
+
if (isDirectRun) {
|
|
193
|
+
runTeammateIdleHook().catch(() => process.exit(0));
|
|
194
|
+
}
|
|
195
|
+
export {
|
|
196
|
+
runTeammateIdleHook
|
|
197
|
+
};
|
|
@@ -283,6 +283,7 @@ var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
|
283
283
|
var WORKSPACE_ID = null;
|
|
284
284
|
var PROJECT_ID = null;
|
|
285
285
|
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.
|
|
286
|
+
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.
|
|
286
287
|
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").
|
|
287
288
|
[END]`;
|
|
288
289
|
var FULL_REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]
|
|
@@ -296,7 +297,7 @@ var FULL_REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]
|
|
|
296
297
|
|
|
297
298
|
2. FOR CODE SEARCH: Check index status, then search appropriately
|
|
298
299
|
\u26A0\uFE0F BEFORE searching: mcp__contextstream__project(action="index_status")
|
|
299
|
-
\u2705 IF indexed & fresh: Use mcp__contextstream__search(mode="auto", query="...")
|
|
300
|
+
\u2705 IF indexed & fresh: Use mcp__contextstream__search(mode="auto", query="...") BEFORE Glob/Grep/Read/Explore/Task/EnterPlanMode
|
|
300
301
|
\u2705 IF NOT indexed OR stale: Use local tools (Glob/Grep/Read) directly
|
|
301
302
|
\u2705 IF search returns 0 results: Fallback to local tools (Glob/Grep/Read)
|
|
302
303
|
|
|
@@ -309,7 +310,8 @@ var FULL_REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]
|
|
|
309
310
|
4. FOR PLANS & TASKS: Use ContextStream, not file-based plans
|
|
310
311
|
\u2705 Plans: mcp__contextstream__session(action="capture_plan", ...)
|
|
311
312
|
\u2705 Tasks: mcp__contextstream__memory(action="create_task", ...)
|
|
312
|
-
\u274C DO NOT use EnterPlanMode or
|
|
313
|
+
\u274C DO NOT use EnterPlanMode or Task(subagent_type="Explore") for file-by-file discovery
|
|
314
|
+
\u2705 For planning discovery: mcp__contextstream__search(mode="auto", query="...", output_format="paths")
|
|
313
315
|
|
|
314
316
|
5. CHECK THESE from context() response:
|
|
315
317
|
- Lessons: Past mistakes to avoid (shown as warnings)
|
|
@@ -664,7 +666,7 @@ function buildEnhancedReminder(ctx, isNewSession2, versionNotice) {
|
|
|
664
666
|
2. Wait for indexing: if \`init\` returns \`indexing_status: "started"\`, files are being indexed
|
|
665
667
|
3. Generate a unique session_id (e.g., "session-" + timestamp or UUID) - use this for ALL context() calls
|
|
666
668
|
4. Call \`context(user_message="...", save_exchange=true, session_id="<your-session-id>")\` for task-specific context
|
|
667
|
-
5. Use \`search(mode="auto")\` for code discovery (not Glob/Grep/Read)
|
|
669
|
+
5. Use \`search(mode="auto")\` for code discovery (not Glob/Grep/Read/Explore/Task/EnterPlanMode)
|
|
668
670
|
|
|
669
671
|
`);
|
|
670
672
|
}
|
|
@@ -827,7 +829,7 @@ async function runUserPromptSubmitHook() {
|
|
|
827
829
|
const newSession = isNewSession(input, editorFormat);
|
|
828
830
|
const [ctx, versionNotice] = await Promise.all([fetchSessionContext(), versionNoticePromise, saveExchangePromise]);
|
|
829
831
|
if (editorFormat === "cursor") {
|
|
830
|
-
let cursorReminder = ctx?.lessons?.length ? `[CONTEXTSTREAM] \u26A0\uFE0F ${ctx.lessons.length} lessons from past mistakes. Call context(save_exchange=true, session_id="...") FIRST. Use search(mode="auto") before Glob/Grep. After file edits: project(action="index").` : `[CONTEXTSTREAM] Call context(save_exchange=true, session_id="...") FIRST. Use search(mode="auto") before Glob/Grep/Read. After file edits: project(action="index").`;
|
|
832
|
+
let cursorReminder = ctx?.lessons?.length ? `[CONTEXTSTREAM] \u26A0\uFE0F ${ctx.lessons.length} lessons from past mistakes. Call context(save_exchange=true, session_id="...") FIRST. Use search(mode="auto") before Glob/Grep/Read/Explore/Task/EnterPlanMode. After file edits: project(action="index").` : `[CONTEXTSTREAM] Call context(save_exchange=true, session_id="...") FIRST. Use search(mode="auto") before Glob/Grep/Read/Explore/Task/EnterPlanMode. After file edits: project(action="index").`;
|
|
831
833
|
if (versionNotice?.behind) {
|
|
832
834
|
cursorReminder += ` [UPDATE v${versionNotice.current}\u2192${versionNotice.latest}]`;
|
|
833
835
|
}
|