@contextstream/mcp-server 0.4.50 → 0.4.53
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 +140 -25
- package/dist/hooks/on-bash.js +190 -0
- package/dist/hooks/on-read.js +163 -0
- package/dist/hooks/on-save-intent.js +132 -0
- package/dist/hooks/on-task.js +139 -0
- package/dist/hooks/on-web.js +155 -0
- package/dist/hooks/post-compact.js +172 -0
- package/dist/hooks/pre-compact.js +3 -1
- package/dist/hooks/pre-tool-use.js +29 -4
- package/dist/hooks/runner.js +3161 -0
- package/dist/hooks/session-end.js +191 -0
- package/dist/hooks/session-init.js +174 -0
- package/dist/hooks/user-prompt-submit.js +250 -9
- package/dist/index.js +2458 -198
- package/dist/test-server.js +3 -0
- package/package.json +7 -4
- package/scripts/postinstall.js +56 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/on-save-intent.ts
|
|
4
|
+
var ENABLED = process.env.CONTEXTSTREAM_SAVE_INTENT_ENABLED !== "false";
|
|
5
|
+
var SAVE_PATTERNS = [
|
|
6
|
+
// Direct save requests
|
|
7
|
+
/\b(save|store|record|capture|log|document|write down|note down|keep track)\b.*\b(this|that|it|the)\b/i,
|
|
8
|
+
/\b(save|store|record|capture|log)\b.*\b(to|in|for)\b.*\b(contextstream|memory|later|reference|future)\b/i,
|
|
9
|
+
// Document creation
|
|
10
|
+
/\b(create|make|write|draft)\b.*\b(a|the)\b.*\b(document|doc|note|summary|report|spec|design)\b/i,
|
|
11
|
+
/\b(document|summarize|write up)\b.*\b(this|that|the|our)\b.*\b(decision|discussion|conversation|meeting|finding)\b/i,
|
|
12
|
+
// Memory/reference requests
|
|
13
|
+
/\b(remember|don't forget|keep in mind|note that|important to remember)\b/i,
|
|
14
|
+
/\bfor\s+(future|later)\s+reference\b/i,
|
|
15
|
+
/\b(add|put)\s+(this|it|that)\s+(to|in)\s+(memory|notes|docs)\b/i,
|
|
16
|
+
// Decision tracking
|
|
17
|
+
/\b(we\s+)?(decided|agreed|concluded|determined)\b.*\b(to|that)\b/i,
|
|
18
|
+
/\blet('s|s)\s+document\b/i,
|
|
19
|
+
/\bsave\s+(this|the)\s+(decision|choice|approach)\b/i,
|
|
20
|
+
// Implementation/design docs
|
|
21
|
+
/\b(implementation|design|architecture|spec)\s+(doc|document|plan)\b/i,
|
|
22
|
+
/\bwrite\s+(the|a|an)\s+.*(md|markdown|readme)\b/i
|
|
23
|
+
];
|
|
24
|
+
var LOCAL_FILE_PATTERNS = [
|
|
25
|
+
/\b(save|write|create)\s+(it|this|the\s+\w+)\s+(to|in|as)\s+[./~]/i,
|
|
26
|
+
/\b(save|write)\s+to\s+.*(\.md|\.txt|\.json|docs\/|notes\/)/i,
|
|
27
|
+
/\bcreate\s+(a|the)\s+file\b/i
|
|
28
|
+
];
|
|
29
|
+
function detectsSaveIntent(text) {
|
|
30
|
+
const hasSaveIntent = SAVE_PATTERNS.some((p) => p.test(text));
|
|
31
|
+
const isLocalFile = LOCAL_FILE_PATTERNS.some((p) => p.test(text));
|
|
32
|
+
return { hasSaveIntent, isLocalFile };
|
|
33
|
+
}
|
|
34
|
+
var SAVE_GUIDANCE = `[CONTEXTSTREAM DOCUMENT STORAGE]
|
|
35
|
+
The user wants to save/store content. Use ContextStream instead of local files:
|
|
36
|
+
|
|
37
|
+
**For decisions/notes:**
|
|
38
|
+
\`\`\`
|
|
39
|
+
mcp__contextstream__session(
|
|
40
|
+
action="capture",
|
|
41
|
+
event_type="decision|note|insight",
|
|
42
|
+
title="...",
|
|
43
|
+
content="...",
|
|
44
|
+
importance="high|medium|low"
|
|
45
|
+
)
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
**For documents/specs:**
|
|
49
|
+
\`\`\`
|
|
50
|
+
mcp__contextstream__docs(
|
|
51
|
+
action="create",
|
|
52
|
+
title="...",
|
|
53
|
+
content="...",
|
|
54
|
+
doc_type="implementation|design|spec|guide"
|
|
55
|
+
)
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
**For plans:**
|
|
59
|
+
\`\`\`
|
|
60
|
+
mcp__contextstream__session(
|
|
61
|
+
action="capture_plan",
|
|
62
|
+
title="...",
|
|
63
|
+
steps=[...]
|
|
64
|
+
)
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
**Why ContextStream?**
|
|
68
|
+
- Persists across sessions (local files don't)
|
|
69
|
+
- Searchable and retrievable
|
|
70
|
+
- Shows up in context automatically
|
|
71
|
+
- Can be shared with team
|
|
72
|
+
|
|
73
|
+
Only save to local files if user explicitly requests a specific file path.
|
|
74
|
+
[END GUIDANCE]`;
|
|
75
|
+
async function runOnSaveIntentHook() {
|
|
76
|
+
if (!ENABLED) {
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
let inputData = "";
|
|
80
|
+
for await (const chunk of process.stdin) {
|
|
81
|
+
inputData += chunk;
|
|
82
|
+
}
|
|
83
|
+
if (!inputData.trim()) {
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
let input;
|
|
87
|
+
try {
|
|
88
|
+
input = JSON.parse(inputData);
|
|
89
|
+
} catch {
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
let prompt = input.prompt || "";
|
|
93
|
+
if (!prompt && input.session?.messages) {
|
|
94
|
+
for (const msg of [...input.session.messages].reverse()) {
|
|
95
|
+
if (msg.role === "user") {
|
|
96
|
+
if (typeof msg.content === "string") {
|
|
97
|
+
prompt = msg.content;
|
|
98
|
+
} else if (Array.isArray(msg.content)) {
|
|
99
|
+
for (const block of msg.content) {
|
|
100
|
+
if (block.type === "text" && block.text) {
|
|
101
|
+
prompt = block.text;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!prompt) {
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
const { hasSaveIntent, isLocalFile } = detectsSaveIntent(prompt);
|
|
114
|
+
if (hasSaveIntent || isLocalFile) {
|
|
115
|
+
console.log(
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
hookSpecificOutput: {
|
|
118
|
+
hookEventName: "UserPromptSubmit",
|
|
119
|
+
additionalContext: SAVE_GUIDANCE
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
var isDirectRun = process.argv[1]?.includes("on-save-intent") || process.argv[2] === "on-save-intent";
|
|
127
|
+
if (isDirectRun) {
|
|
128
|
+
runOnSaveIntentHook().catch(() => process.exit(0));
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
runOnSaveIntentHook
|
|
132
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/on-task.ts
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
var ENABLED = process.env.CONTEXTSTREAM_TASK_HOOK_ENABLED !== "false";
|
|
8
|
+
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
9
|
+
var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
10
|
+
var WORKSPACE_ID = null;
|
|
11
|
+
function loadConfigFromMcpJson(cwd) {
|
|
12
|
+
let searchDir = path.resolve(cwd);
|
|
13
|
+
for (let i = 0; i < 5; i++) {
|
|
14
|
+
if (!API_KEY) {
|
|
15
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
16
|
+
if (fs.existsSync(mcpPath)) {
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
19
|
+
const config = JSON.parse(content);
|
|
20
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
21
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
22
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
23
|
+
}
|
|
24
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
25
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!WORKSPACE_ID) {
|
|
32
|
+
const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
33
|
+
if (fs.existsSync(csConfigPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(csConfigPath, "utf-8");
|
|
36
|
+
const csConfig = JSON.parse(content);
|
|
37
|
+
if (csConfig.workspace_id) {
|
|
38
|
+
WORKSPACE_ID = csConfig.workspace_id;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const parentDir = path.dirname(searchDir);
|
|
45
|
+
if (parentDir === searchDir) break;
|
|
46
|
+
searchDir = parentDir;
|
|
47
|
+
}
|
|
48
|
+
if (!API_KEY) {
|
|
49
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
50
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
53
|
+
const config = JSON.parse(content);
|
|
54
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
55
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
56
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
57
|
+
}
|
|
58
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
59
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function captureTaskInvocation(description, prompt, agentType, result, sessionId) {
|
|
67
|
+
if (!API_KEY) return;
|
|
68
|
+
const payload = {
|
|
69
|
+
event_type: "task_agent",
|
|
70
|
+
title: `Agent: ${agentType} - ${description}`,
|
|
71
|
+
content: JSON.stringify({
|
|
72
|
+
description,
|
|
73
|
+
prompt: prompt.slice(0, 1e3),
|
|
74
|
+
agent_type: agentType,
|
|
75
|
+
result: result.slice(0, 2e3),
|
|
76
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
77
|
+
}),
|
|
78
|
+
importance: "medium",
|
|
79
|
+
tags: ["task", "agent", agentType.toLowerCase()],
|
|
80
|
+
source_type: "hook",
|
|
81
|
+
session_id: sessionId
|
|
82
|
+
};
|
|
83
|
+
if (WORKSPACE_ID) {
|
|
84
|
+
payload.workspace_id = WORKSPACE_ID;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
89
|
+
await fetch(`${API_URL}/api/v1/memory/events`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
"Content-Type": "application/json",
|
|
93
|
+
"X-API-Key": API_KEY
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify(payload),
|
|
96
|
+
signal: controller.signal
|
|
97
|
+
});
|
|
98
|
+
clearTimeout(timeoutId);
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function runOnTaskHook() {
|
|
103
|
+
if (!ENABLED) {
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
let inputData = "";
|
|
107
|
+
for await (const chunk of process.stdin) {
|
|
108
|
+
inputData += chunk;
|
|
109
|
+
}
|
|
110
|
+
if (!inputData.trim()) {
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
let input;
|
|
114
|
+
try {
|
|
115
|
+
input = JSON.parse(inputData);
|
|
116
|
+
} catch {
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
if (input.tool_name !== "Task") {
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
const cwd = input.cwd || process.cwd();
|
|
123
|
+
loadConfigFromMcpJson(cwd);
|
|
124
|
+
const description = input.tool_input?.description || "Unknown task";
|
|
125
|
+
const prompt = input.tool_input?.prompt || "";
|
|
126
|
+
const agentType = input.tool_input?.subagent_type || "general-purpose";
|
|
127
|
+
const result = input.tool_result?.output || "";
|
|
128
|
+
const sessionId = input.session_id || "unknown";
|
|
129
|
+
captureTaskInvocation(description, prompt, agentType, result, sessionId).catch(() => {
|
|
130
|
+
});
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
var isDirectRun = process.argv[1]?.includes("on-task") || process.argv[2] === "on-task";
|
|
134
|
+
if (isDirectRun) {
|
|
135
|
+
runOnTaskHook().catch(() => process.exit(0));
|
|
136
|
+
}
|
|
137
|
+
export {
|
|
138
|
+
runOnTaskHook
|
|
139
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/on-web.ts
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
var ENABLED = process.env.CONTEXTSTREAM_WEB_HOOK_ENABLED !== "false";
|
|
8
|
+
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
9
|
+
var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
10
|
+
var WORKSPACE_ID = null;
|
|
11
|
+
function loadConfigFromMcpJson(cwd) {
|
|
12
|
+
let searchDir = path.resolve(cwd);
|
|
13
|
+
for (let i = 0; i < 5; i++) {
|
|
14
|
+
if (!API_KEY) {
|
|
15
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
16
|
+
if (fs.existsSync(mcpPath)) {
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
19
|
+
const config = JSON.parse(content);
|
|
20
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
21
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
22
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
23
|
+
}
|
|
24
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
25
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!WORKSPACE_ID) {
|
|
32
|
+
const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
33
|
+
if (fs.existsSync(csConfigPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(csConfigPath, "utf-8");
|
|
36
|
+
const csConfig = JSON.parse(content);
|
|
37
|
+
if (csConfig.workspace_id) {
|
|
38
|
+
WORKSPACE_ID = csConfig.workspace_id;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const parentDir = path.dirname(searchDir);
|
|
45
|
+
if (parentDir === searchDir) break;
|
|
46
|
+
searchDir = parentDir;
|
|
47
|
+
}
|
|
48
|
+
if (!API_KEY) {
|
|
49
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
50
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
53
|
+
const config = JSON.parse(content);
|
|
54
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
55
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
56
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
57
|
+
}
|
|
58
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
59
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function captureWebResearch(toolName, target, summary, sessionId) {
|
|
67
|
+
if (!API_KEY) return;
|
|
68
|
+
const payload = {
|
|
69
|
+
event_type: "web_research",
|
|
70
|
+
title: `${toolName}: ${target.slice(0, 60)}`,
|
|
71
|
+
content: JSON.stringify({
|
|
72
|
+
tool: toolName,
|
|
73
|
+
target,
|
|
74
|
+
summary: summary.slice(0, 1e3),
|
|
75
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
76
|
+
}),
|
|
77
|
+
importance: "medium",
|
|
78
|
+
tags: ["research", "web", toolName.toLowerCase()],
|
|
79
|
+
source_type: "hook",
|
|
80
|
+
session_id: sessionId
|
|
81
|
+
};
|
|
82
|
+
if (WORKSPACE_ID) {
|
|
83
|
+
payload.workspace_id = WORKSPACE_ID;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
88
|
+
await fetch(`${API_URL}/api/v1/memory/events`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"X-API-Key": API_KEY
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify(payload),
|
|
95
|
+
signal: controller.signal
|
|
96
|
+
});
|
|
97
|
+
clearTimeout(timeoutId);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function runOnWebHook() {
|
|
102
|
+
if (!ENABLED) {
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
let inputData = "";
|
|
106
|
+
for await (const chunk of process.stdin) {
|
|
107
|
+
inputData += chunk;
|
|
108
|
+
}
|
|
109
|
+
if (!inputData.trim()) {
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
let input;
|
|
113
|
+
try {
|
|
114
|
+
input = JSON.parse(inputData);
|
|
115
|
+
} catch {
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
const toolName = input.tool_name || "";
|
|
119
|
+
if (!["WebFetch", "WebSearch"].includes(toolName)) {
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
const cwd = input.cwd || process.cwd();
|
|
123
|
+
loadConfigFromMcpJson(cwd);
|
|
124
|
+
const sessionId = input.session_id || "unknown";
|
|
125
|
+
let target = "";
|
|
126
|
+
let summary = "";
|
|
127
|
+
switch (toolName) {
|
|
128
|
+
case "WebFetch":
|
|
129
|
+
target = input.tool_input?.url || "";
|
|
130
|
+
const prompt = input.tool_input?.prompt || "fetched content";
|
|
131
|
+
const content = input.tool_result?.output || input.tool_result?.content || "";
|
|
132
|
+
summary = `Fetched ${target} (${prompt}): ${content.slice(0, 300)}`;
|
|
133
|
+
break;
|
|
134
|
+
case "WebSearch":
|
|
135
|
+
target = input.tool_input?.query || "";
|
|
136
|
+
const results = input.tool_result?.results || [];
|
|
137
|
+
const topResults = results.slice(0, 3).map((r) => `- ${r.title}: ${r.url}`).join("\n");
|
|
138
|
+
summary = `Search: "${target}"
|
|
139
|
+
Top results:
|
|
140
|
+
${topResults}`;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
if (target) {
|
|
144
|
+
captureWebResearch(toolName, target, summary, sessionId).catch(() => {
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
var isDirectRun = process.argv[1]?.includes("on-web") || process.argv[2] === "on-web";
|
|
150
|
+
if (isDirectRun) {
|
|
151
|
+
runOnWebHook().catch(() => process.exit(0));
|
|
152
|
+
}
|
|
153
|
+
export {
|
|
154
|
+
runOnWebHook
|
|
155
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/post-compact.ts
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
var ENABLED = process.env.CONTEXTSTREAM_POSTCOMPACT_ENABLED !== "false";
|
|
8
|
+
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
9
|
+
var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
10
|
+
var WORKSPACE_ID = null;
|
|
11
|
+
function loadConfigFromMcpJson(cwd) {
|
|
12
|
+
let searchDir = path.resolve(cwd);
|
|
13
|
+
for (let i = 0; i < 5; i++) {
|
|
14
|
+
if (!API_KEY) {
|
|
15
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
16
|
+
if (fs.existsSync(mcpPath)) {
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
19
|
+
const config = JSON.parse(content);
|
|
20
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
21
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
22
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
23
|
+
}
|
|
24
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
25
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!WORKSPACE_ID) {
|
|
32
|
+
const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
33
|
+
if (fs.existsSync(csConfigPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(csConfigPath, "utf-8");
|
|
36
|
+
const csConfig = JSON.parse(content);
|
|
37
|
+
if (csConfig.workspace_id) {
|
|
38
|
+
WORKSPACE_ID = csConfig.workspace_id;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const parentDir = path.dirname(searchDir);
|
|
45
|
+
if (parentDir === searchDir) break;
|
|
46
|
+
searchDir = parentDir;
|
|
47
|
+
}
|
|
48
|
+
if (!API_KEY) {
|
|
49
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
50
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
53
|
+
const config = JSON.parse(content);
|
|
54
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
55
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
56
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
57
|
+
}
|
|
58
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
59
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function fetchLastTranscript(sessionId) {
|
|
67
|
+
if (!API_KEY) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
73
|
+
const url = new URL(`${API_URL}/api/v1/transcripts`);
|
|
74
|
+
url.searchParams.set("session_id", sessionId);
|
|
75
|
+
url.searchParams.set("limit", "1");
|
|
76
|
+
url.searchParams.set("sort", "created_at:desc");
|
|
77
|
+
if (WORKSPACE_ID) {
|
|
78
|
+
url.searchParams.set("workspace_id", WORKSPACE_ID);
|
|
79
|
+
}
|
|
80
|
+
const response = await fetch(url.toString(), {
|
|
81
|
+
method: "GET",
|
|
82
|
+
headers: {
|
|
83
|
+
"X-API-Key": API_KEY
|
|
84
|
+
},
|
|
85
|
+
signal: controller.signal
|
|
86
|
+
});
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
if (response.ok) {
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
if (data.transcripts && data.transcripts.length > 0) {
|
|
91
|
+
return data.transcripts[0];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function formatTranscriptSummary(transcript) {
|
|
100
|
+
const messages = transcript.messages || [];
|
|
101
|
+
const activeFiles = transcript.metadata?.active_files || [];
|
|
102
|
+
const toolCallCount = transcript.metadata?.tool_call_count || 0;
|
|
103
|
+
const userMessages = messages.filter((m) => m.role === "user").slice(-3).map((m) => `- "${m.content.slice(0, 100)}${m.content.length > 100 ? "..." : ""}"`).join("\n");
|
|
104
|
+
const lastAssistant = messages.filter((m) => m.role === "assistant" && !m.content.startsWith("[Tool:")).slice(-1)[0];
|
|
105
|
+
const lastWork = lastAssistant ? lastAssistant.content.slice(0, 300) + (lastAssistant.content.length > 300 ? "..." : "") : "None recorded";
|
|
106
|
+
return `## Pre-Compaction State Restored
|
|
107
|
+
|
|
108
|
+
### Active Files (${activeFiles.length})
|
|
109
|
+
${activeFiles.slice(0, 10).map((f) => `- ${f}`).join("\n") || "None tracked"}
|
|
110
|
+
|
|
111
|
+
### Recent User Requests
|
|
112
|
+
${userMessages || "None recorded"}
|
|
113
|
+
|
|
114
|
+
### Last Work in Progress
|
|
115
|
+
${lastWork}
|
|
116
|
+
|
|
117
|
+
### Session Stats
|
|
118
|
+
- Tool calls: ${toolCallCount}
|
|
119
|
+
- Messages: ${messages.length}
|
|
120
|
+
- Saved at: ${transcript.created_at}`;
|
|
121
|
+
}
|
|
122
|
+
async function runPostCompactHook() {
|
|
123
|
+
if (!ENABLED) {
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
let inputData = "";
|
|
127
|
+
for await (const chunk of process.stdin) {
|
|
128
|
+
inputData += chunk;
|
|
129
|
+
}
|
|
130
|
+
if (!inputData.trim()) {
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
let input;
|
|
134
|
+
try {
|
|
135
|
+
input = JSON.parse(inputData);
|
|
136
|
+
} catch {
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
const cwd = input.cwd || process.cwd();
|
|
140
|
+
loadConfigFromMcpJson(cwd);
|
|
141
|
+
const sessionId = input.session_id || "";
|
|
142
|
+
let restoredContext = "";
|
|
143
|
+
if (sessionId && API_KEY) {
|
|
144
|
+
const transcript = await fetchLastTranscript(sessionId);
|
|
145
|
+
if (transcript) {
|
|
146
|
+
restoredContext = formatTranscriptSummary(transcript);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const context = `[POST-COMPACTION - Context Restored]
|
|
150
|
+
|
|
151
|
+
${restoredContext || "No saved state found. Starting fresh."}
|
|
152
|
+
|
|
153
|
+
**IMPORTANT:** Call \`mcp__contextstream__context(user_message="resuming after compaction")\` to get full context and any pending tasks.
|
|
154
|
+
|
|
155
|
+
The conversation was compacted to save memory. The above summary was automatically restored from ContextStream.`;
|
|
156
|
+
console.log(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
hookSpecificOutput: {
|
|
159
|
+
hookEventName: "PostCompact",
|
|
160
|
+
additionalContext: context
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
var isDirectRun = process.argv[1]?.includes("post-compact") || process.argv[2] === "post-compact";
|
|
167
|
+
if (isDirectRun) {
|
|
168
|
+
runPostCompactHook().catch(() => process.exit(0));
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
runPostCompactHook
|
|
172
|
+
};
|
|
@@ -269,7 +269,9 @@ async function runPreCompactHook() {
|
|
|
269
269
|
activeFiles: [],
|
|
270
270
|
toolCallCount: 0,
|
|
271
271
|
messageCount: 0,
|
|
272
|
-
lastTools: []
|
|
272
|
+
lastTools: [],
|
|
273
|
+
messages: [],
|
|
274
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
273
275
|
};
|
|
274
276
|
if (transcriptPath && fs.existsSync(transcriptPath)) {
|
|
275
277
|
transcriptData = parseTranscript(transcriptPath);
|
|
@@ -6,6 +6,7 @@ import * as path from "node:path";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
var ENABLED = process.env.CONTEXTSTREAM_HOOK_ENABLED !== "false";
|
|
8
8
|
var INDEX_STATUS_FILE = path.join(homedir(), ".contextstream", "indexed-projects.json");
|
|
9
|
+
var DEBUG_FILE = "/tmp/pretooluse-hook-debug.log";
|
|
9
10
|
var STALE_THRESHOLD_DAYS = 7;
|
|
10
11
|
var DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"];
|
|
11
12
|
function isDiscoveryGlob(pattern) {
|
|
@@ -82,8 +83,17 @@ function extractToolInput(input) {
|
|
|
82
83
|
return input.tool_input || input.parameters || input.toolParameters || {};
|
|
83
84
|
}
|
|
84
85
|
function blockClaudeCode(message) {
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
const response = {
|
|
87
|
+
hookSpecificOutput: {
|
|
88
|
+
hookEventName: "PreToolUse",
|
|
89
|
+
// Use additionalContext instead of deny - tool runs but Claude sees the message
|
|
90
|
+
additionalContext: `[CONTEXTSTREAM] ${message}`
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
fs.appendFileSync(DEBUG_FILE, `[PreToolUse] REDIRECT (additionalContext): ${JSON.stringify(response)}
|
|
94
|
+
`);
|
|
95
|
+
console.log(JSON.stringify(response));
|
|
96
|
+
process.exit(0);
|
|
87
97
|
}
|
|
88
98
|
function outputClineBlock(errorMessage, contextMod) {
|
|
89
99
|
const result = {
|
|
@@ -112,13 +122,18 @@ function detectEditorFormat(input) {
|
|
|
112
122
|
if (input.hookName !== void 0 || input.toolName !== void 0) {
|
|
113
123
|
return "cline";
|
|
114
124
|
}
|
|
115
|
-
if (input.hook_event_name !== void 0) {
|
|
116
|
-
return "
|
|
125
|
+
if (input.hook_event_name !== void 0 || input.tool_name !== void 0) {
|
|
126
|
+
return "claude";
|
|
117
127
|
}
|
|
118
128
|
return "claude";
|
|
119
129
|
}
|
|
120
130
|
async function runPreToolUseHook() {
|
|
131
|
+
fs.appendFileSync(DEBUG_FILE, `[PreToolUse] Hook invoked at ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
132
|
+
`);
|
|
133
|
+
console.error("[PreToolUse] Hook invoked at", (/* @__PURE__ */ new Date()).toISOString());
|
|
121
134
|
if (!ENABLED) {
|
|
135
|
+
fs.appendFileSync(DEBUG_FILE, "[PreToolUse] Hook disabled, exiting\n");
|
|
136
|
+
console.error("[PreToolUse] Hook disabled, exiting");
|
|
122
137
|
process.exit(0);
|
|
123
138
|
}
|
|
124
139
|
let inputData = "";
|
|
@@ -138,8 +153,14 @@ async function runPreToolUseHook() {
|
|
|
138
153
|
const cwd = extractCwd(input);
|
|
139
154
|
const tool = extractToolName(input);
|
|
140
155
|
const toolInput = extractToolInput(input);
|
|
156
|
+
fs.appendFileSync(DEBUG_FILE, `[PreToolUse] tool=${tool}, cwd=${cwd}, editorFormat=${editorFormat}
|
|
157
|
+
`);
|
|
141
158
|
const { isIndexed } = isProjectIndexed(cwd);
|
|
159
|
+
fs.appendFileSync(DEBUG_FILE, `[PreToolUse] isIndexed=${isIndexed}
|
|
160
|
+
`);
|
|
142
161
|
if (!isIndexed) {
|
|
162
|
+
fs.appendFileSync(DEBUG_FILE, `[PreToolUse] Project not indexed, allowing
|
|
163
|
+
`);
|
|
143
164
|
if (editorFormat === "cline") {
|
|
144
165
|
outputClineAllow();
|
|
145
166
|
} else if (editorFormat === "cursor") {
|
|
@@ -149,8 +170,12 @@ async function runPreToolUseHook() {
|
|
|
149
170
|
}
|
|
150
171
|
if (tool === "Glob") {
|
|
151
172
|
const pattern = toolInput?.pattern || "";
|
|
173
|
+
fs.appendFileSync(DEBUG_FILE, `[PreToolUse] Glob pattern=${pattern}, isDiscovery=${isDiscoveryGlob(pattern)}
|
|
174
|
+
`);
|
|
152
175
|
if (isDiscoveryGlob(pattern)) {
|
|
153
176
|
const msg = `STOP: Use mcp__contextstream__search(mode="hybrid", query="${pattern}") instead of Glob.`;
|
|
177
|
+
fs.appendFileSync(DEBUG_FILE, `[PreToolUse] Intercepting discovery glob: ${msg}
|
|
178
|
+
`);
|
|
154
179
|
if (editorFormat === "cline") {
|
|
155
180
|
outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
|
|
156
181
|
} else if (editorFormat === "cursor") {
|