@contextstream/mcp-server 0.4.82 → 1.0.0

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.
@@ -1,260 +0,0 @@
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
- var HOOK_SPECIFIC_OUTPUT_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit", "PostToolUse"]);
9
- function readHookInput() {
10
- try {
11
- return JSON.parse(fs.readFileSync(0, "utf8"));
12
- } catch {
13
- return {};
14
- }
15
- }
16
- function writeHookOutput(output) {
17
- const eventName = output?.hookEventName || (typeof process.env.HOOK_EVENT_NAME === "string" ? process.env.HOOK_EVENT_NAME.trim() : "");
18
- const canUseHookSpecificOutput = HOOK_SPECIFIC_OUTPUT_EVENTS.has(eventName);
19
- const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
20
- hookSpecificOutput: output.additionalContext && canUseHookSpecificOutput ? {
21
- hookEventName: eventName,
22
- additionalContext: output.additionalContext
23
- } : void 0,
24
- additionalContext: output.additionalContext,
25
- blocked: output.blocked,
26
- reason: output.reason
27
- } : {};
28
- console.log(JSON.stringify(payload));
29
- }
30
- function extractCwd(input) {
31
- const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
32
- return cwd;
33
- }
34
- function loadHookConfig(cwd) {
35
- let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
36
- let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
37
- let jwt = process.env.CONTEXTSTREAM_JWT || "";
38
- let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
39
- let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
40
- let searchDir = path.resolve(cwd);
41
- for (let i = 0; i < 6; i++) {
42
- if (!apiKey && !jwt) {
43
- const mcpPath = path.join(searchDir, ".mcp.json");
44
- if (fs.existsSync(mcpPath)) {
45
- try {
46
- const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
47
- const env = config.mcpServers?.contextstream?.env;
48
- if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
49
- if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
50
- if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
51
- if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
52
- if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
53
- } catch {
54
- }
55
- }
56
- }
57
- if (!workspaceId || !projectId) {
58
- const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
59
- if (fs.existsSync(localConfigPath)) {
60
- try {
61
- const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
62
- if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
63
- if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
64
- } catch {
65
- }
66
- }
67
- }
68
- const parentDir = path.dirname(searchDir);
69
- if (parentDir === searchDir) break;
70
- searchDir = parentDir;
71
- }
72
- if (!apiKey && !jwt) {
73
- const homeMcpPath = path.join(homedir(), ".mcp.json");
74
- if (fs.existsSync(homeMcpPath)) {
75
- try {
76
- const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
77
- const env = config.mcpServers?.contextstream?.env;
78
- if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
79
- if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
80
- if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
81
- } catch {
82
- }
83
- }
84
- }
85
- return { apiUrl, apiKey, jwt, workspaceId, projectId };
86
- }
87
- function isConfigured(config) {
88
- return Boolean(config.apiKey || config.jwt);
89
- }
90
- function authHeaders(config) {
91
- if (config.apiKey) {
92
- return { "X-API-Key": config.apiKey };
93
- }
94
- if (config.jwt) {
95
- return { Authorization: `Bearer ${config.jwt}` };
96
- }
97
- return {};
98
- }
99
- async function apiRequest(config, apiPath, init = {}) {
100
- const response = await fetch(`${config.apiUrl}${apiPath}`, {
101
- method: init.method || "GET",
102
- headers: {
103
- "Content-Type": "application/json",
104
- ...authHeaders(config)
105
- },
106
- body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
107
- });
108
- if (!response.ok) {
109
- throw new Error(`${response.status} ${response.statusText}`);
110
- }
111
- const json = await response.json();
112
- if (json && typeof json === "object" && "data" in json) {
113
- return json.data;
114
- }
115
- return json;
116
- }
117
- async function postMemoryEvent(config, title, content, tags, eventType = "operation") {
118
- if (!isConfigured(config) || !config.workspaceId) return;
119
- await apiRequest(config, "/memory/events", {
120
- method: "POST",
121
- body: {
122
- workspace_id: config.workspaceId,
123
- project_id: config.projectId || void 0,
124
- event_type: eventType,
125
- title,
126
- content: typeof content === "string" ? content : JSON.stringify(content),
127
- metadata: {
128
- tags,
129
- source: "mcp_hook",
130
- captured_at: (/* @__PURE__ */ new Date()).toISOString()
131
- }
132
- }
133
- });
134
- }
135
- async function createTask(config, params) {
136
- if (!isConfigured(config) || !config.workspaceId) return null;
137
- try {
138
- const result = await apiRequest(config, "/tasks", {
139
- method: "POST",
140
- body: {
141
- workspace_id: config.workspaceId,
142
- project_id: config.projectId || void 0,
143
- title: params.title,
144
- description: params.description,
145
- plan_id: params.planId || void 0,
146
- status: params.status
147
- }
148
- });
149
- return typeof result?.id === "string" ? result.id : null;
150
- } catch {
151
- return null;
152
- }
153
- }
154
- async function updateTaskStatus(config, taskId, status, title, description) {
155
- if (!isConfigured(config) || !taskId) return false;
156
- try {
157
- await apiRequest(config, `/tasks/${taskId}`, {
158
- method: "PATCH",
159
- body: {
160
- status,
161
- ...title ? { title } : {},
162
- ...description ? { description } : {}
163
- }
164
- });
165
- return true;
166
- } catch {
167
- return false;
168
- }
169
- }
170
-
171
- // src/hooks/task-completed.ts
172
- function firstString(input, keys) {
173
- for (const key of keys) {
174
- const value = input[key];
175
- if (typeof value === "string" && value.trim()) return value.trim();
176
- }
177
- const task = input.task;
178
- if (task && typeof task === "object") {
179
- return firstString(task, keys);
180
- }
181
- return void 0;
182
- }
183
- function looksLikeRecoveryTask(description) {
184
- const lower = description.toLowerCase();
185
- return ["error", "failure", "retry", "recover", "fix", "incident"].some(
186
- (keyword) => lower.includes(keyword)
187
- );
188
- }
189
- async function runTaskCompletedHook() {
190
- const input = readHookInput();
191
- const cwd = extractCwd(input);
192
- const config = loadHookConfig(cwd);
193
- const taskId = firstString(input, ["task_id", "taskId"]) || "";
194
- const taskSubject = firstString(input, ["task_subject", "task_title", "title", "subject", "description"]) || "Completed task";
195
- const taskDescription = firstString(input, ["task_description", "details", "content"]);
196
- const planId = firstString(input, ["plan_id", "planId"]);
197
- if (process.env.CONTEXTSTREAM_TASK_COMPLETED_REQUIRE_SUBJECT === "true" && !taskSubject.trim()) {
198
- writeHookOutput({ blocked: true, reason: "TaskCompleted requires a non-empty task subject" });
199
- return;
200
- }
201
- if (isConfigured(config)) {
202
- let updated = false;
203
- if (taskId) {
204
- updated = await updateTaskStatus(
205
- config,
206
- taskId,
207
- "completed",
208
- taskSubject,
209
- taskDescription
210
- );
211
- }
212
- if (!updated) {
213
- await createTask(config, {
214
- title: taskSubject,
215
- description: taskDescription,
216
- planId: planId || void 0,
217
- status: "completed"
218
- });
219
- }
220
- await postMemoryEvent(
221
- config,
222
- "Task completed",
223
- {
224
- task_id: taskId || null,
225
- title: taskSubject,
226
- description: taskDescription,
227
- plan_id: planId || null,
228
- agent_id: firstString(input, ["agent_id", "agentId"]) || null,
229
- team_name: firstString(input, ["team_name", "teamName"]) || null,
230
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
231
- source: "task_completed_hook"
232
- },
233
- ["hook", "task", "completed"],
234
- "task"
235
- ).catch(() => {
236
- });
237
- if (taskDescription && looksLikeRecoveryTask(taskDescription)) {
238
- await postMemoryEvent(
239
- config,
240
- "Lesson from task completion",
241
- {
242
- task: taskSubject,
243
- description: taskDescription,
244
- lesson: "Recovered from an execution issue; consider codifying this into tests/guards."
245
- },
246
- ["hook", "lesson", "task_completed"],
247
- "lesson"
248
- ).catch(() => {
249
- });
250
- }
251
- }
252
- writeHookOutput();
253
- }
254
- var isDirectRun = process.argv[1]?.includes("task-completed") || process.argv[2] === "task-completed";
255
- if (isDirectRun) {
256
- runTaskCompletedHook().catch(() => process.exit(0));
257
- }
258
- export {
259
- runTaskCompletedHook
260
- };
@@ -1,200 +0,0 @@
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
- var HOOK_SPECIFIC_OUTPUT_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit", "PostToolUse"]);
9
- function readHookInput() {
10
- try {
11
- return JSON.parse(fs.readFileSync(0, "utf8"));
12
- } catch {
13
- return {};
14
- }
15
- }
16
- function writeHookOutput(output) {
17
- const eventName = output?.hookEventName || (typeof process.env.HOOK_EVENT_NAME === "string" ? process.env.HOOK_EVENT_NAME.trim() : "");
18
- const canUseHookSpecificOutput = HOOK_SPECIFIC_OUTPUT_EVENTS.has(eventName);
19
- const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
20
- hookSpecificOutput: output.additionalContext && canUseHookSpecificOutput ? {
21
- hookEventName: eventName,
22
- additionalContext: output.additionalContext
23
- } : void 0,
24
- additionalContext: output.additionalContext,
25
- blocked: output.blocked,
26
- reason: output.reason
27
- } : {};
28
- console.log(JSON.stringify(payload));
29
- }
30
- function extractCwd(input) {
31
- const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
32
- return cwd;
33
- }
34
- function loadHookConfig(cwd) {
35
- let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
36
- let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
37
- let jwt = process.env.CONTEXTSTREAM_JWT || "";
38
- let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
39
- let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
40
- let searchDir = path.resolve(cwd);
41
- for (let i = 0; i < 6; i++) {
42
- if (!apiKey && !jwt) {
43
- const mcpPath = path.join(searchDir, ".mcp.json");
44
- if (fs.existsSync(mcpPath)) {
45
- try {
46
- const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
47
- const env = config.mcpServers?.contextstream?.env;
48
- if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
49
- if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
50
- if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
51
- if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
52
- if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
53
- } catch {
54
- }
55
- }
56
- }
57
- if (!workspaceId || !projectId) {
58
- const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
59
- if (fs.existsSync(localConfigPath)) {
60
- try {
61
- const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
62
- if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
63
- if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
64
- } catch {
65
- }
66
- }
67
- }
68
- const parentDir = path.dirname(searchDir);
69
- if (parentDir === searchDir) break;
70
- searchDir = parentDir;
71
- }
72
- if (!apiKey && !jwt) {
73
- const homeMcpPath = path.join(homedir(), ".mcp.json");
74
- if (fs.existsSync(homeMcpPath)) {
75
- try {
76
- const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
77
- const env = config.mcpServers?.contextstream?.env;
78
- if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
79
- if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
80
- if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
81
- } catch {
82
- }
83
- }
84
- }
85
- return { apiUrl, apiKey, jwt, workspaceId, projectId };
86
- }
87
- function isConfigured(config) {
88
- return Boolean(config.apiKey || config.jwt);
89
- }
90
- function authHeaders(config) {
91
- if (config.apiKey) {
92
- return { "X-API-Key": config.apiKey };
93
- }
94
- if (config.jwt) {
95
- return { Authorization: `Bearer ${config.jwt}` };
96
- }
97
- return {};
98
- }
99
- async function apiRequest(config, apiPath, init = {}) {
100
- const response = await fetch(`${config.apiUrl}${apiPath}`, {
101
- method: init.method || "GET",
102
- headers: {
103
- "Content-Type": "application/json",
104
- ...authHeaders(config)
105
- },
106
- body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
107
- });
108
- if (!response.ok) {
109
- throw new Error(`${response.status} ${response.statusText}`);
110
- }
111
- const json = await response.json();
112
- if (json && typeof json === "object" && "data" in json) {
113
- return json.data;
114
- }
115
- return json;
116
- }
117
- async function postMemoryEvent(config, title, content, tags, eventType = "operation") {
118
- if (!isConfigured(config) || !config.workspaceId) return;
119
- await apiRequest(config, "/memory/events", {
120
- method: "POST",
121
- body: {
122
- workspace_id: config.workspaceId,
123
- project_id: config.projectId || void 0,
124
- event_type: eventType,
125
- title,
126
- content: typeof content === "string" ? content : JSON.stringify(content),
127
- metadata: {
128
- tags,
129
- source: "mcp_hook",
130
- captured_at: (/* @__PURE__ */ new Date()).toISOString()
131
- }
132
- }
133
- });
134
- }
135
- async function listPendingTasks(config, limit = 5) {
136
- if (!isConfigured(config) || !config.workspaceId) return [];
137
- try {
138
- const params = new URLSearchParams();
139
- params.set("workspace_id", config.workspaceId);
140
- params.set("status", "pending");
141
- params.set("limit", String(limit));
142
- if (config.projectId) params.set("project_id", config.projectId);
143
- const result = await apiRequest(config, `/tasks?${params.toString()}`);
144
- if (Array.isArray(result)) return result;
145
- if (Array.isArray(result?.items)) return result.items;
146
- if (Array.isArray(result?.tasks)) return result.tasks;
147
- return [];
148
- } catch {
149
- return [];
150
- }
151
- }
152
-
153
- // src/hooks/teammate-idle.ts
154
- function firstString(input, keys) {
155
- for (const key of keys) {
156
- const value = input[key];
157
- if (typeof value === "string" && value.trim()) return value.trim();
158
- }
159
- return void 0;
160
- }
161
- async function runTeammateIdleHook() {
162
- const input = readHookInput();
163
- const cwd = extractCwd(input);
164
- const config = loadHookConfig(cwd);
165
- const teammateName = firstString(input, ["teammate_name", "teammateName", "agent_name"]) || "teammate";
166
- const teamName = firstString(input, ["team_name", "teamName"]) || "team";
167
- if (!isConfigured(config)) {
168
- writeHookOutput();
169
- return;
170
- }
171
- const pendingTasks = await listPendingTasks(config, 5);
172
- await postMemoryEvent(
173
- config,
174
- "Teammate idle",
175
- {
176
- teammate_name: teammateName,
177
- team_name: teamName,
178
- pending_tasks: pendingTasks.slice(0, 5),
179
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
180
- },
181
- ["hook", "teammate", "idle"]
182
- ).catch(() => {
183
- });
184
- const shouldRedirect = process.env.CONTEXTSTREAM_TEAMMATE_IDLE_REDIRECT !== "false";
185
- if (shouldRedirect && pendingTasks.length > 0) {
186
- const firstTask = pendingTasks[0];
187
- const taskTitle = typeof firstTask.title === "string" && firstTask.title || typeof firstTask.subject === "string" && firstTask.subject || typeof firstTask.name === "string" && firstTask.name || "pending task";
188
- const message = `Pending ContextStream task available: ${taskTitle}. Continue and complete this task before idling.`;
189
- writeHookOutput({ additionalContext: message, blocked: true, reason: message });
190
- return;
191
- }
192
- writeHookOutput();
193
- }
194
- var isDirectRun = process.argv[1]?.includes("teammate-idle") || process.argv[2] === "teammate-idle";
195
- if (isDirectRun) {
196
- runTeammateIdleHook().catch(() => process.exit(0));
197
- }
198
- export {
199
- runTeammateIdleHook
200
- };