@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,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/subagent-stop.ts
|
|
4
|
+
import * as fs2 from "node:fs";
|
|
5
|
+
|
|
6
|
+
// src/hooks/common.ts
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
var DEFAULT_API_URL = "https://api.contextstream.io";
|
|
11
|
+
function readHookInput() {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
14
|
+
} catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function writeHookOutput(output) {
|
|
19
|
+
const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
|
|
20
|
+
hookSpecificOutput: output.additionalContext ? {
|
|
21
|
+
hookEventName: output.hookEventName,
|
|
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 createPlan(config, title, description) {
|
|
136
|
+
if (!isConfigured(config) || !config.workspaceId) return null;
|
|
137
|
+
try {
|
|
138
|
+
const result = await apiRequest(config, "/plans", {
|
|
139
|
+
method: "POST",
|
|
140
|
+
body: {
|
|
141
|
+
workspace_id: config.workspaceId,
|
|
142
|
+
project_id: config.projectId || void 0,
|
|
143
|
+
title,
|
|
144
|
+
description
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return typeof result?.id === "string" ? result.id : null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function createTask(config, params) {
|
|
153
|
+
if (!isConfigured(config) || !config.workspaceId) return null;
|
|
154
|
+
try {
|
|
155
|
+
const result = await apiRequest(config, "/tasks", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
body: {
|
|
158
|
+
workspace_id: config.workspaceId,
|
|
159
|
+
project_id: config.projectId || void 0,
|
|
160
|
+
title: params.title,
|
|
161
|
+
description: params.description,
|
|
162
|
+
plan_id: params.planId || void 0,
|
|
163
|
+
status: params.status
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return typeof result?.id === "string" ? result.id : null;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/hooks/subagent-stop.ts
|
|
173
|
+
function parseTranscript(transcriptPath) {
|
|
174
|
+
try {
|
|
175
|
+
const content = fs2.readFileSync(transcriptPath, "utf8");
|
|
176
|
+
const assistantMessages = [];
|
|
177
|
+
let toolCallCount = 0;
|
|
178
|
+
for (const rawLine of content.split("\n")) {
|
|
179
|
+
const line = rawLine.trim();
|
|
180
|
+
if (!line) continue;
|
|
181
|
+
let entry;
|
|
182
|
+
try {
|
|
183
|
+
entry = JSON.parse(line);
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (entry.type === "tool_use") {
|
|
188
|
+
toolCallCount += 1;
|
|
189
|
+
}
|
|
190
|
+
const isAssistant = entry.type === "assistant" || entry.role === "assistant" || Array.isArray(entry.content) && entry.role === "assistant";
|
|
191
|
+
if (!isAssistant) continue;
|
|
192
|
+
const contentValue = entry.content;
|
|
193
|
+
if (typeof contentValue === "string" && contentValue.trim()) {
|
|
194
|
+
assistantMessages.push(contentValue.trim());
|
|
195
|
+
} else if (Array.isArray(contentValue)) {
|
|
196
|
+
const text = contentValue.map((item) => typeof item?.text === "string" ? item.text : typeof item === "string" ? item : "").filter(Boolean).join("\n").trim();
|
|
197
|
+
if (text) assistantMessages.push(text);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { assistantMessages, toolCallCount };
|
|
201
|
+
} catch {
|
|
202
|
+
return { assistantMessages: [], toolCallCount: 0 };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function derivePlanTitle(summary, agentId) {
|
|
206
|
+
for (const line of summary.split("\n")) {
|
|
207
|
+
const trimmed = line.trim();
|
|
208
|
+
if (trimmed.startsWith("#")) {
|
|
209
|
+
return trimmed.replace(/^#+\s*/, "").slice(0, 120);
|
|
210
|
+
}
|
|
211
|
+
if (trimmed) return trimmed.slice(0, 120);
|
|
212
|
+
}
|
|
213
|
+
return `Plan generated by ${agentId}`;
|
|
214
|
+
}
|
|
215
|
+
function extractPlanTasks(summary) {
|
|
216
|
+
const tasks = [];
|
|
217
|
+
for (const line of summary.split("\n")) {
|
|
218
|
+
const trimmed = line.trim();
|
|
219
|
+
if (/^[-*]\s+/.test(trimmed) || /^\d+\.\s+/.test(trimmed)) {
|
|
220
|
+
const task = trimmed.replace(/^([-*]|\d+\.)\s+/, "").trim();
|
|
221
|
+
if (task) tasks.push(task);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return Array.from(new Set(tasks)).slice(0, 20);
|
|
225
|
+
}
|
|
226
|
+
async function runSubagentStopHook() {
|
|
227
|
+
const input = readHookInput();
|
|
228
|
+
const cwd = extractCwd(input);
|
|
229
|
+
const config = loadHookConfig(cwd);
|
|
230
|
+
if (!isConfigured(config)) {
|
|
231
|
+
writeHookOutput();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const agentType = typeof input.agent_type === "string" && input.agent_type || typeof input.subagent_type === "string" && input.subagent_type || "unknown";
|
|
235
|
+
const agentId = typeof input.agent_id === "string" && input.agent_id || "unknown";
|
|
236
|
+
const transcriptPath = typeof input.agent_transcript_path === "string" && input.agent_transcript_path || typeof input.transcript_path === "string" && input.transcript_path || "";
|
|
237
|
+
const transcript = transcriptPath && fs2.existsSync(transcriptPath) ? parseTranscript(transcriptPath) : { assistantMessages: [], toolCallCount: 0 };
|
|
238
|
+
const summaryText = transcript.assistantMessages.join("\n\n") || typeof input.summary === "string" && input.summary || "(No assistant output found in subagent transcript.)";
|
|
239
|
+
if (agentType.toLowerCase() === "plan") {
|
|
240
|
+
const suppliedPlanId = typeof input.plan_id === "string" && input.plan_id || typeof input.planId === "string" && input.planId || null;
|
|
241
|
+
const planTitle = derivePlanTitle(summaryText, agentId);
|
|
242
|
+
const planId = suppliedPlanId || await createPlan(config, planTitle, summaryText.slice(0, 12e3));
|
|
243
|
+
const tasks = extractPlanTasks(summaryText);
|
|
244
|
+
let createdTaskCount = 0;
|
|
245
|
+
if (planId) {
|
|
246
|
+
for (const task of tasks) {
|
|
247
|
+
const created = await createTask(config, {
|
|
248
|
+
title: task,
|
|
249
|
+
planId,
|
|
250
|
+
status: "pending"
|
|
251
|
+
});
|
|
252
|
+
if (created) createdTaskCount += 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
await postMemoryEvent(
|
|
256
|
+
config,
|
|
257
|
+
"Plan subagent captured",
|
|
258
|
+
{
|
|
259
|
+
agent_type: "Plan",
|
|
260
|
+
agent_id: agentId,
|
|
261
|
+
plan_title: planTitle,
|
|
262
|
+
plan_id: planId,
|
|
263
|
+
supplied_plan_id: suppliedPlanId,
|
|
264
|
+
extracted_task_count: tasks.length,
|
|
265
|
+
created_task_count: createdTaskCount,
|
|
266
|
+
assistant_message_count: transcript.assistantMessages.length,
|
|
267
|
+
tool_call_count: transcript.toolCallCount,
|
|
268
|
+
summary_preview: summaryText.slice(0, 2e3),
|
|
269
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
270
|
+
},
|
|
271
|
+
["hook", "subagent_stop", "plan"]
|
|
272
|
+
).catch(() => {
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
await postMemoryEvent(
|
|
276
|
+
config,
|
|
277
|
+
`Subagent finished: ${agentType}`,
|
|
278
|
+
{
|
|
279
|
+
agent_type: agentType,
|
|
280
|
+
agent_id: agentId,
|
|
281
|
+
summary: summaryText.slice(0, 4e3),
|
|
282
|
+
assistant_message_count: transcript.assistantMessages.length,
|
|
283
|
+
tool_call_count: transcript.toolCallCount,
|
|
284
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
285
|
+
},
|
|
286
|
+
["hook", "subagent_stop", agentType]
|
|
287
|
+
).catch(() => {
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
writeHookOutput();
|
|
291
|
+
}
|
|
292
|
+
var isDirectRun = process.argv[1]?.includes("subagent-stop") || process.argv[2] === "subagent-stop";
|
|
293
|
+
if (isDirectRun) {
|
|
294
|
+
runSubagentStopHook().catch(() => process.exit(0));
|
|
295
|
+
}
|
|
296
|
+
export {
|
|
297
|
+
runSubagentStopHook
|
|
298
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
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 createTask(config, params) {
|
|
133
|
+
if (!isConfigured(config) || !config.workspaceId) return null;
|
|
134
|
+
try {
|
|
135
|
+
const result = await apiRequest(config, "/tasks", {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: {
|
|
138
|
+
workspace_id: config.workspaceId,
|
|
139
|
+
project_id: config.projectId || void 0,
|
|
140
|
+
title: params.title,
|
|
141
|
+
description: params.description,
|
|
142
|
+
plan_id: params.planId || void 0,
|
|
143
|
+
status: params.status
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return typeof result?.id === "string" ? result.id : null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function updateTaskStatus(config, taskId, status, title, description) {
|
|
152
|
+
if (!isConfigured(config) || !taskId) return false;
|
|
153
|
+
try {
|
|
154
|
+
await apiRequest(config, `/tasks/${taskId}`, {
|
|
155
|
+
method: "PATCH",
|
|
156
|
+
body: {
|
|
157
|
+
status,
|
|
158
|
+
...title ? { title } : {},
|
|
159
|
+
...description ? { description } : {}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/hooks/task-completed.ts
|
|
169
|
+
function firstString(input, keys) {
|
|
170
|
+
for (const key of keys) {
|
|
171
|
+
const value = input[key];
|
|
172
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
173
|
+
}
|
|
174
|
+
const task = input.task;
|
|
175
|
+
if (task && typeof task === "object") {
|
|
176
|
+
return firstString(task, keys);
|
|
177
|
+
}
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
function looksLikeRecoveryTask(description) {
|
|
181
|
+
const lower = description.toLowerCase();
|
|
182
|
+
return ["error", "failure", "retry", "recover", "fix", "incident"].some(
|
|
183
|
+
(keyword) => lower.includes(keyword)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
async function runTaskCompletedHook() {
|
|
187
|
+
const input = readHookInput();
|
|
188
|
+
const cwd = extractCwd(input);
|
|
189
|
+
const config = loadHookConfig(cwd);
|
|
190
|
+
const taskId = firstString(input, ["task_id", "taskId"]) || "";
|
|
191
|
+
const taskSubject = firstString(input, ["task_subject", "task_title", "title", "subject", "description"]) || "Completed task";
|
|
192
|
+
const taskDescription = firstString(input, ["task_description", "details", "content"]);
|
|
193
|
+
const planId = firstString(input, ["plan_id", "planId"]);
|
|
194
|
+
if (process.env.CONTEXTSTREAM_TASK_COMPLETED_REQUIRE_SUBJECT === "true" && !taskSubject.trim()) {
|
|
195
|
+
writeHookOutput({ blocked: true, reason: "TaskCompleted requires a non-empty task subject" });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (isConfigured(config)) {
|
|
199
|
+
let updated = false;
|
|
200
|
+
if (taskId) {
|
|
201
|
+
updated = await updateTaskStatus(
|
|
202
|
+
config,
|
|
203
|
+
taskId,
|
|
204
|
+
"completed",
|
|
205
|
+
taskSubject,
|
|
206
|
+
taskDescription
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (!updated) {
|
|
210
|
+
await createTask(config, {
|
|
211
|
+
title: taskSubject,
|
|
212
|
+
description: taskDescription,
|
|
213
|
+
planId: planId || void 0,
|
|
214
|
+
status: "completed"
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
await postMemoryEvent(
|
|
218
|
+
config,
|
|
219
|
+
"Task completed",
|
|
220
|
+
{
|
|
221
|
+
task_id: taskId || null,
|
|
222
|
+
title: taskSubject,
|
|
223
|
+
description: taskDescription,
|
|
224
|
+
plan_id: planId || null,
|
|
225
|
+
agent_id: firstString(input, ["agent_id", "agentId"]) || null,
|
|
226
|
+
team_name: firstString(input, ["team_name", "teamName"]) || null,
|
|
227
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
228
|
+
source: "task_completed_hook"
|
|
229
|
+
},
|
|
230
|
+
["hook", "task", "completed"],
|
|
231
|
+
"task"
|
|
232
|
+
).catch(() => {
|
|
233
|
+
});
|
|
234
|
+
if (taskDescription && looksLikeRecoveryTask(taskDescription)) {
|
|
235
|
+
await postMemoryEvent(
|
|
236
|
+
config,
|
|
237
|
+
"Lesson from task completion",
|
|
238
|
+
{
|
|
239
|
+
task: taskSubject,
|
|
240
|
+
description: taskDescription,
|
|
241
|
+
lesson: "Recovered from an execution issue; consider codifying this into tests/guards."
|
|
242
|
+
},
|
|
243
|
+
["hook", "lesson", "task_completed"],
|
|
244
|
+
"lesson"
|
|
245
|
+
).catch(() => {
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
writeHookOutput();
|
|
250
|
+
}
|
|
251
|
+
var isDirectRun = process.argv[1]?.includes("task-completed") || process.argv[2] === "task-completed";
|
|
252
|
+
if (isDirectRun) {
|
|
253
|
+
runTaskCompletedHook().catch(() => process.exit(0));
|
|
254
|
+
}
|
|
255
|
+
export {
|
|
256
|
+
runTaskCompletedHook
|
|
257
|
+
};
|