@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
package/dist/hooks/auto-rules.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
4
10
|
var __esm = (fn, res) => function __init() {
|
|
5
11
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
12
|
};
|
|
@@ -28,6 +34,7 @@ __export(hooks_config_exports, {
|
|
|
28
34
|
getClineHooksDir: () => getClineHooksDir,
|
|
29
35
|
getCursorHooksConfigPath: () => getCursorHooksConfigPath,
|
|
30
36
|
getCursorHooksDir: () => getCursorHooksDir,
|
|
37
|
+
getHookCommand: () => getHookCommand,
|
|
31
38
|
getHooksDir: () => getHooksDir,
|
|
32
39
|
getIndexStatusPath: () => getIndexStatusPath,
|
|
33
40
|
getKiloCodeHooksDir: () => getKiloCodeHooksDir,
|
|
@@ -53,6 +60,23 @@ __export(hooks_config_exports, {
|
|
|
53
60
|
import * as fs from "node:fs/promises";
|
|
54
61
|
import * as path from "node:path";
|
|
55
62
|
import { homedir } from "node:os";
|
|
63
|
+
import { fileURLToPath } from "node:url";
|
|
64
|
+
function getHookCommand(hookName) {
|
|
65
|
+
const fs3 = __require("node:fs");
|
|
66
|
+
const binaryPath = "/usr/local/bin/contextstream-mcp";
|
|
67
|
+
if (fs3.existsSync(binaryPath)) {
|
|
68
|
+
return `${binaryPath} hook ${hookName}`;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
72
|
+
const indexPath = path.join(__dirname, "index.js");
|
|
73
|
+
if (fs3.existsSync(indexPath)) {
|
|
74
|
+
return `node ${indexPath} hook ${hookName}`;
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
return `npx @contextstream/mcp-server hook ${hookName}`;
|
|
79
|
+
}
|
|
56
80
|
function getClaudeSettingsPath(scope, projectPath) {
|
|
57
81
|
if (scope === "user") {
|
|
58
82
|
return path.join(homedir(), ".claude", "settings.json");
|
|
@@ -72,19 +96,31 @@ function buildHooksConfig(options) {
|
|
|
72
96
|
hooks: [
|
|
73
97
|
{
|
|
74
98
|
type: "command",
|
|
75
|
-
command: "
|
|
99
|
+
command: getHookCommand("user-prompt-submit"),
|
|
76
100
|
timeout: 5
|
|
77
101
|
}
|
|
78
102
|
]
|
|
79
103
|
}
|
|
80
104
|
];
|
|
105
|
+
if (options?.includeOnSaveIntent !== false) {
|
|
106
|
+
userPromptHooks.push({
|
|
107
|
+
matcher: "*",
|
|
108
|
+
hooks: [
|
|
109
|
+
{
|
|
110
|
+
type: "command",
|
|
111
|
+
command: getHookCommand("on-save-intent"),
|
|
112
|
+
timeout: 5
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
81
117
|
if (options?.includeMediaAware !== false) {
|
|
82
118
|
userPromptHooks.push({
|
|
83
119
|
matcher: "*",
|
|
84
120
|
hooks: [
|
|
85
121
|
{
|
|
86
122
|
type: "command",
|
|
87
|
-
command: "
|
|
123
|
+
command: getHookCommand("media-aware"),
|
|
88
124
|
timeout: 5
|
|
89
125
|
}
|
|
90
126
|
]
|
|
@@ -97,7 +133,7 @@ function buildHooksConfig(options) {
|
|
|
97
133
|
hooks: [
|
|
98
134
|
{
|
|
99
135
|
type: "command",
|
|
100
|
-
command: "
|
|
136
|
+
command: getHookCommand("pre-tool-use"),
|
|
101
137
|
timeout: 5
|
|
102
138
|
}
|
|
103
139
|
]
|
|
@@ -108,12 +144,39 @@ function buildHooksConfig(options) {
|
|
|
108
144
|
if (options?.includePreCompact !== false) {
|
|
109
145
|
config.PreCompact = [
|
|
110
146
|
{
|
|
111
|
-
// Match both manual (/compact) and automatic compaction
|
|
112
147
|
matcher: "*",
|
|
113
148
|
hooks: [
|
|
114
149
|
{
|
|
115
150
|
type: "command",
|
|
116
|
-
command: "
|
|
151
|
+
command: getHookCommand("pre-compact"),
|
|
152
|
+
timeout: 10
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
if (options?.includeSessionInit !== false) {
|
|
159
|
+
config.SessionStart = [
|
|
160
|
+
{
|
|
161
|
+
matcher: "*",
|
|
162
|
+
hooks: [
|
|
163
|
+
{
|
|
164
|
+
type: "command",
|
|
165
|
+
command: getHookCommand("session-init"),
|
|
166
|
+
timeout: 10
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
if (options?.includeSessionEnd !== false) {
|
|
173
|
+
config.Stop = [
|
|
174
|
+
{
|
|
175
|
+
matcher: "*",
|
|
176
|
+
hooks: [
|
|
177
|
+
{
|
|
178
|
+
type: "command",
|
|
179
|
+
command: getHookCommand("session-end"),
|
|
117
180
|
timeout: 10
|
|
118
181
|
}
|
|
119
182
|
]
|
|
@@ -127,7 +190,7 @@ function buildHooksConfig(options) {
|
|
|
127
190
|
hooks: [
|
|
128
191
|
{
|
|
129
192
|
type: "command",
|
|
130
|
-
command: "
|
|
193
|
+
command: getHookCommand("post-write"),
|
|
131
194
|
timeout: 10
|
|
132
195
|
}
|
|
133
196
|
]
|
|
@@ -139,12 +202,60 @@ function buildHooksConfig(options) {
|
|
|
139
202
|
hooks: [
|
|
140
203
|
{
|
|
141
204
|
type: "command",
|
|
142
|
-
command: "
|
|
205
|
+
command: getHookCommand("auto-rules"),
|
|
143
206
|
timeout: 15
|
|
144
207
|
}
|
|
145
208
|
]
|
|
146
209
|
});
|
|
147
210
|
}
|
|
211
|
+
if (options?.includeOnBash !== false) {
|
|
212
|
+
postToolUseHooks.push({
|
|
213
|
+
matcher: "Bash",
|
|
214
|
+
hooks: [
|
|
215
|
+
{
|
|
216
|
+
type: "command",
|
|
217
|
+
command: getHookCommand("on-bash"),
|
|
218
|
+
timeout: 5
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (options?.includeOnTask !== false) {
|
|
224
|
+
postToolUseHooks.push({
|
|
225
|
+
matcher: "Task",
|
|
226
|
+
hooks: [
|
|
227
|
+
{
|
|
228
|
+
type: "command",
|
|
229
|
+
command: getHookCommand("on-task"),
|
|
230
|
+
timeout: 5
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (options?.includeOnRead !== false) {
|
|
236
|
+
postToolUseHooks.push({
|
|
237
|
+
matcher: "Read|Glob|Grep",
|
|
238
|
+
hooks: [
|
|
239
|
+
{
|
|
240
|
+
type: "command",
|
|
241
|
+
command: getHookCommand("on-read"),
|
|
242
|
+
timeout: 5
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (options?.includeOnWeb !== false) {
|
|
248
|
+
postToolUseHooks.push({
|
|
249
|
+
matcher: "WebFetch|WebSearch",
|
|
250
|
+
hooks: [
|
|
251
|
+
{
|
|
252
|
+
type: "command",
|
|
253
|
+
command: getHookCommand("on-web"),
|
|
254
|
+
timeout: 5
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
});
|
|
258
|
+
}
|
|
148
259
|
if (postToolUseHooks.length > 0) {
|
|
149
260
|
config.PostToolUse = postToolUseHooks;
|
|
150
261
|
}
|
|
@@ -154,17 +265,17 @@ async function installHookScripts(options) {
|
|
|
154
265
|
const hooksDir = getHooksDir();
|
|
155
266
|
await fs.mkdir(hooksDir, { recursive: true });
|
|
156
267
|
const result = {
|
|
157
|
-
preToolUse: "
|
|
158
|
-
userPrompt: "
|
|
268
|
+
preToolUse: getHookCommand("pre-tool-use"),
|
|
269
|
+
userPrompt: getHookCommand("user-prompt-submit")
|
|
159
270
|
};
|
|
160
271
|
if (options?.includePreCompact !== false) {
|
|
161
|
-
result.preCompact = "
|
|
272
|
+
result.preCompact = getHookCommand("pre-compact");
|
|
162
273
|
}
|
|
163
274
|
if (options?.includeMediaAware !== false) {
|
|
164
|
-
result.mediaAware = "
|
|
275
|
+
result.mediaAware = getHookCommand("media-aware");
|
|
165
276
|
}
|
|
166
277
|
if (options?.includeAutoRules !== false) {
|
|
167
|
-
result.autoRules = "
|
|
278
|
+
result.autoRules = getHookCommand("auto-rules");
|
|
168
279
|
}
|
|
169
280
|
return result;
|
|
170
281
|
}
|
|
@@ -200,20 +311,20 @@ function mergeHooksIntoSettings(existingSettings, newHooks) {
|
|
|
200
311
|
async function installClaudeCodeHooks(options) {
|
|
201
312
|
const result = { scripts: [], settings: [] };
|
|
202
313
|
result.scripts.push(
|
|
203
|
-
"
|
|
204
|
-
"
|
|
314
|
+
getHookCommand("pre-tool-use"),
|
|
315
|
+
getHookCommand("user-prompt-submit")
|
|
205
316
|
);
|
|
206
317
|
if (options.includePreCompact !== false) {
|
|
207
|
-
result.scripts.push("
|
|
318
|
+
result.scripts.push(getHookCommand("pre-compact"));
|
|
208
319
|
}
|
|
209
320
|
if (options.includeMediaAware !== false) {
|
|
210
|
-
result.scripts.push("
|
|
321
|
+
result.scripts.push(getHookCommand("media-aware"));
|
|
211
322
|
}
|
|
212
323
|
if (options.includePostWrite !== false) {
|
|
213
|
-
result.scripts.push("
|
|
324
|
+
result.scripts.push(getHookCommand("post-write"));
|
|
214
325
|
}
|
|
215
326
|
if (options.includeAutoRules !== false) {
|
|
216
|
-
result.scripts.push("
|
|
327
|
+
result.scripts.push(getHookCommand("auto-rules"));
|
|
217
328
|
}
|
|
218
329
|
const hooksConfig = buildHooksConfig({
|
|
219
330
|
includePreCompact: options.includePreCompact,
|
|
@@ -491,6 +602,8 @@ async function installCursorHookScripts(options) {
|
|
|
491
602
|
};
|
|
492
603
|
const filteredPreToolUse = filterContextStreamHooks(existingConfig.hooks.preToolUse);
|
|
493
604
|
const filteredBeforeSubmit = filterContextStreamHooks(existingConfig.hooks.beforeSubmitPrompt);
|
|
605
|
+
const preToolUseCommand = getHookCommand("pre-tool-use");
|
|
606
|
+
const userPromptCommand = getHookCommand("user-prompt-submit");
|
|
494
607
|
const config = {
|
|
495
608
|
version: 1,
|
|
496
609
|
hooks: {
|
|
@@ -498,7 +611,7 @@ async function installCursorHookScripts(options) {
|
|
|
498
611
|
preToolUse: [
|
|
499
612
|
...filteredPreToolUse,
|
|
500
613
|
{
|
|
501
|
-
command:
|
|
614
|
+
command: preToolUseCommand,
|
|
502
615
|
type: "command",
|
|
503
616
|
timeout: 5,
|
|
504
617
|
matcher: { tool_name: "Glob|Grep|search_files|list_files|ripgrep" }
|
|
@@ -507,7 +620,7 @@ async function installCursorHookScripts(options) {
|
|
|
507
620
|
beforeSubmitPrompt: [
|
|
508
621
|
...filteredBeforeSubmit,
|
|
509
622
|
{
|
|
510
|
-
command:
|
|
623
|
+
command: userPromptCommand,
|
|
511
624
|
type: "command",
|
|
512
625
|
timeout: 5
|
|
513
626
|
}
|
|
@@ -517,8 +630,8 @@ async function installCursorHookScripts(options) {
|
|
|
517
630
|
await writeCursorHooksConfig(config, options.scope, options.projectPath);
|
|
518
631
|
const configPath = getCursorHooksConfigPath(options.scope, options.projectPath);
|
|
519
632
|
return {
|
|
520
|
-
preToolUse:
|
|
521
|
-
beforeSubmitPrompt:
|
|
633
|
+
preToolUse: preToolUseCommand,
|
|
634
|
+
beforeSubmitPrompt: userPromptCommand,
|
|
522
635
|
config: configPath
|
|
523
636
|
};
|
|
524
637
|
}
|
|
@@ -1327,11 +1440,13 @@ esac
|
|
|
1327
1440
|
|
|
1328
1441
|
exit 0
|
|
1329
1442
|
`;
|
|
1330
|
-
CLINE_HOOK_WRAPPER = (hookName) =>
|
|
1443
|
+
CLINE_HOOK_WRAPPER = (hookName) => {
|
|
1444
|
+
const command = getHookCommand(hookName);
|
|
1445
|
+
return `#!/bin/bash
|
|
1331
1446
|
# ContextStream ${hookName} Hook Wrapper for Cline/Roo/Kilo Code
|
|
1332
|
-
|
|
1333
|
-
exec npx @contextstream/mcp-server hook ${hookName}
|
|
1447
|
+
exec ${command}
|
|
1334
1448
|
`;
|
|
1449
|
+
};
|
|
1335
1450
|
CURSOR_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
1336
1451
|
"""
|
|
1337
1452
|
ContextStream PreToolUse Hook for Cursor
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/on-bash.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_BASH_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 captureCommand(command, output, exitCode, isError, sessionId) {
|
|
67
|
+
if (!API_KEY) return;
|
|
68
|
+
const payload = {
|
|
69
|
+
event_type: isError ? "bash_error" : "bash_command",
|
|
70
|
+
title: isError ? `Bash Error: ${command.slice(0, 50)}...` : `Command: ${command.slice(0, 50)}...`,
|
|
71
|
+
content: JSON.stringify({
|
|
72
|
+
command,
|
|
73
|
+
output: output.slice(0, 2e3),
|
|
74
|
+
exit_code: exitCode,
|
|
75
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
76
|
+
}),
|
|
77
|
+
importance: isError ? "high" : "low",
|
|
78
|
+
tags: isError ? ["bash", "error", "command"] : ["bash", "command"],
|
|
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 suggestLesson(command, error) {
|
|
102
|
+
const errorPatterns = [
|
|
103
|
+
{
|
|
104
|
+
pattern: /command not found/i,
|
|
105
|
+
lesson: `The command "${command.split(" ")[0]}" is not installed. Check if the package needs to be installed first.`
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
pattern: /permission denied/i,
|
|
109
|
+
lesson: "Permission denied. May need sudo or to check file permissions."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
pattern: /no such file or directory/i,
|
|
113
|
+
lesson: "Path does not exist. Verify the file/directory path before running commands."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
pattern: /EADDRINUSE|address already in use/i,
|
|
117
|
+
lesson: "Port is already in use. Kill the existing process or use a different port."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
pattern: /npm ERR!|ERESOLVE/i,
|
|
121
|
+
lesson: "npm dependency conflict. Try `npm install --legacy-peer-deps` or check package versions."
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
pattern: /ENOENT.*package\.json/i,
|
|
125
|
+
lesson: "No package.json found. Make sure you're in the right directory or run `npm init`."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
pattern: /git.*not a git repository/i,
|
|
129
|
+
lesson: "Not in a git repository. Run `git init` or navigate to a git repo."
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
for (const { pattern, lesson } of errorPatterns) {
|
|
133
|
+
if (pattern.test(error)) {
|
|
134
|
+
return lesson;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
async function runOnBashHook() {
|
|
140
|
+
if (!ENABLED) {
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
let inputData = "";
|
|
144
|
+
for await (const chunk of process.stdin) {
|
|
145
|
+
inputData += chunk;
|
|
146
|
+
}
|
|
147
|
+
if (!inputData.trim()) {
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
let input;
|
|
151
|
+
try {
|
|
152
|
+
input = JSON.parse(inputData);
|
|
153
|
+
} catch {
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
if (input.tool_name !== "Bash") {
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
const cwd = input.cwd || process.cwd();
|
|
160
|
+
loadConfigFromMcpJson(cwd);
|
|
161
|
+
const command = input.tool_input?.command || "";
|
|
162
|
+
const output = input.tool_result?.output || input.tool_result?.error || "";
|
|
163
|
+
const exitCode = input.tool_result?.exit_code ?? 0;
|
|
164
|
+
const sessionId = input.session_id || "unknown";
|
|
165
|
+
const isError = exitCode !== 0 || !!input.tool_result?.error;
|
|
166
|
+
captureCommand(command, output, exitCode, isError, sessionId).catch(() => {
|
|
167
|
+
});
|
|
168
|
+
if (isError) {
|
|
169
|
+
const lesson = await suggestLesson(command, output);
|
|
170
|
+
if (lesson) {
|
|
171
|
+
console.log(
|
|
172
|
+
JSON.stringify({
|
|
173
|
+
hookSpecificOutput: {
|
|
174
|
+
hookEventName: "PostToolUse",
|
|
175
|
+
additionalContext: `[ContextStream Insight] ${lesson}`
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
var isDirectRun = process.argv[1]?.includes("on-bash") || process.argv[2] === "on-bash";
|
|
185
|
+
if (isDirectRun) {
|
|
186
|
+
runOnBashHook().catch(() => process.exit(0));
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
runOnBashHook
|
|
190
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/on-read.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_READ_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
|
+
var recentCaptures = /* @__PURE__ */ new Set();
|
|
12
|
+
var CAPTURE_WINDOW_MS = 6e4;
|
|
13
|
+
function loadConfigFromMcpJson(cwd) {
|
|
14
|
+
let searchDir = path.resolve(cwd);
|
|
15
|
+
for (let i = 0; i < 5; i++) {
|
|
16
|
+
if (!API_KEY) {
|
|
17
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
18
|
+
if (fs.existsSync(mcpPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
21
|
+
const config = JSON.parse(content);
|
|
22
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
23
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
24
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
25
|
+
}
|
|
26
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
27
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!WORKSPACE_ID) {
|
|
34
|
+
const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
35
|
+
if (fs.existsSync(csConfigPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(csConfigPath, "utf-8");
|
|
38
|
+
const csConfig = JSON.parse(content);
|
|
39
|
+
if (csConfig.workspace_id) {
|
|
40
|
+
WORKSPACE_ID = csConfig.workspace_id;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const parentDir = path.dirname(searchDir);
|
|
47
|
+
if (parentDir === searchDir) break;
|
|
48
|
+
searchDir = parentDir;
|
|
49
|
+
}
|
|
50
|
+
if (!API_KEY) {
|
|
51
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
52
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
55
|
+
const config = JSON.parse(content);
|
|
56
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
57
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
58
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
59
|
+
}
|
|
60
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
61
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function captureExploration(toolName, target, resultSummary, sessionId) {
|
|
69
|
+
if (!API_KEY) return;
|
|
70
|
+
const cacheKey = `${toolName}:${target}`;
|
|
71
|
+
if (recentCaptures.has(cacheKey)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
recentCaptures.add(cacheKey);
|
|
75
|
+
setTimeout(() => recentCaptures.delete(cacheKey), CAPTURE_WINDOW_MS);
|
|
76
|
+
const payload = {
|
|
77
|
+
event_type: "file_exploration",
|
|
78
|
+
title: `${toolName}: ${target.slice(0, 50)}`,
|
|
79
|
+
content: JSON.stringify({
|
|
80
|
+
tool: toolName,
|
|
81
|
+
target,
|
|
82
|
+
result_summary: resultSummary.slice(0, 500),
|
|
83
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
84
|
+
}),
|
|
85
|
+
importance: "low",
|
|
86
|
+
tags: ["exploration", toolName.toLowerCase()],
|
|
87
|
+
source_type: "hook",
|
|
88
|
+
session_id: sessionId
|
|
89
|
+
};
|
|
90
|
+
if (WORKSPACE_ID) {
|
|
91
|
+
payload.workspace_id = WORKSPACE_ID;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
96
|
+
await fetch(`${API_URL}/api/v1/memory/events`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"X-API-Key": API_KEY
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify(payload),
|
|
103
|
+
signal: controller.signal
|
|
104
|
+
});
|
|
105
|
+
clearTimeout(timeoutId);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function runOnReadHook() {
|
|
110
|
+
if (!ENABLED) {
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
let inputData = "";
|
|
114
|
+
for await (const chunk of process.stdin) {
|
|
115
|
+
inputData += chunk;
|
|
116
|
+
}
|
|
117
|
+
if (!inputData.trim()) {
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
let input;
|
|
121
|
+
try {
|
|
122
|
+
input = JSON.parse(inputData);
|
|
123
|
+
} catch {
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
const toolName = input.tool_name || "";
|
|
127
|
+
if (!["Read", "Glob", "Grep"].includes(toolName)) {
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
const cwd = input.cwd || process.cwd();
|
|
131
|
+
loadConfigFromMcpJson(cwd);
|
|
132
|
+
const sessionId = input.session_id || "unknown";
|
|
133
|
+
let target = "";
|
|
134
|
+
let resultSummary = "";
|
|
135
|
+
switch (toolName) {
|
|
136
|
+
case "Read":
|
|
137
|
+
target = input.tool_input?.file_path || "";
|
|
138
|
+
resultSummary = `Read file: ${target}`;
|
|
139
|
+
break;
|
|
140
|
+
case "Glob":
|
|
141
|
+
target = input.tool_input?.pattern || "";
|
|
142
|
+
const globFiles = input.tool_result?.files || [];
|
|
143
|
+
resultSummary = `Found ${globFiles.length} files matching ${target}`;
|
|
144
|
+
break;
|
|
145
|
+
case "Grep":
|
|
146
|
+
target = input.tool_input?.pattern || "";
|
|
147
|
+
const matches = input.tool_result?.matches || 0;
|
|
148
|
+
resultSummary = `Found ${matches} matches for "${target}"`;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
if (target) {
|
|
152
|
+
captureExploration(toolName, target, resultSummary, sessionId).catch(() => {
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
var isDirectRun = process.argv[1]?.includes("on-read") || process.argv[2] === "on-read";
|
|
158
|
+
if (isDirectRun) {
|
|
159
|
+
runOnReadHook().catch(() => process.exit(0));
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
runOnReadHook
|
|
163
|
+
};
|