@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,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/session-end.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_SESSION_END_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
|
+
function parseTranscriptStats(transcriptPath) {
|
|
67
|
+
const stats = {
|
|
68
|
+
messageCount: 0,
|
|
69
|
+
toolCallCount: 0,
|
|
70
|
+
duration: 0,
|
|
71
|
+
filesModified: []
|
|
72
|
+
};
|
|
73
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
74
|
+
return stats;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const content = fs.readFileSync(transcriptPath, "utf-8");
|
|
78
|
+
const lines = content.split("\n");
|
|
79
|
+
let firstTimestamp = null;
|
|
80
|
+
let lastTimestamp = null;
|
|
81
|
+
const modifiedFiles = /* @__PURE__ */ new Set();
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (!line.trim()) continue;
|
|
84
|
+
try {
|
|
85
|
+
const entry = JSON.parse(line);
|
|
86
|
+
if (entry.type === "user" || entry.type === "assistant") {
|
|
87
|
+
stats.messageCount++;
|
|
88
|
+
} else if (entry.type === "tool_use") {
|
|
89
|
+
stats.toolCallCount++;
|
|
90
|
+
if (["Write", "Edit", "NotebookEdit"].includes(entry.name || "")) {
|
|
91
|
+
const filePath = entry.input?.file_path;
|
|
92
|
+
if (filePath) {
|
|
93
|
+
modifiedFiles.add(filePath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (entry.timestamp) {
|
|
98
|
+
const ts = new Date(entry.timestamp);
|
|
99
|
+
if (!firstTimestamp || ts < firstTimestamp) {
|
|
100
|
+
firstTimestamp = ts;
|
|
101
|
+
}
|
|
102
|
+
if (!lastTimestamp || ts > lastTimestamp) {
|
|
103
|
+
lastTimestamp = ts;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (firstTimestamp && lastTimestamp) {
|
|
111
|
+
stats.duration = Math.round((lastTimestamp.getTime() - firstTimestamp.getTime()) / 1e3);
|
|
112
|
+
}
|
|
113
|
+
stats.filesModified = Array.from(modifiedFiles);
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
return stats;
|
|
117
|
+
}
|
|
118
|
+
async function finalizeSession(sessionId, stats, reason) {
|
|
119
|
+
if (!API_KEY) return;
|
|
120
|
+
const payload = {
|
|
121
|
+
event_type: "session_end",
|
|
122
|
+
title: `Session Ended: ${reason}`,
|
|
123
|
+
content: JSON.stringify({
|
|
124
|
+
session_id: sessionId,
|
|
125
|
+
reason,
|
|
126
|
+
stats: {
|
|
127
|
+
messages: stats.messageCount,
|
|
128
|
+
tool_calls: stats.toolCallCount,
|
|
129
|
+
duration_seconds: stats.duration,
|
|
130
|
+
files_modified: stats.filesModified.length
|
|
131
|
+
},
|
|
132
|
+
files_modified: stats.filesModified.slice(0, 20),
|
|
133
|
+
ended_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
134
|
+
}),
|
|
135
|
+
importance: "low",
|
|
136
|
+
tags: ["session", "end", reason],
|
|
137
|
+
source_type: "hook",
|
|
138
|
+
session_id: sessionId
|
|
139
|
+
};
|
|
140
|
+
if (WORKSPACE_ID) {
|
|
141
|
+
payload.workspace_id = WORKSPACE_ID;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
146
|
+
await fetch(`${API_URL}/api/v1/memory/events`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
"X-API-Key": API_KEY
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify(payload),
|
|
153
|
+
signal: controller.signal
|
|
154
|
+
});
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function runSessionEndHook() {
|
|
160
|
+
if (!ENABLED) {
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
let inputData = "";
|
|
164
|
+
for await (const chunk of process.stdin) {
|
|
165
|
+
inputData += chunk;
|
|
166
|
+
}
|
|
167
|
+
if (!inputData.trim()) {
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
let input;
|
|
171
|
+
try {
|
|
172
|
+
input = JSON.parse(inputData);
|
|
173
|
+
} catch {
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
const cwd = input.cwd || process.cwd();
|
|
177
|
+
loadConfigFromMcpJson(cwd);
|
|
178
|
+
const sessionId = input.session_id || "unknown";
|
|
179
|
+
const transcriptPath = input.transcript_path || "";
|
|
180
|
+
const reason = input.reason || "user_exit";
|
|
181
|
+
const stats = parseTranscriptStats(transcriptPath);
|
|
182
|
+
await finalizeSession(sessionId, stats, reason);
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
var isDirectRun = process.argv[1]?.includes("session-end") || process.argv[2] === "session-end";
|
|
186
|
+
if (isDirectRun) {
|
|
187
|
+
runSessionEndHook().catch(() => process.exit(0));
|
|
188
|
+
}
|
|
189
|
+
export {
|
|
190
|
+
runSessionEndHook
|
|
191
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/session-init.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_SESSION_INIT_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
|
+
var PROJECT_ID = null;
|
|
12
|
+
function loadConfigFromMcpJson(cwd) {
|
|
13
|
+
let searchDir = path.resolve(cwd);
|
|
14
|
+
for (let i = 0; i < 5; i++) {
|
|
15
|
+
if (!API_KEY) {
|
|
16
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
17
|
+
if (fs.existsSync(mcpPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
20
|
+
const config = JSON.parse(content);
|
|
21
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
22
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
23
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
24
|
+
}
|
|
25
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
26
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
27
|
+
}
|
|
28
|
+
if (csEnv?.CONTEXTSTREAM_WORKSPACE_ID) {
|
|
29
|
+
WORKSPACE_ID = csEnv.CONTEXTSTREAM_WORKSPACE_ID;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!WORKSPACE_ID || !PROJECT_ID) {
|
|
36
|
+
const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
37
|
+
if (fs.existsSync(csConfigPath)) {
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(csConfigPath, "utf-8");
|
|
40
|
+
const csConfig = JSON.parse(content);
|
|
41
|
+
if (csConfig.workspace_id && !WORKSPACE_ID) {
|
|
42
|
+
WORKSPACE_ID = csConfig.workspace_id;
|
|
43
|
+
}
|
|
44
|
+
if (csConfig.project_id && !PROJECT_ID) {
|
|
45
|
+
PROJECT_ID = csConfig.project_id;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const parentDir = path.dirname(searchDir);
|
|
52
|
+
if (parentDir === searchDir) break;
|
|
53
|
+
searchDir = parentDir;
|
|
54
|
+
}
|
|
55
|
+
if (!API_KEY) {
|
|
56
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
57
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
58
|
+
try {
|
|
59
|
+
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
60
|
+
const config = JSON.parse(content);
|
|
61
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
62
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
63
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
64
|
+
}
|
|
65
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
66
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function fetchSessionContext() {
|
|
74
|
+
if (!API_KEY) return null;
|
|
75
|
+
try {
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
78
|
+
const url = new URL(`${API_URL}/api/v1/context`);
|
|
79
|
+
if (WORKSPACE_ID) url.searchParams.set("workspace_id", WORKSPACE_ID);
|
|
80
|
+
if (PROJECT_ID) url.searchParams.set("project_id", PROJECT_ID);
|
|
81
|
+
url.searchParams.set("include_rules", "true");
|
|
82
|
+
url.searchParams.set("include_lessons", "true");
|
|
83
|
+
url.searchParams.set("include_decisions", "true");
|
|
84
|
+
url.searchParams.set("include_plans", "true");
|
|
85
|
+
url.searchParams.set("limit", "5");
|
|
86
|
+
const response = await fetch(url.toString(), {
|
|
87
|
+
method: "GET",
|
|
88
|
+
headers: {
|
|
89
|
+
"X-API-Key": API_KEY
|
|
90
|
+
},
|
|
91
|
+
signal: controller.signal
|
|
92
|
+
});
|
|
93
|
+
clearTimeout(timeoutId);
|
|
94
|
+
if (response.ok) {
|
|
95
|
+
return await response.json();
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function formatContext(ctx) {
|
|
103
|
+
if (!ctx) {
|
|
104
|
+
return `[ContextStream Session Start]
|
|
105
|
+
|
|
106
|
+
No stored context found. Call \`mcp__contextstream__context(user_message="starting new session")\` to initialize.`;
|
|
107
|
+
}
|
|
108
|
+
const parts = ["[ContextStream Session Start]"];
|
|
109
|
+
if (ctx.lessons && ctx.lessons.length > 0) {
|
|
110
|
+
parts.push("\n## \u26A0\uFE0F Lessons from Past Mistakes");
|
|
111
|
+
for (const lesson of ctx.lessons.slice(0, 3)) {
|
|
112
|
+
parts.push(`- **${lesson.title}**: ${lesson.prevention}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (ctx.active_plans && ctx.active_plans.length > 0) {
|
|
116
|
+
parts.push("\n## \u{1F4CB} Active Plans");
|
|
117
|
+
for (const plan of ctx.active_plans.slice(0, 3)) {
|
|
118
|
+
parts.push(`- ${plan.title} (${plan.status})`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (ctx.pending_tasks && ctx.pending_tasks.length > 0) {
|
|
122
|
+
parts.push("\n## \u2705 Pending Tasks");
|
|
123
|
+
for (const task of ctx.pending_tasks.slice(0, 5)) {
|
|
124
|
+
parts.push(`- ${task.title}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (ctx.recent_decisions && ctx.recent_decisions.length > 0) {
|
|
128
|
+
parts.push("\n## \u{1F4DD} Recent Decisions");
|
|
129
|
+
for (const decision of ctx.recent_decisions.slice(0, 3)) {
|
|
130
|
+
parts.push(`- **${decision.title}**`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
parts.push("\n---");
|
|
134
|
+
parts.push('Call `mcp__contextstream__context(user_message="...")` for task-specific context.');
|
|
135
|
+
return parts.join("\n");
|
|
136
|
+
}
|
|
137
|
+
async function runSessionInitHook() {
|
|
138
|
+
if (!ENABLED) {
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
let inputData = "";
|
|
142
|
+
for await (const chunk of process.stdin) {
|
|
143
|
+
inputData += chunk;
|
|
144
|
+
}
|
|
145
|
+
if (!inputData.trim()) {
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
let input;
|
|
149
|
+
try {
|
|
150
|
+
input = JSON.parse(inputData);
|
|
151
|
+
} catch {
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
const cwd = input.cwd || process.cwd();
|
|
155
|
+
loadConfigFromMcpJson(cwd);
|
|
156
|
+
const context = await fetchSessionContext();
|
|
157
|
+
const formattedContext = formatContext(context);
|
|
158
|
+
console.log(
|
|
159
|
+
JSON.stringify({
|
|
160
|
+
hookSpecificOutput: {
|
|
161
|
+
hookEventName: "SessionStart",
|
|
162
|
+
additionalContext: formattedContext
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
var isDirectRun = process.argv[1]?.includes("session-init") || process.argv[2] === "session-init";
|
|
169
|
+
if (isDirectRun) {
|
|
170
|
+
runSessionInitHook().catch(() => process.exit(0));
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
runSessionInitHook
|
|
174
|
+
};
|
|
@@ -1,17 +1,223 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/hooks/user-prompt-submit.ts
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
4
7
|
var ENABLED = process.env.CONTEXTSTREAM_REMINDER_ENABLED !== "false";
|
|
5
|
-
var
|
|
6
|
-
|
|
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
|
+
var PROJECT_ID = null;
|
|
12
|
+
var REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
1. FIRST: Call mcp__contextstream__context(user_message="...") before ANY other tool
|
|
15
|
+
- Returns: dynamic rules, lessons from past mistakes, relevant context
|
|
16
|
+
- Check response for: [LESSONS_WARNING], [RULES_NOTICE], preferences
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
2. FOR CODE SEARCH: Check index status, then search appropriately
|
|
19
|
+
\u26A0\uFE0F BEFORE searching: mcp__contextstream__project(action="index_status")
|
|
20
|
+
\u2705 IF indexed & fresh: Use mcp__contextstream__search(mode="hybrid", query="...")
|
|
21
|
+
\u2705 IF NOT indexed OR stale: Use local tools (Glob/Grep/Read) directly
|
|
22
|
+
\u2705 IF search returns 0 results: Fallback to local tools (Glob/Grep/Read)
|
|
23
|
+
|
|
24
|
+
3. WHEN LOCAL TOOLS (Glob/Grep/Read) ARE OK:
|
|
25
|
+
\u2705 Project is NOT indexed (index_status.indexed=false)
|
|
26
|
+
\u2705 Index is stale/outdated (>7 days old)
|
|
27
|
+
\u2705 ContextStream search returns 0 results or errors
|
|
28
|
+
\u2705 User explicitly requests local tools
|
|
29
|
+
|
|
30
|
+
4. FOR PLANS & TASKS: Use ContextStream, not file-based plans
|
|
31
|
+
\u2705 Plans: mcp__contextstream__session(action="capture_plan", ...)
|
|
32
|
+
\u2705 Tasks: mcp__contextstream__memory(action="create_task", ...)
|
|
33
|
+
\u274C DO NOT use EnterPlanMode or write plans to markdown files
|
|
34
|
+
|
|
35
|
+
5. CHECK THESE from context() response:
|
|
36
|
+
- Lessons: Past mistakes to avoid (shown as warnings)
|
|
37
|
+
- Reminders: Active reminders for this project
|
|
38
|
+
- Preferences: User's coding style and preferences
|
|
39
|
+
- Rules: Dynamic rules matched to current task
|
|
40
|
+
|
|
41
|
+
6. SKIP CONTEXTSTREAM: If user preference says "skip contextstream", use local tools instead
|
|
14
42
|
[END]`;
|
|
43
|
+
var ENHANCED_REMINDER_HEADER = `[CONTEXTSTREAM - ENHANCED CONTEXT]
|
|
44
|
+
|
|
45
|
+
`;
|
|
46
|
+
function loadConfigFromMcpJson(cwd) {
|
|
47
|
+
let searchDir = path.resolve(cwd);
|
|
48
|
+
for (let i = 0; i < 5; i++) {
|
|
49
|
+
if (!API_KEY) {
|
|
50
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
51
|
+
if (fs.existsSync(mcpPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
54
|
+
const config = JSON.parse(content);
|
|
55
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
56
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
57
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
58
|
+
}
|
|
59
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
60
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
61
|
+
}
|
|
62
|
+
if (csEnv?.CONTEXTSTREAM_WORKSPACE_ID) {
|
|
63
|
+
WORKSPACE_ID = csEnv.CONTEXTSTREAM_WORKSPACE_ID;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!WORKSPACE_ID || !PROJECT_ID) {
|
|
70
|
+
const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
71
|
+
if (fs.existsSync(csConfigPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const content = fs.readFileSync(csConfigPath, "utf-8");
|
|
74
|
+
const csConfig = JSON.parse(content);
|
|
75
|
+
if (csConfig.workspace_id && !WORKSPACE_ID) {
|
|
76
|
+
WORKSPACE_ID = csConfig.workspace_id;
|
|
77
|
+
}
|
|
78
|
+
if (csConfig.project_id && !PROJECT_ID) {
|
|
79
|
+
PROJECT_ID = csConfig.project_id;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const parentDir = path.dirname(searchDir);
|
|
86
|
+
if (parentDir === searchDir) break;
|
|
87
|
+
searchDir = parentDir;
|
|
88
|
+
}
|
|
89
|
+
if (!API_KEY) {
|
|
90
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
91
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
94
|
+
const config = JSON.parse(content);
|
|
95
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
96
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
97
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
98
|
+
}
|
|
99
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
100
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function fetchSessionContext() {
|
|
108
|
+
if (!API_KEY) return null;
|
|
109
|
+
try {
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
112
|
+
const url = new URL(`${API_URL}/api/v1/context`);
|
|
113
|
+
if (WORKSPACE_ID) url.searchParams.set("workspace_id", WORKSPACE_ID);
|
|
114
|
+
if (PROJECT_ID) url.searchParams.set("project_id", PROJECT_ID);
|
|
115
|
+
url.searchParams.set("include_lessons", "true");
|
|
116
|
+
url.searchParams.set("include_decisions", "true");
|
|
117
|
+
url.searchParams.set("include_plans", "true");
|
|
118
|
+
url.searchParams.set("include_reminders", "true");
|
|
119
|
+
url.searchParams.set("limit", "3");
|
|
120
|
+
const response = await fetch(url.toString(), {
|
|
121
|
+
method: "GET",
|
|
122
|
+
headers: {
|
|
123
|
+
"X-API-Key": API_KEY
|
|
124
|
+
},
|
|
125
|
+
signal: controller.signal
|
|
126
|
+
});
|
|
127
|
+
clearTimeout(timeoutId);
|
|
128
|
+
if (response.ok) {
|
|
129
|
+
return await response.json();
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function buildEnhancedReminder(ctx, isNewSession2) {
|
|
137
|
+
const parts = [ENHANCED_REMINDER_HEADER];
|
|
138
|
+
if (isNewSession2) {
|
|
139
|
+
parts.push(`## \u{1F680} NEW SESSION DETECTED
|
|
140
|
+
1. Call \`init(folder_path="...")\` - this triggers project indexing
|
|
141
|
+
2. Wait for indexing: if \`init\` returns \`indexing_status: "started"\`, files are being indexed
|
|
142
|
+
3. Then call \`context(user_message="...")\` for task-specific context
|
|
143
|
+
4. Use \`search(mode="hybrid")\` for code discovery (not Glob/Grep/Read)
|
|
144
|
+
|
|
145
|
+
`);
|
|
146
|
+
}
|
|
147
|
+
if (ctx?.lessons && ctx.lessons.length > 0) {
|
|
148
|
+
parts.push(`## \u26A0\uFE0F LESSONS FROM PAST MISTAKES`);
|
|
149
|
+
for (const lesson of ctx.lessons.slice(0, 3)) {
|
|
150
|
+
parts.push(`- **${lesson.title}**: ${lesson.prevention}`);
|
|
151
|
+
}
|
|
152
|
+
parts.push("");
|
|
153
|
+
}
|
|
154
|
+
if (ctx?.active_plans && ctx.active_plans.length > 0) {
|
|
155
|
+
parts.push(`## \u{1F4CB} Active Plans`);
|
|
156
|
+
for (const plan of ctx.active_plans.slice(0, 3)) {
|
|
157
|
+
parts.push(`- ${plan.title} (${plan.status})`);
|
|
158
|
+
}
|
|
159
|
+
parts.push("");
|
|
160
|
+
}
|
|
161
|
+
if (ctx?.pending_tasks && ctx.pending_tasks.length > 0) {
|
|
162
|
+
parts.push(`## \u2705 Pending Tasks`);
|
|
163
|
+
for (const task of ctx.pending_tasks.slice(0, 5)) {
|
|
164
|
+
parts.push(`- ${task.title}`);
|
|
165
|
+
}
|
|
166
|
+
parts.push("");
|
|
167
|
+
}
|
|
168
|
+
if (ctx?.reminders && ctx.reminders.length > 0) {
|
|
169
|
+
parts.push(`## \u{1F514} Reminders`);
|
|
170
|
+
for (const reminder of ctx.reminders.slice(0, 3)) {
|
|
171
|
+
parts.push(`- ${reminder.title}`);
|
|
172
|
+
}
|
|
173
|
+
parts.push("");
|
|
174
|
+
}
|
|
175
|
+
parts.push("---\n");
|
|
176
|
+
parts.push(REMINDER);
|
|
177
|
+
parts.push(`
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
## \u{1F6A8} FILE INDEXING & SEARCH - CRITICAL (No PostToolUse Hook) \u{1F6A8}
|
|
181
|
+
|
|
182
|
+
**This editor does NOT have automatic file indexing after Edit/Write.**
|
|
183
|
+
|
|
184
|
+
### \u26A0\uFE0F BEFORE ANY SEARCH - Check Index Status:
|
|
185
|
+
\`\`\`
|
|
186
|
+
project(action="index_status")
|
|
187
|
+
\`\`\`
|
|
188
|
+
Returns: \`indexed\` (true/false), \`last_indexed_at\`, \`file_count\`
|
|
189
|
+
|
|
190
|
+
### \u{1F50D} Search Decision Tree:
|
|
191
|
+
|
|
192
|
+
**IF indexed=true AND last_indexed_at is recent:**
|
|
193
|
+
\u2192 Use \`search(mode="hybrid", query="...")\`
|
|
194
|
+
|
|
195
|
+
**IF indexed=false OR last_indexed_at is stale (>7 days):**
|
|
196
|
+
\u2192 Use local tools (Glob/Grep/Read) directly
|
|
197
|
+
\u2192 OR run \`project(action="index")\` first, then search
|
|
198
|
+
|
|
199
|
+
**IF search returns 0 results or errors:**
|
|
200
|
+
\u2192 Fallback to local tools (Glob/Grep/Read)
|
|
201
|
+
|
|
202
|
+
### \u2705 When Local Tools (Glob/Grep/Read) Are OK:
|
|
203
|
+
- Project is NOT indexed
|
|
204
|
+
- Index is stale/outdated (>7 days)
|
|
205
|
+
- ContextStream search returns 0 results
|
|
206
|
+
- ContextStream returns errors
|
|
207
|
+
- User explicitly requests local tools
|
|
208
|
+
|
|
209
|
+
### On Session Start:
|
|
210
|
+
1. Call \`init(folder_path="...")\` - triggers initial indexing
|
|
211
|
+
2. Check \`project(action="index_status")\` before searching
|
|
212
|
+
3. If not indexed: use local tools OR wait for indexing
|
|
213
|
+
|
|
214
|
+
### After File Changes (Edit/Write/Create):
|
|
215
|
+
Files are NOT auto-indexed. You MUST:
|
|
216
|
+
1. After significant edits: \`project(action="index")\`
|
|
217
|
+
2. For single file: \`project(action="ingest_local", path="<file>")\`
|
|
218
|
+
3. Then search will find your changes`);
|
|
219
|
+
return parts.join("\n");
|
|
220
|
+
}
|
|
15
221
|
function detectEditorFormat(input) {
|
|
16
222
|
if (input.hookName !== void 0) {
|
|
17
223
|
return "cline";
|
|
@@ -19,8 +225,23 @@ function detectEditorFormat(input) {
|
|
|
19
225
|
if (input.hook_event_name === "beforeSubmitPrompt") {
|
|
20
226
|
return "cursor";
|
|
21
227
|
}
|
|
228
|
+
if (input.hook_event_name === "beforeAgentAction" || input.hook_event_name === "onPromptSubmit") {
|
|
229
|
+
return "antigravity";
|
|
230
|
+
}
|
|
22
231
|
return "claude";
|
|
23
232
|
}
|
|
233
|
+
function isNewSession(input, editorFormat) {
|
|
234
|
+
if (editorFormat === "claude" && input.session?.messages) {
|
|
235
|
+
return input.session.messages.length <= 1;
|
|
236
|
+
}
|
|
237
|
+
if (editorFormat === "cursor" && input.history !== void 0) {
|
|
238
|
+
return input.history.length === 0;
|
|
239
|
+
}
|
|
240
|
+
if (editorFormat === "antigravity" && input.history !== void 0) {
|
|
241
|
+
return input.history.length === 0;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
24
245
|
async function runUserPromptSubmitHook() {
|
|
25
246
|
if (!ENABLED) {
|
|
26
247
|
process.exit(0);
|
|
@@ -39,6 +260,7 @@ async function runUserPromptSubmitHook() {
|
|
|
39
260
|
process.exit(0);
|
|
40
261
|
}
|
|
41
262
|
const editorFormat = detectEditorFormat(input);
|
|
263
|
+
const cwd = input.cwd || process.cwd();
|
|
42
264
|
if (editorFormat === "claude") {
|
|
43
265
|
console.log(
|
|
44
266
|
JSON.stringify({
|
|
@@ -49,17 +271,36 @@ async function runUserPromptSubmitHook() {
|
|
|
49
271
|
})
|
|
50
272
|
);
|
|
51
273
|
} else if (editorFormat === "cline") {
|
|
274
|
+
loadConfigFromMcpJson(cwd);
|
|
275
|
+
const newSession = isNewSession(input, editorFormat);
|
|
276
|
+
const ctx = await fetchSessionContext();
|
|
277
|
+
const enhancedReminder = buildEnhancedReminder(ctx, newSession);
|
|
52
278
|
console.log(
|
|
53
279
|
JSON.stringify({
|
|
54
280
|
cancel: false,
|
|
55
|
-
contextModification:
|
|
281
|
+
contextModification: enhancedReminder
|
|
56
282
|
})
|
|
57
283
|
);
|
|
58
284
|
} else if (editorFormat === "cursor") {
|
|
285
|
+
loadConfigFromMcpJson(cwd);
|
|
286
|
+
const newSession = isNewSession(input, editorFormat);
|
|
287
|
+
const ctx = await fetchSessionContext();
|
|
288
|
+
const cursorReminder = ctx?.lessons?.length ? `[CONTEXTSTREAM] \u26A0\uFE0F ${ctx.lessons.length} lessons from past mistakes. Use search(mode="hybrid") before Glob/Grep. Call context() first. After file edits: project(action="index") to re-index.` : `[CONTEXTSTREAM] Use search(mode="hybrid") before Glob/Grep/Read. Call context() first. After file edits: project(action="index") to re-index.`;
|
|
59
289
|
console.log(
|
|
60
290
|
JSON.stringify({
|
|
61
291
|
continue: true,
|
|
62
|
-
user_message:
|
|
292
|
+
user_message: cursorReminder
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
} else if (editorFormat === "antigravity") {
|
|
296
|
+
loadConfigFromMcpJson(cwd);
|
|
297
|
+
const newSession = isNewSession(input, editorFormat);
|
|
298
|
+
const ctx = await fetchSessionContext();
|
|
299
|
+
const enhancedReminder = buildEnhancedReminder(ctx, newSession);
|
|
300
|
+
console.log(
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
cancel: false,
|
|
303
|
+
contextModification: enhancedReminder
|
|
63
304
|
})
|
|
64
305
|
);
|
|
65
306
|
}
|