@contextstream/mcp-server 0.4.50 → 0.4.51
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 +136 -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/runner.js +2889 -0
- package/dist/hooks/session-end.js +191 -0
- package/dist/hooks/session-init.js +174 -0
- package/dist/index.js +1994 -146
- 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,19 @@ __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
|
+
try {
|
|
66
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
67
|
+
const indexPath = path.join(__dirname, "index.js");
|
|
68
|
+
const fs3 = __require("node:fs");
|
|
69
|
+
if (fs3.existsSync(indexPath)) {
|
|
70
|
+
return `node ${indexPath} hook ${hookName}`;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
return `npx @contextstream/mcp-server hook ${hookName}`;
|
|
75
|
+
}
|
|
56
76
|
function getClaudeSettingsPath(scope, projectPath) {
|
|
57
77
|
if (scope === "user") {
|
|
58
78
|
return path.join(homedir(), ".claude", "settings.json");
|
|
@@ -72,19 +92,31 @@ function buildHooksConfig(options) {
|
|
|
72
92
|
hooks: [
|
|
73
93
|
{
|
|
74
94
|
type: "command",
|
|
75
|
-
command: "
|
|
95
|
+
command: getHookCommand("user-prompt-submit"),
|
|
76
96
|
timeout: 5
|
|
77
97
|
}
|
|
78
98
|
]
|
|
79
99
|
}
|
|
80
100
|
];
|
|
101
|
+
if (options?.includeOnSaveIntent !== false) {
|
|
102
|
+
userPromptHooks.push({
|
|
103
|
+
matcher: "*",
|
|
104
|
+
hooks: [
|
|
105
|
+
{
|
|
106
|
+
type: "command",
|
|
107
|
+
command: getHookCommand("on-save-intent"),
|
|
108
|
+
timeout: 5
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
81
113
|
if (options?.includeMediaAware !== false) {
|
|
82
114
|
userPromptHooks.push({
|
|
83
115
|
matcher: "*",
|
|
84
116
|
hooks: [
|
|
85
117
|
{
|
|
86
118
|
type: "command",
|
|
87
|
-
command: "
|
|
119
|
+
command: getHookCommand("media-aware"),
|
|
88
120
|
timeout: 5
|
|
89
121
|
}
|
|
90
122
|
]
|
|
@@ -97,7 +129,7 @@ function buildHooksConfig(options) {
|
|
|
97
129
|
hooks: [
|
|
98
130
|
{
|
|
99
131
|
type: "command",
|
|
100
|
-
command: "
|
|
132
|
+
command: getHookCommand("pre-tool-use"),
|
|
101
133
|
timeout: 5
|
|
102
134
|
}
|
|
103
135
|
]
|
|
@@ -108,12 +140,39 @@ function buildHooksConfig(options) {
|
|
|
108
140
|
if (options?.includePreCompact !== false) {
|
|
109
141
|
config.PreCompact = [
|
|
110
142
|
{
|
|
111
|
-
// Match both manual (/compact) and automatic compaction
|
|
112
143
|
matcher: "*",
|
|
113
144
|
hooks: [
|
|
114
145
|
{
|
|
115
146
|
type: "command",
|
|
116
|
-
command: "
|
|
147
|
+
command: getHookCommand("pre-compact"),
|
|
148
|
+
timeout: 10
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
if (options?.includeSessionInit !== false) {
|
|
155
|
+
config.SessionStart = [
|
|
156
|
+
{
|
|
157
|
+
matcher: "*",
|
|
158
|
+
hooks: [
|
|
159
|
+
{
|
|
160
|
+
type: "command",
|
|
161
|
+
command: getHookCommand("session-init"),
|
|
162
|
+
timeout: 10
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
if (options?.includeSessionEnd !== false) {
|
|
169
|
+
config.Stop = [
|
|
170
|
+
{
|
|
171
|
+
matcher: "*",
|
|
172
|
+
hooks: [
|
|
173
|
+
{
|
|
174
|
+
type: "command",
|
|
175
|
+
command: getHookCommand("session-end"),
|
|
117
176
|
timeout: 10
|
|
118
177
|
}
|
|
119
178
|
]
|
|
@@ -127,7 +186,7 @@ function buildHooksConfig(options) {
|
|
|
127
186
|
hooks: [
|
|
128
187
|
{
|
|
129
188
|
type: "command",
|
|
130
|
-
command: "
|
|
189
|
+
command: getHookCommand("post-write"),
|
|
131
190
|
timeout: 10
|
|
132
191
|
}
|
|
133
192
|
]
|
|
@@ -139,12 +198,60 @@ function buildHooksConfig(options) {
|
|
|
139
198
|
hooks: [
|
|
140
199
|
{
|
|
141
200
|
type: "command",
|
|
142
|
-
command: "
|
|
201
|
+
command: getHookCommand("auto-rules"),
|
|
143
202
|
timeout: 15
|
|
144
203
|
}
|
|
145
204
|
]
|
|
146
205
|
});
|
|
147
206
|
}
|
|
207
|
+
if (options?.includeOnBash !== false) {
|
|
208
|
+
postToolUseHooks.push({
|
|
209
|
+
matcher: "Bash",
|
|
210
|
+
hooks: [
|
|
211
|
+
{
|
|
212
|
+
type: "command",
|
|
213
|
+
command: getHookCommand("on-bash"),
|
|
214
|
+
timeout: 5
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (options?.includeOnTask !== false) {
|
|
220
|
+
postToolUseHooks.push({
|
|
221
|
+
matcher: "Task",
|
|
222
|
+
hooks: [
|
|
223
|
+
{
|
|
224
|
+
type: "command",
|
|
225
|
+
command: getHookCommand("on-task"),
|
|
226
|
+
timeout: 5
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (options?.includeOnRead !== false) {
|
|
232
|
+
postToolUseHooks.push({
|
|
233
|
+
matcher: "Read|Glob|Grep",
|
|
234
|
+
hooks: [
|
|
235
|
+
{
|
|
236
|
+
type: "command",
|
|
237
|
+
command: getHookCommand("on-read"),
|
|
238
|
+
timeout: 5
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (options?.includeOnWeb !== false) {
|
|
244
|
+
postToolUseHooks.push({
|
|
245
|
+
matcher: "WebFetch|WebSearch",
|
|
246
|
+
hooks: [
|
|
247
|
+
{
|
|
248
|
+
type: "command",
|
|
249
|
+
command: getHookCommand("on-web"),
|
|
250
|
+
timeout: 5
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
});
|
|
254
|
+
}
|
|
148
255
|
if (postToolUseHooks.length > 0) {
|
|
149
256
|
config.PostToolUse = postToolUseHooks;
|
|
150
257
|
}
|
|
@@ -154,17 +261,17 @@ async function installHookScripts(options) {
|
|
|
154
261
|
const hooksDir = getHooksDir();
|
|
155
262
|
await fs.mkdir(hooksDir, { recursive: true });
|
|
156
263
|
const result = {
|
|
157
|
-
preToolUse: "
|
|
158
|
-
userPrompt: "
|
|
264
|
+
preToolUse: getHookCommand("pre-tool-use"),
|
|
265
|
+
userPrompt: getHookCommand("user-prompt-submit")
|
|
159
266
|
};
|
|
160
267
|
if (options?.includePreCompact !== false) {
|
|
161
|
-
result.preCompact = "
|
|
268
|
+
result.preCompact = getHookCommand("pre-compact");
|
|
162
269
|
}
|
|
163
270
|
if (options?.includeMediaAware !== false) {
|
|
164
|
-
result.mediaAware = "
|
|
271
|
+
result.mediaAware = getHookCommand("media-aware");
|
|
165
272
|
}
|
|
166
273
|
if (options?.includeAutoRules !== false) {
|
|
167
|
-
result.autoRules = "
|
|
274
|
+
result.autoRules = getHookCommand("auto-rules");
|
|
168
275
|
}
|
|
169
276
|
return result;
|
|
170
277
|
}
|
|
@@ -200,20 +307,20 @@ function mergeHooksIntoSettings(existingSettings, newHooks) {
|
|
|
200
307
|
async function installClaudeCodeHooks(options) {
|
|
201
308
|
const result = { scripts: [], settings: [] };
|
|
202
309
|
result.scripts.push(
|
|
203
|
-
"
|
|
204
|
-
"
|
|
310
|
+
getHookCommand("pre-tool-use"),
|
|
311
|
+
getHookCommand("user-prompt-submit")
|
|
205
312
|
);
|
|
206
313
|
if (options.includePreCompact !== false) {
|
|
207
|
-
result.scripts.push("
|
|
314
|
+
result.scripts.push(getHookCommand("pre-compact"));
|
|
208
315
|
}
|
|
209
316
|
if (options.includeMediaAware !== false) {
|
|
210
|
-
result.scripts.push("
|
|
317
|
+
result.scripts.push(getHookCommand("media-aware"));
|
|
211
318
|
}
|
|
212
319
|
if (options.includePostWrite !== false) {
|
|
213
|
-
result.scripts.push("
|
|
320
|
+
result.scripts.push(getHookCommand("post-write"));
|
|
214
321
|
}
|
|
215
322
|
if (options.includeAutoRules !== false) {
|
|
216
|
-
result.scripts.push("
|
|
323
|
+
result.scripts.push(getHookCommand("auto-rules"));
|
|
217
324
|
}
|
|
218
325
|
const hooksConfig = buildHooksConfig({
|
|
219
326
|
includePreCompact: options.includePreCompact,
|
|
@@ -491,6 +598,8 @@ async function installCursorHookScripts(options) {
|
|
|
491
598
|
};
|
|
492
599
|
const filteredPreToolUse = filterContextStreamHooks(existingConfig.hooks.preToolUse);
|
|
493
600
|
const filteredBeforeSubmit = filterContextStreamHooks(existingConfig.hooks.beforeSubmitPrompt);
|
|
601
|
+
const preToolUseCommand = getHookCommand("pre-tool-use");
|
|
602
|
+
const userPromptCommand = getHookCommand("user-prompt-submit");
|
|
494
603
|
const config = {
|
|
495
604
|
version: 1,
|
|
496
605
|
hooks: {
|
|
@@ -498,7 +607,7 @@ async function installCursorHookScripts(options) {
|
|
|
498
607
|
preToolUse: [
|
|
499
608
|
...filteredPreToolUse,
|
|
500
609
|
{
|
|
501
|
-
command:
|
|
610
|
+
command: preToolUseCommand,
|
|
502
611
|
type: "command",
|
|
503
612
|
timeout: 5,
|
|
504
613
|
matcher: { tool_name: "Glob|Grep|search_files|list_files|ripgrep" }
|
|
@@ -507,7 +616,7 @@ async function installCursorHookScripts(options) {
|
|
|
507
616
|
beforeSubmitPrompt: [
|
|
508
617
|
...filteredBeforeSubmit,
|
|
509
618
|
{
|
|
510
|
-
command:
|
|
619
|
+
command: userPromptCommand,
|
|
511
620
|
type: "command",
|
|
512
621
|
timeout: 5
|
|
513
622
|
}
|
|
@@ -517,8 +626,8 @@ async function installCursorHookScripts(options) {
|
|
|
517
626
|
await writeCursorHooksConfig(config, options.scope, options.projectPath);
|
|
518
627
|
const configPath = getCursorHooksConfigPath(options.scope, options.projectPath);
|
|
519
628
|
return {
|
|
520
|
-
preToolUse:
|
|
521
|
-
beforeSubmitPrompt:
|
|
629
|
+
preToolUse: preToolUseCommand,
|
|
630
|
+
beforeSubmitPrompt: userPromptCommand,
|
|
522
631
|
config: configPath
|
|
523
632
|
};
|
|
524
633
|
}
|
|
@@ -1327,11 +1436,13 @@ esac
|
|
|
1327
1436
|
|
|
1328
1437
|
exit 0
|
|
1329
1438
|
`;
|
|
1330
|
-
CLINE_HOOK_WRAPPER = (hookName) =>
|
|
1439
|
+
CLINE_HOOK_WRAPPER = (hookName) => {
|
|
1440
|
+
const command = getHookCommand(hookName);
|
|
1441
|
+
return `#!/bin/bash
|
|
1331
1442
|
# ContextStream ${hookName} Hook Wrapper for Cline/Roo/Kilo Code
|
|
1332
|
-
|
|
1333
|
-
exec npx @contextstream/mcp-server hook ${hookName}
|
|
1443
|
+
exec ${command}
|
|
1334
1444
|
`;
|
|
1445
|
+
};
|
|
1335
1446
|
CURSOR_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
1336
1447
|
"""
|
|
1337
1448
|
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
|
+
};
|