@contextstream/mcp-server 0.4.62 → 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.
@@ -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 write plans to markdown files
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
  }