@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.
- package/NOTICE +25 -0
- package/README.md +77 -353
- package/npm/bin/contextstream-hook.mjs +9 -0
- package/npm/bin/contextstream-mcp.mjs +9 -0
- package/npm/bin/mcp-server.mjs +9 -0
- package/npm/launcher.mjs +390 -0
- package/package.json +27 -72
- package/server.json +43 -0
- package/dist/hooks/auto-rules.js +0 -3529
- package/dist/hooks/media-aware.js +0 -103
- package/dist/hooks/notification.js +0 -162
- package/dist/hooks/on-bash.js +0 -190
- package/dist/hooks/on-read.js +0 -321
- package/dist/hooks/on-save-intent.js +0 -132
- package/dist/hooks/on-task.js +0 -139
- package/dist/hooks/on-web.js +0 -155
- package/dist/hooks/permission-request.js +0 -174
- package/dist/hooks/post-compact.js +0 -172
- package/dist/hooks/post-tool-use-failure.js +0 -213
- package/dist/hooks/post-write.js +0 -1023
- package/dist/hooks/pre-compact.js +0 -317
- package/dist/hooks/pre-tool-use.js +0 -553
- package/dist/hooks/runner.js +0 -5826
- package/dist/hooks/session-end.js +0 -285
- package/dist/hooks/session-init.js +0 -1880
- package/dist/hooks/stop.js +0 -171
- package/dist/hooks/subagent-start.js +0 -195
- package/dist/hooks/subagent-stop.js +0 -301
- package/dist/hooks/task-completed.js +0 -260
- package/dist/hooks/teammate-idle.js +0 -200
- package/dist/hooks/user-prompt-submit.js +0 -866
- package/dist/index.js +0 -41985
- package/dist/test-server.js +0 -4363
- package/scripts/postinstall.js +0 -56
- package/scripts/registration-smoke.mts +0 -45
package/dist/hooks/stop.js
DELETED
|
@@ -1,171 +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
|
-
|
|
136
|
-
// src/hooks/stop.ts
|
|
137
|
-
async function runStopHook() {
|
|
138
|
-
if (process.env.CONTEXTSTREAM_STOP_ENABLED === "false") {
|
|
139
|
-
writeHookOutput();
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const input = readHookInput();
|
|
143
|
-
const cwd = extractCwd(input);
|
|
144
|
-
const config = loadHookConfig(cwd);
|
|
145
|
-
if (isConfigured(config)) {
|
|
146
|
-
const sessionId = typeof input.session_id === "string" && input.session_id || "unknown";
|
|
147
|
-
const reason = typeof input.reason === "string" && input.reason || typeof input.stop_reason === "string" && input.stop_reason || "response_complete";
|
|
148
|
-
await postMemoryEvent(
|
|
149
|
-
config,
|
|
150
|
-
"Stop checkpoint",
|
|
151
|
-
{
|
|
152
|
-
session_id: sessionId,
|
|
153
|
-
reason,
|
|
154
|
-
hook: "stop",
|
|
155
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
156
|
-
tool_name: typeof input.tool_name === "string" ? input.tool_name : null,
|
|
157
|
-
model: typeof input.model === "string" ? input.model : null
|
|
158
|
-
},
|
|
159
|
-
["hook", "stop", "checkpoint"]
|
|
160
|
-
).catch(() => {
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
writeHookOutput();
|
|
164
|
-
}
|
|
165
|
-
var isDirectRun = process.argv[1]?.includes("stop") || process.argv[2] === "stop";
|
|
166
|
-
if (isDirectRun) {
|
|
167
|
-
runStopHook().catch(() => process.exit(0));
|
|
168
|
-
}
|
|
169
|
-
export {
|
|
170
|
-
runStopHook
|
|
171
|
-
};
|
|
@@ -1,195 +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 fetchFastContext(config, body) {
|
|
118
|
-
if (!isConfigured(config)) return null;
|
|
119
|
-
try {
|
|
120
|
-
const result = await apiRequest(config, "/context/hook", {
|
|
121
|
-
method: "POST",
|
|
122
|
-
body: {
|
|
123
|
-
workspace_id: config.workspaceId || void 0,
|
|
124
|
-
project_id: config.projectId || void 0,
|
|
125
|
-
...body
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
if (typeof result?.context === "string") return result.context;
|
|
129
|
-
if (typeof result?.data?.context === "string") return result.data.context;
|
|
130
|
-
return null;
|
|
131
|
-
} catch {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// src/hooks/subagent-start.ts
|
|
137
|
-
var SEARCH_PROTOCOL = `[CONTEXTSTREAM SEARCH]
|
|
138
|
-
When searching code, prefer ContextStream search tools first:
|
|
139
|
-
- mcp__contextstream__search(mode="auto", query="...")
|
|
140
|
-
- mcp__contextstream__search(mode="keyword", query="...", include_content=true)
|
|
141
|
-
- mcp__contextstream__graph(...) for dependency analysis
|
|
142
|
-
Fall back to local discovery tools only if ContextStream search returns no results.`;
|
|
143
|
-
var EXPLORE_SEARCH_FIRST = `[CRITICAL: SEARCH-FIRST PROTOCOL]
|
|
144
|
-
You MUST call mcp__contextstream__search(mode="auto", query="...") BEFORE reading source files.
|
|
145
|
-
Read only the small set of files and line ranges identified by search.`;
|
|
146
|
-
var PLAN_SEARCH_FIRST = `[PLAN MODE: SEARCH-FIRST]
|
|
147
|
-
Plan mode does NOT justify file-by-file repository scans.
|
|
148
|
-
Start with ContextStream search, then read only the narrowed files and ranges.`;
|
|
149
|
-
function fallbackContext() {
|
|
150
|
-
return `${SEARCH_PROTOCOL}
|
|
151
|
-
|
|
152
|
-
[CONTEXTSTREAM] Call mcp__contextstream__context(user_message="...") for task-specific context.`;
|
|
153
|
-
}
|
|
154
|
-
async function runSubagentStartHook() {
|
|
155
|
-
if (process.env.CONTEXTSTREAM_SUBAGENT_CONTEXT_ENABLED === "false") {
|
|
156
|
-
writeHookOutput();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const input = readHookInput();
|
|
160
|
-
const cwd = extractCwd(input);
|
|
161
|
-
const config = loadHookConfig(cwd);
|
|
162
|
-
const agentType = typeof input.agent_type === "string" && input.agent_type || typeof input.subagent_type === "string" && input.subagent_type || "unknown";
|
|
163
|
-
if (!isConfigured(config)) {
|
|
164
|
-
writeHookOutput({ additionalContext: fallbackContext(), hookEventName: "SubagentStart" });
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const context = await fetchFastContext(config, {
|
|
168
|
-
session_id: typeof input.session_id === "string" && input.session_id || typeof input.sessionId === "string" && input.sessionId || void 0,
|
|
169
|
-
user_message: `SubagentStart:${agentType}`
|
|
170
|
-
});
|
|
171
|
-
const parts = [];
|
|
172
|
-
if (context) parts.push(context);
|
|
173
|
-
if (agentType.toLowerCase() === "explore") {
|
|
174
|
-
parts.push(EXPLORE_SEARCH_FIRST);
|
|
175
|
-
} else if (agentType.toLowerCase() === "plan") {
|
|
176
|
-
parts.push(PLAN_SEARCH_FIRST);
|
|
177
|
-
parts.push(
|
|
178
|
-
`[CONTEXTSTREAM PLAN]
|
|
179
|
-
Save plans to ContextStream with mcp__contextstream__session(action="capture_plan", ...).
|
|
180
|
-
Create tasks with mcp__contextstream__memory(action="create_task", ...).`
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
parts.push(SEARCH_PROTOCOL);
|
|
184
|
-
writeHookOutput({
|
|
185
|
-
additionalContext: parts.filter(Boolean).join("\n\n") || fallbackContext(),
|
|
186
|
-
hookEventName: "SubagentStart"
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
var isDirectRun = process.argv[1]?.includes("subagent-start") || process.argv[2] === "subagent-start";
|
|
190
|
-
if (isDirectRun) {
|
|
191
|
-
runSubagentStartHook().catch(() => process.exit(0));
|
|
192
|
-
}
|
|
193
|
-
export {
|
|
194
|
-
runSubagentStartHook
|
|
195
|
-
};
|
|
@@ -1,301 +0,0 @@
|
|
|
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
|
-
var HOOK_SPECIFIC_OUTPUT_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit", "PostToolUse"]);
|
|
12
|
-
function readHookInput() {
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
15
|
-
} catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function writeHookOutput(output) {
|
|
20
|
-
const eventName = output?.hookEventName || (typeof process.env.HOOK_EVENT_NAME === "string" ? process.env.HOOK_EVENT_NAME.trim() : "");
|
|
21
|
-
const canUseHookSpecificOutput = HOOK_SPECIFIC_OUTPUT_EVENTS.has(eventName);
|
|
22
|
-
const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
|
|
23
|
-
hookSpecificOutput: output.additionalContext && canUseHookSpecificOutput ? {
|
|
24
|
-
hookEventName: eventName,
|
|
25
|
-
additionalContext: output.additionalContext
|
|
26
|
-
} : void 0,
|
|
27
|
-
additionalContext: output.additionalContext,
|
|
28
|
-
blocked: output.blocked,
|
|
29
|
-
reason: output.reason
|
|
30
|
-
} : {};
|
|
31
|
-
console.log(JSON.stringify(payload));
|
|
32
|
-
}
|
|
33
|
-
function extractCwd(input) {
|
|
34
|
-
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
|
|
35
|
-
return cwd;
|
|
36
|
-
}
|
|
37
|
-
function loadHookConfig(cwd) {
|
|
38
|
-
let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
|
|
39
|
-
let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
40
|
-
let jwt = process.env.CONTEXTSTREAM_JWT || "";
|
|
41
|
-
let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
|
|
42
|
-
let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
|
|
43
|
-
let searchDir = path.resolve(cwd);
|
|
44
|
-
for (let i = 0; i < 6; i++) {
|
|
45
|
-
if (!apiKey && !jwt) {
|
|
46
|
-
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
47
|
-
if (fs.existsSync(mcpPath)) {
|
|
48
|
-
try {
|
|
49
|
-
const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
50
|
-
const env = config.mcpServers?.contextstream?.env;
|
|
51
|
-
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
52
|
-
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
53
|
-
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
54
|
-
if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
|
|
55
|
-
if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
|
|
56
|
-
} catch {
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (!workspaceId || !projectId) {
|
|
61
|
-
const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
62
|
-
if (fs.existsSync(localConfigPath)) {
|
|
63
|
-
try {
|
|
64
|
-
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
|
|
65
|
-
if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
|
|
66
|
-
if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
|
|
67
|
-
} catch {
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
const parentDir = path.dirname(searchDir);
|
|
72
|
-
if (parentDir === searchDir) break;
|
|
73
|
-
searchDir = parentDir;
|
|
74
|
-
}
|
|
75
|
-
if (!apiKey && !jwt) {
|
|
76
|
-
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
77
|
-
if (fs.existsSync(homeMcpPath)) {
|
|
78
|
-
try {
|
|
79
|
-
const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
|
|
80
|
-
const env = config.mcpServers?.contextstream?.env;
|
|
81
|
-
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
82
|
-
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
83
|
-
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
84
|
-
} catch {
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return { apiUrl, apiKey, jwt, workspaceId, projectId };
|
|
89
|
-
}
|
|
90
|
-
function isConfigured(config) {
|
|
91
|
-
return Boolean(config.apiKey || config.jwt);
|
|
92
|
-
}
|
|
93
|
-
function authHeaders(config) {
|
|
94
|
-
if (config.apiKey) {
|
|
95
|
-
return { "X-API-Key": config.apiKey };
|
|
96
|
-
}
|
|
97
|
-
if (config.jwt) {
|
|
98
|
-
return { Authorization: `Bearer ${config.jwt}` };
|
|
99
|
-
}
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
102
|
-
async function apiRequest(config, apiPath, init = {}) {
|
|
103
|
-
const response = await fetch(`${config.apiUrl}${apiPath}`, {
|
|
104
|
-
method: init.method || "GET",
|
|
105
|
-
headers: {
|
|
106
|
-
"Content-Type": "application/json",
|
|
107
|
-
...authHeaders(config)
|
|
108
|
-
},
|
|
109
|
-
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
110
|
-
});
|
|
111
|
-
if (!response.ok) {
|
|
112
|
-
throw new Error(`${response.status} ${response.statusText}`);
|
|
113
|
-
}
|
|
114
|
-
const json = await response.json();
|
|
115
|
-
if (json && typeof json === "object" && "data" in json) {
|
|
116
|
-
return json.data;
|
|
117
|
-
}
|
|
118
|
-
return json;
|
|
119
|
-
}
|
|
120
|
-
async function postMemoryEvent(config, title, content, tags, eventType = "operation") {
|
|
121
|
-
if (!isConfigured(config) || !config.workspaceId) return;
|
|
122
|
-
await apiRequest(config, "/memory/events", {
|
|
123
|
-
method: "POST",
|
|
124
|
-
body: {
|
|
125
|
-
workspace_id: config.workspaceId,
|
|
126
|
-
project_id: config.projectId || void 0,
|
|
127
|
-
event_type: eventType,
|
|
128
|
-
title,
|
|
129
|
-
content: typeof content === "string" ? content : JSON.stringify(content),
|
|
130
|
-
metadata: {
|
|
131
|
-
tags,
|
|
132
|
-
source: "mcp_hook",
|
|
133
|
-
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
async function createPlan(config, title, description) {
|
|
139
|
-
if (!isConfigured(config) || !config.workspaceId) return null;
|
|
140
|
-
try {
|
|
141
|
-
const result = await apiRequest(config, "/plans", {
|
|
142
|
-
method: "POST",
|
|
143
|
-
body: {
|
|
144
|
-
workspace_id: config.workspaceId,
|
|
145
|
-
project_id: config.projectId || void 0,
|
|
146
|
-
title,
|
|
147
|
-
description
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
return typeof result?.id === "string" ? result.id : null;
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
async function createTask(config, params) {
|
|
156
|
-
if (!isConfigured(config) || !config.workspaceId) return null;
|
|
157
|
-
try {
|
|
158
|
-
const result = await apiRequest(config, "/tasks", {
|
|
159
|
-
method: "POST",
|
|
160
|
-
body: {
|
|
161
|
-
workspace_id: config.workspaceId,
|
|
162
|
-
project_id: config.projectId || void 0,
|
|
163
|
-
title: params.title,
|
|
164
|
-
description: params.description,
|
|
165
|
-
plan_id: params.planId || void 0,
|
|
166
|
-
status: params.status
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
return typeof result?.id === "string" ? result.id : null;
|
|
170
|
-
} catch {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// src/hooks/subagent-stop.ts
|
|
176
|
-
function parseTranscript(transcriptPath) {
|
|
177
|
-
try {
|
|
178
|
-
const content = fs2.readFileSync(transcriptPath, "utf8");
|
|
179
|
-
const assistantMessages = [];
|
|
180
|
-
let toolCallCount = 0;
|
|
181
|
-
for (const rawLine of content.split("\n")) {
|
|
182
|
-
const line = rawLine.trim();
|
|
183
|
-
if (!line) continue;
|
|
184
|
-
let entry;
|
|
185
|
-
try {
|
|
186
|
-
entry = JSON.parse(line);
|
|
187
|
-
} catch {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (entry.type === "tool_use") {
|
|
191
|
-
toolCallCount += 1;
|
|
192
|
-
}
|
|
193
|
-
const isAssistant = entry.type === "assistant" || entry.role === "assistant" || Array.isArray(entry.content) && entry.role === "assistant";
|
|
194
|
-
if (!isAssistant) continue;
|
|
195
|
-
const contentValue = entry.content;
|
|
196
|
-
if (typeof contentValue === "string" && contentValue.trim()) {
|
|
197
|
-
assistantMessages.push(contentValue.trim());
|
|
198
|
-
} else if (Array.isArray(contentValue)) {
|
|
199
|
-
const text = contentValue.map((item) => typeof item?.text === "string" ? item.text : typeof item === "string" ? item : "").filter(Boolean).join("\n").trim();
|
|
200
|
-
if (text) assistantMessages.push(text);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return { assistantMessages, toolCallCount };
|
|
204
|
-
} catch {
|
|
205
|
-
return { assistantMessages: [], toolCallCount: 0 };
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
function derivePlanTitle(summary, agentId) {
|
|
209
|
-
for (const line of summary.split("\n")) {
|
|
210
|
-
const trimmed = line.trim();
|
|
211
|
-
if (trimmed.startsWith("#")) {
|
|
212
|
-
return trimmed.replace(/^#+\s*/, "").slice(0, 120);
|
|
213
|
-
}
|
|
214
|
-
if (trimmed) return trimmed.slice(0, 120);
|
|
215
|
-
}
|
|
216
|
-
return `Plan generated by ${agentId}`;
|
|
217
|
-
}
|
|
218
|
-
function extractPlanTasks(summary) {
|
|
219
|
-
const tasks = [];
|
|
220
|
-
for (const line of summary.split("\n")) {
|
|
221
|
-
const trimmed = line.trim();
|
|
222
|
-
if (/^[-*]\s+/.test(trimmed) || /^\d+\.\s+/.test(trimmed)) {
|
|
223
|
-
const task = trimmed.replace(/^([-*]|\d+\.)\s+/, "").trim();
|
|
224
|
-
if (task) tasks.push(task);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return Array.from(new Set(tasks)).slice(0, 20);
|
|
228
|
-
}
|
|
229
|
-
async function runSubagentStopHook() {
|
|
230
|
-
const input = readHookInput();
|
|
231
|
-
const cwd = extractCwd(input);
|
|
232
|
-
const config = loadHookConfig(cwd);
|
|
233
|
-
if (!isConfigured(config)) {
|
|
234
|
-
writeHookOutput();
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const agentType = typeof input.agent_type === "string" && input.agent_type || typeof input.subagent_type === "string" && input.subagent_type || "unknown";
|
|
238
|
-
const agentId = typeof input.agent_id === "string" && input.agent_id || "unknown";
|
|
239
|
-
const transcriptPath = typeof input.agent_transcript_path === "string" && input.agent_transcript_path || typeof input.transcript_path === "string" && input.transcript_path || "";
|
|
240
|
-
const transcript = transcriptPath && fs2.existsSync(transcriptPath) ? parseTranscript(transcriptPath) : { assistantMessages: [], toolCallCount: 0 };
|
|
241
|
-
const summaryText = transcript.assistantMessages.join("\n\n") || typeof input.summary === "string" && input.summary || "(No assistant output found in subagent transcript.)";
|
|
242
|
-
if (agentType.toLowerCase() === "plan") {
|
|
243
|
-
const suppliedPlanId = typeof input.plan_id === "string" && input.plan_id || typeof input.planId === "string" && input.planId || null;
|
|
244
|
-
const planTitle = derivePlanTitle(summaryText, agentId);
|
|
245
|
-
const planId = suppliedPlanId || await createPlan(config, planTitle, summaryText.slice(0, 12e3));
|
|
246
|
-
const tasks = extractPlanTasks(summaryText);
|
|
247
|
-
let createdTaskCount = 0;
|
|
248
|
-
if (planId) {
|
|
249
|
-
for (const task of tasks) {
|
|
250
|
-
const created = await createTask(config, {
|
|
251
|
-
title: task,
|
|
252
|
-
planId,
|
|
253
|
-
status: "pending"
|
|
254
|
-
});
|
|
255
|
-
if (created) createdTaskCount += 1;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
await postMemoryEvent(
|
|
259
|
-
config,
|
|
260
|
-
"Plan subagent captured",
|
|
261
|
-
{
|
|
262
|
-
agent_type: "Plan",
|
|
263
|
-
agent_id: agentId,
|
|
264
|
-
plan_title: planTitle,
|
|
265
|
-
plan_id: planId,
|
|
266
|
-
supplied_plan_id: suppliedPlanId,
|
|
267
|
-
extracted_task_count: tasks.length,
|
|
268
|
-
created_task_count: createdTaskCount,
|
|
269
|
-
assistant_message_count: transcript.assistantMessages.length,
|
|
270
|
-
tool_call_count: transcript.toolCallCount,
|
|
271
|
-
summary_preview: summaryText.slice(0, 2e3),
|
|
272
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
273
|
-
},
|
|
274
|
-
["hook", "subagent_stop", "plan"]
|
|
275
|
-
).catch(() => {
|
|
276
|
-
});
|
|
277
|
-
} else {
|
|
278
|
-
await postMemoryEvent(
|
|
279
|
-
config,
|
|
280
|
-
`Subagent finished: ${agentType}`,
|
|
281
|
-
{
|
|
282
|
-
agent_type: agentType,
|
|
283
|
-
agent_id: agentId,
|
|
284
|
-
summary: summaryText.slice(0, 4e3),
|
|
285
|
-
assistant_message_count: transcript.assistantMessages.length,
|
|
286
|
-
tool_call_count: transcript.toolCallCount,
|
|
287
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
288
|
-
},
|
|
289
|
-
["hook", "subagent_stop", agentType]
|
|
290
|
-
).catch(() => {
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
writeHookOutput();
|
|
294
|
-
}
|
|
295
|
-
var isDirectRun = process.argv[1]?.includes("subagent-stop") || process.argv[2] === "subagent-stop";
|
|
296
|
-
if (isDirectRun) {
|
|
297
|
-
runSubagentStopHook().catch(() => process.exit(0));
|
|
298
|
-
}
|
|
299
|
-
export {
|
|
300
|
-
runSubagentStopHook
|
|
301
|
-
};
|