@contextstream/mcp-server 0.4.63 → 0.4.65
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/README.md +43 -1
- package/dist/hooks/auto-rules.js +103 -24
- package/dist/hooks/notification.js +162 -0
- package/dist/hooks/permission-request.js +174 -0
- package/dist/hooks/post-tool-use-failure.js +213 -0
- package/dist/hooks/pre-compact.js +1 -4
- package/dist/hooks/pre-tool-use.js +14 -5
- package/dist/hooks/runner.js +2306 -5164
- package/dist/hooks/session-init.js +33 -13
- package/dist/hooks/stop.js +171 -0
- package/dist/hooks/subagent-start.js +195 -0
- package/dist/hooks/subagent-stop.js +301 -0
- package/dist/hooks/task-completed.js +260 -0
- package/dist/hooks/teammate-idle.js +200 -0
- package/dist/hooks/user-prompt-submit.js +6 -4
- package/dist/index.js +3420 -1650
- package/dist/test-server.js +4 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ npx @contextstream/mcp-server@latest setup
|
|
|
84
84
|
|
|
85
85
|
The wizard handles everything: authentication, configuration, editor integration, and optional hooks that supercharge your workflow.
|
|
86
86
|
|
|
87
|
-
**Works with:** Claude Code • Cursor • VS Code • Claude Desktop • Codex CLI • Antigravity
|
|
87
|
+
**Works with:** Claude Code • Cursor • VS Code • Claude Desktop • Codex CLI • OpenCode • Antigravity
|
|
88
88
|
|
|
89
89
|
---
|
|
90
90
|
|
|
@@ -139,6 +139,48 @@ claude mcp update contextstream -e CONTEXTSTREAM_API_KEY=your_key
|
|
|
139
139
|
|
|
140
140
|
</details>
|
|
141
141
|
|
|
142
|
+
<details>
|
|
143
|
+
<summary><b>OpenCode</b></summary>
|
|
144
|
+
|
|
145
|
+
Local server:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"$schema": "https://opencode.ai/config.json",
|
|
150
|
+
"mcp": {
|
|
151
|
+
"contextstream": {
|
|
152
|
+
"type": "local",
|
|
153
|
+
"command": ["npx", "-y", "contextstream-mcp"],
|
|
154
|
+
"environment": {
|
|
155
|
+
"CONTEXTSTREAM_API_KEY": "{env:CONTEXTSTREAM_API_KEY}"
|
|
156
|
+
},
|
|
157
|
+
"enabled": true
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Remote server:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"$schema": "https://opencode.ai/config.json",
|
|
168
|
+
"mcp": {
|
|
169
|
+
"contextstream": {
|
|
170
|
+
"type": "remote",
|
|
171
|
+
"url": "https://mcp.contextstream.com",
|
|
172
|
+
"enabled": true
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
For the local variant, export `CONTEXTSTREAM_API_KEY` before launching OpenCode.
|
|
179
|
+
|
|
180
|
+
**Locations:** `./opencode.json` • `~/.config/opencode/opencode.json`
|
|
181
|
+
|
|
182
|
+
</details>
|
|
183
|
+
|
|
142
184
|
<details>
|
|
143
185
|
<summary><b>VS Code</b></summary>
|
|
144
186
|
|
package/dist/hooks/auto-rules.js
CHANGED
|
@@ -994,6 +994,7 @@ function detectLanguage(filePath) {
|
|
|
994
994
|
swift: "swift",
|
|
995
995
|
scala: "scala",
|
|
996
996
|
sql: "sql",
|
|
997
|
+
dart: "dart",
|
|
997
998
|
md: "markdown",
|
|
998
999
|
json: "json",
|
|
999
1000
|
yaml: "yaml",
|
|
@@ -1105,7 +1106,9 @@ var init_files = __esm({
|
|
|
1105
1106
|
// Other
|
|
1106
1107
|
"graphql",
|
|
1107
1108
|
"proto",
|
|
1108
|
-
"dockerfile"
|
|
1109
|
+
"dockerfile",
|
|
1110
|
+
// Dart/Flutter
|
|
1111
|
+
"dart"
|
|
1109
1112
|
]);
|
|
1110
1113
|
IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
1111
1114
|
"node_modules",
|
|
@@ -1264,7 +1267,7 @@ function buildHooksConfig(options) {
|
|
|
1264
1267
|
]
|
|
1265
1268
|
});
|
|
1266
1269
|
}
|
|
1267
|
-
if (options?.includeMediaAware
|
|
1270
|
+
if (options?.includeMediaAware === true) {
|
|
1268
1271
|
userPromptHooks.push({
|
|
1269
1272
|
matcher: "*",
|
|
1270
1273
|
hooks: [
|
|
@@ -1279,7 +1282,7 @@ function buildHooksConfig(options) {
|
|
|
1279
1282
|
const config = {
|
|
1280
1283
|
PreToolUse: [
|
|
1281
1284
|
{
|
|
1282
|
-
matcher: "
|
|
1285
|
+
matcher: "*",
|
|
1283
1286
|
hooks: [
|
|
1284
1287
|
{
|
|
1285
1288
|
type: "command",
|
|
@@ -1308,12 +1311,12 @@ function buildHooksConfig(options) {
|
|
|
1308
1311
|
if (options?.includeSessionInit !== false) {
|
|
1309
1312
|
config.SessionStart = [
|
|
1310
1313
|
{
|
|
1311
|
-
matcher: "
|
|
1314
|
+
matcher: "startup|resume|compact",
|
|
1312
1315
|
hooks: [
|
|
1313
1316
|
{
|
|
1314
1317
|
type: "command",
|
|
1315
|
-
command: getHookCommand("session-
|
|
1316
|
-
timeout:
|
|
1318
|
+
command: getHookCommand("session-start"),
|
|
1319
|
+
timeout: 15
|
|
1317
1320
|
}
|
|
1318
1321
|
]
|
|
1319
1322
|
}
|
|
@@ -1321,6 +1324,18 @@ function buildHooksConfig(options) {
|
|
|
1321
1324
|
}
|
|
1322
1325
|
if (options?.includeSessionEnd !== false) {
|
|
1323
1326
|
config.Stop = [
|
|
1327
|
+
{
|
|
1328
|
+
matcher: "*",
|
|
1329
|
+
hooks: [
|
|
1330
|
+
{
|
|
1331
|
+
type: "command",
|
|
1332
|
+
command: getHookCommand("stop"),
|
|
1333
|
+
timeout: 15
|
|
1334
|
+
}
|
|
1335
|
+
]
|
|
1336
|
+
}
|
|
1337
|
+
];
|
|
1338
|
+
config.SessionEnd = [
|
|
1324
1339
|
{
|
|
1325
1340
|
matcher: "*",
|
|
1326
1341
|
hooks: [
|
|
@@ -1346,7 +1361,7 @@ function buildHooksConfig(options) {
|
|
|
1346
1361
|
]
|
|
1347
1362
|
});
|
|
1348
1363
|
}
|
|
1349
|
-
if (options?.includeAutoRules
|
|
1364
|
+
if (options?.includeAutoRules === true) {
|
|
1350
1365
|
postToolUseHooks.push({
|
|
1351
1366
|
matcher: "mcp__contextstream__init|mcp__contextstream__context",
|
|
1352
1367
|
hooks: [
|
|
@@ -1358,7 +1373,7 @@ function buildHooksConfig(options) {
|
|
|
1358
1373
|
]
|
|
1359
1374
|
});
|
|
1360
1375
|
}
|
|
1361
|
-
if (options?.includeOnBash
|
|
1376
|
+
if (options?.includeOnBash === true) {
|
|
1362
1377
|
postToolUseHooks.push({
|
|
1363
1378
|
matcher: "Bash",
|
|
1364
1379
|
hooks: [
|
|
@@ -1370,7 +1385,7 @@ function buildHooksConfig(options) {
|
|
|
1370
1385
|
]
|
|
1371
1386
|
});
|
|
1372
1387
|
}
|
|
1373
|
-
if (options?.includeOnTask
|
|
1388
|
+
if (options?.includeOnTask === true) {
|
|
1374
1389
|
postToolUseHooks.push({
|
|
1375
1390
|
matcher: "Task",
|
|
1376
1391
|
hooks: [
|
|
@@ -1382,7 +1397,7 @@ function buildHooksConfig(options) {
|
|
|
1382
1397
|
]
|
|
1383
1398
|
});
|
|
1384
1399
|
}
|
|
1385
|
-
if (options?.includeOnRead
|
|
1400
|
+
if (options?.includeOnRead === true) {
|
|
1386
1401
|
postToolUseHooks.push({
|
|
1387
1402
|
matcher: "Read|Glob|Grep",
|
|
1388
1403
|
hooks: [
|
|
@@ -1394,7 +1409,7 @@ function buildHooksConfig(options) {
|
|
|
1394
1409
|
]
|
|
1395
1410
|
});
|
|
1396
1411
|
}
|
|
1397
|
-
if (options?.includeOnWeb
|
|
1412
|
+
if (options?.includeOnWeb === true) {
|
|
1398
1413
|
postToolUseHooks.push({
|
|
1399
1414
|
matcher: "WebFetch|WebSearch",
|
|
1400
1415
|
hooks: [
|
|
@@ -1409,6 +1424,48 @@ function buildHooksConfig(options) {
|
|
|
1409
1424
|
if (postToolUseHooks.length > 0) {
|
|
1410
1425
|
config.PostToolUse = postToolUseHooks;
|
|
1411
1426
|
}
|
|
1427
|
+
config.PostToolUseFailure = [
|
|
1428
|
+
{
|
|
1429
|
+
matcher: "*",
|
|
1430
|
+
hooks: [{ type: "command", command: getHookCommand("post-tool-use-failure"), timeout: 10 }]
|
|
1431
|
+
}
|
|
1432
|
+
];
|
|
1433
|
+
config.SubagentStart = [
|
|
1434
|
+
{
|
|
1435
|
+
matcher: "Explore|Plan|general-purpose|custom",
|
|
1436
|
+
hooks: [{ type: "command", command: getHookCommand("subagent-start"), timeout: 10 }]
|
|
1437
|
+
}
|
|
1438
|
+
];
|
|
1439
|
+
config.SubagentStop = [
|
|
1440
|
+
{
|
|
1441
|
+
matcher: "Plan",
|
|
1442
|
+
hooks: [{ type: "command", command: getHookCommand("subagent-stop"), timeout: 15 }]
|
|
1443
|
+
}
|
|
1444
|
+
];
|
|
1445
|
+
config.TaskCompleted = [
|
|
1446
|
+
{
|
|
1447
|
+
matcher: "*",
|
|
1448
|
+
hooks: [{ type: "command", command: getHookCommand("task-completed"), timeout: 10 }]
|
|
1449
|
+
}
|
|
1450
|
+
];
|
|
1451
|
+
config.TeammateIdle = [
|
|
1452
|
+
{
|
|
1453
|
+
matcher: "*",
|
|
1454
|
+
hooks: [{ type: "command", command: getHookCommand("teammate-idle"), timeout: 10 }]
|
|
1455
|
+
}
|
|
1456
|
+
];
|
|
1457
|
+
config.Notification = [
|
|
1458
|
+
{
|
|
1459
|
+
matcher: "*",
|
|
1460
|
+
hooks: [{ type: "command", command: getHookCommand("notification"), timeout: 10 }]
|
|
1461
|
+
}
|
|
1462
|
+
];
|
|
1463
|
+
config.PermissionRequest = [
|
|
1464
|
+
{
|
|
1465
|
+
matcher: "*",
|
|
1466
|
+
hooks: [{ type: "command", command: getHookCommand("permission-request"), timeout: 10 }]
|
|
1467
|
+
}
|
|
1468
|
+
];
|
|
1412
1469
|
return config;
|
|
1413
1470
|
}
|
|
1414
1471
|
async function installHookScripts(options) {
|
|
@@ -1462,18 +1519,29 @@ async function installClaudeCodeHooks(options) {
|
|
|
1462
1519
|
const result = { scripts: [], settings: [] };
|
|
1463
1520
|
result.scripts.push(
|
|
1464
1521
|
getHookCommand("pre-tool-use"),
|
|
1465
|
-
getHookCommand("user-prompt-submit")
|
|
1522
|
+
getHookCommand("user-prompt-submit"),
|
|
1523
|
+
getHookCommand("on-save-intent"),
|
|
1524
|
+
getHookCommand("session-start"),
|
|
1525
|
+
getHookCommand("stop"),
|
|
1526
|
+
getHookCommand("session-end"),
|
|
1527
|
+
getHookCommand("post-tool-use-failure"),
|
|
1528
|
+
getHookCommand("subagent-start"),
|
|
1529
|
+
getHookCommand("subagent-stop"),
|
|
1530
|
+
getHookCommand("task-completed"),
|
|
1531
|
+
getHookCommand("teammate-idle"),
|
|
1532
|
+
getHookCommand("notification"),
|
|
1533
|
+
getHookCommand("permission-request")
|
|
1466
1534
|
);
|
|
1467
1535
|
if (options.includePreCompact !== false) {
|
|
1468
1536
|
result.scripts.push(getHookCommand("pre-compact"));
|
|
1469
1537
|
}
|
|
1470
|
-
if (options.includeMediaAware
|
|
1538
|
+
if (options.includeMediaAware === true) {
|
|
1471
1539
|
result.scripts.push(getHookCommand("media-aware"));
|
|
1472
1540
|
}
|
|
1473
1541
|
if (options.includePostWrite !== false) {
|
|
1474
1542
|
result.scripts.push(getHookCommand("post-write"));
|
|
1475
1543
|
}
|
|
1476
|
-
if (options.includeAutoRules
|
|
1544
|
+
if (options.includeAutoRules === true) {
|
|
1477
1545
|
result.scripts.push(getHookCommand("auto-rules"));
|
|
1478
1546
|
}
|
|
1479
1547
|
const hooksConfig = buildHooksConfig({
|
|
@@ -1511,8 +1579,8 @@ All hooks run via Node.js - no Python dependency required.
|
|
|
1511
1579
|
|
|
1512
1580
|
### PreToolUse Hook
|
|
1513
1581
|
- **Command:** \`npx @contextstream/mcp-server hook pre-tool-use\`
|
|
1514
|
-
- **Purpose:** Blocks Glob/Grep/Search/EnterPlanMode and redirects to ContextStream
|
|
1515
|
-
- **Blocked tools:** Glob, Grep, Search, Task(Explore), Task(Plan), EnterPlanMode
|
|
1582
|
+
- **Purpose:** Blocks Glob/Grep/Search/Explore/Task/EnterPlanMode and redirects to ContextStream
|
|
1583
|
+
- **Blocked tools:** Glob, Grep, Search, Explore, Task(Explore), Task(Plan), EnterPlanMode
|
|
1516
1584
|
- **Disable:** Set \`CONTEXTSTREAM_HOOK_ENABLED=false\` environment variable
|
|
1517
1585
|
|
|
1518
1586
|
### UserPromptSubmit Hook
|
|
@@ -1565,7 +1633,7 @@ If you prefer to configure manually, add to \`~/.claude/settings.json\`:
|
|
|
1565
1633
|
{
|
|
1566
1634
|
"hooks": {
|
|
1567
1635
|
"PreToolUse": [{
|
|
1568
|
-
"matcher": "Glob|Grep|Search|Task|EnterPlanMode",
|
|
1636
|
+
"matcher": "Glob|Grep|Search|Explore|Task|EnterPlanMode",
|
|
1569
1637
|
"hooks": [{"type": "command", "command": "npx @contextstream/mcp-server hook pre-tool-use"}]
|
|
1570
1638
|
}],
|
|
1571
1639
|
"UserPromptSubmit": [
|
|
@@ -1790,6 +1858,7 @@ async function installCursorHookScripts(options) {
|
|
|
1790
1858
|
const filteredBeforeSubmit = filterContextStreamHooks(existingConfig.hooks.beforeSubmitPrompt);
|
|
1791
1859
|
const preToolUseCommand = getHookCommand("pre-tool-use");
|
|
1792
1860
|
const userPromptCommand = getHookCommand("user-prompt-submit");
|
|
1861
|
+
const saveIntentCommand = getHookCommand("on-save-intent");
|
|
1793
1862
|
const config = {
|
|
1794
1863
|
version: 1,
|
|
1795
1864
|
hooks: {
|
|
@@ -1799,8 +1868,7 @@ async function installCursorHookScripts(options) {
|
|
|
1799
1868
|
{
|
|
1800
1869
|
command: preToolUseCommand,
|
|
1801
1870
|
type: "command",
|
|
1802
|
-
timeout: 5
|
|
1803
|
-
matcher: { tool_name: "Glob|Grep|search_files|list_files|ripgrep" }
|
|
1871
|
+
timeout: 5
|
|
1804
1872
|
}
|
|
1805
1873
|
],
|
|
1806
1874
|
beforeSubmitPrompt: [
|
|
@@ -1809,6 +1877,11 @@ async function installCursorHookScripts(options) {
|
|
|
1809
1877
|
command: userPromptCommand,
|
|
1810
1878
|
type: "command",
|
|
1811
1879
|
timeout: 5
|
|
1880
|
+
},
|
|
1881
|
+
{
|
|
1882
|
+
command: saveIntentCommand,
|
|
1883
|
+
type: "command",
|
|
1884
|
+
timeout: 5
|
|
1812
1885
|
}
|
|
1813
1886
|
]
|
|
1814
1887
|
}
|
|
@@ -1817,7 +1890,7 @@ async function installCursorHookScripts(options) {
|
|
|
1817
1890
|
const configPath = getCursorHooksConfigPath(options.scope, options.projectPath);
|
|
1818
1891
|
return {
|
|
1819
1892
|
preToolUse: preToolUseCommand,
|
|
1820
|
-
beforeSubmitPrompt: userPromptCommand,
|
|
1893
|
+
beforeSubmitPrompt: `${userPromptCommand}, ${saveIntentCommand}`,
|
|
1821
1894
|
config: configPath
|
|
1822
1895
|
};
|
|
1823
1896
|
}
|
|
@@ -1973,7 +2046,7 @@ var init_hooks_config = __esm({
|
|
|
1973
2046
|
PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
1974
2047
|
"""
|
|
1975
2048
|
ContextStream PreToolUse Hook for Claude Code
|
|
1976
|
-
Blocks Grep/Glob/Search/Task(Explore)/EnterPlanMode and redirects to ContextStream.
|
|
2049
|
+
Blocks Grep/Glob/Search/Explore/Task(Explore|Plan)/EnterPlanMode and redirects to ContextStream.
|
|
1977
2050
|
|
|
1978
2051
|
Only blocks if the current project is indexed in ContextStream.
|
|
1979
2052
|
If not indexed, allows local tools through with a suggestion to index.
|
|
@@ -2092,12 +2165,18 @@ def main():
|
|
|
2092
2165
|
print(f"STOP: Use mcp__contextstream__search(mode=\\"auto\\", query=\\"{pattern}\\") instead of {tool}.", file=sys.stderr)
|
|
2093
2166
|
sys.exit(2)
|
|
2094
2167
|
|
|
2168
|
+
elif tool == "Explore":
|
|
2169
|
+
print("STOP: Use mcp__contextstream__search(mode=\\"auto\\", output_format=\\"paths\\") instead of Explore.", file=sys.stderr)
|
|
2170
|
+
sys.exit(2)
|
|
2171
|
+
|
|
2095
2172
|
elif tool == "Task":
|
|
2096
|
-
|
|
2173
|
+
subagent = inp.get("subagent_type", "") or inp.get("subagentType", "")
|
|
2174
|
+
subagent = subagent.lower()
|
|
2175
|
+
if "explore" in subagent:
|
|
2097
2176
|
print("STOP: Use mcp__contextstream__search(mode=\\"auto\\") instead of Task(Explore).", file=sys.stderr)
|
|
2098
2177
|
sys.exit(2)
|
|
2099
|
-
if
|
|
2100
|
-
print("STOP:
|
|
2178
|
+
if "plan" in subagent:
|
|
2179
|
+
print("STOP: For planning, use mcp__contextstream__search(mode=\\"auto\\", output_format=\\"paths\\") for discovery and mcp__contextstream__session(action=\\"capture_plan\\") for persistence.", file=sys.stderr)
|
|
2101
2180
|
sys.exit(2)
|
|
2102
2181
|
|
|
2103
2182
|
elif tool == "EnterPlanMode":
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/common.ts
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
var DEFAULT_API_URL = "https://api.contextstream.io";
|
|
8
|
+
var HOOK_SPECIFIC_OUTPUT_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit", "PostToolUse"]);
|
|
9
|
+
function readHookInput() {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function writeHookOutput(output) {
|
|
17
|
+
const eventName = output?.hookEventName || (typeof process.env.HOOK_EVENT_NAME === "string" ? process.env.HOOK_EVENT_NAME.trim() : "");
|
|
18
|
+
const canUseHookSpecificOutput = HOOK_SPECIFIC_OUTPUT_EVENTS.has(eventName);
|
|
19
|
+
const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
|
|
20
|
+
hookSpecificOutput: output.additionalContext && canUseHookSpecificOutput ? {
|
|
21
|
+
hookEventName: eventName,
|
|
22
|
+
additionalContext: output.additionalContext
|
|
23
|
+
} : void 0,
|
|
24
|
+
additionalContext: output.additionalContext,
|
|
25
|
+
blocked: output.blocked,
|
|
26
|
+
reason: output.reason
|
|
27
|
+
} : {};
|
|
28
|
+
console.log(JSON.stringify(payload));
|
|
29
|
+
}
|
|
30
|
+
function extractCwd(input) {
|
|
31
|
+
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
|
|
32
|
+
return cwd;
|
|
33
|
+
}
|
|
34
|
+
function loadHookConfig(cwd) {
|
|
35
|
+
let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
|
|
36
|
+
let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
37
|
+
let jwt = process.env.CONTEXTSTREAM_JWT || "";
|
|
38
|
+
let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
|
|
39
|
+
let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
|
|
40
|
+
let searchDir = path.resolve(cwd);
|
|
41
|
+
for (let i = 0; i < 6; i++) {
|
|
42
|
+
if (!apiKey && !jwt) {
|
|
43
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
44
|
+
if (fs.existsSync(mcpPath)) {
|
|
45
|
+
try {
|
|
46
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
47
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
48
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
49
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
50
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
51
|
+
if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
|
|
52
|
+
if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!workspaceId || !projectId) {
|
|
58
|
+
const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
59
|
+
if (fs.existsSync(localConfigPath)) {
|
|
60
|
+
try {
|
|
61
|
+
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
|
|
62
|
+
if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
|
|
63
|
+
if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const parentDir = path.dirname(searchDir);
|
|
69
|
+
if (parentDir === searchDir) break;
|
|
70
|
+
searchDir = parentDir;
|
|
71
|
+
}
|
|
72
|
+
if (!apiKey && !jwt) {
|
|
73
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
74
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
|
|
77
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
78
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
79
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
80
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { apiUrl, apiKey, jwt, workspaceId, projectId };
|
|
86
|
+
}
|
|
87
|
+
function isConfigured(config) {
|
|
88
|
+
return Boolean(config.apiKey || config.jwt);
|
|
89
|
+
}
|
|
90
|
+
function authHeaders(config) {
|
|
91
|
+
if (config.apiKey) {
|
|
92
|
+
return { "X-API-Key": config.apiKey };
|
|
93
|
+
}
|
|
94
|
+
if (config.jwt) {
|
|
95
|
+
return { Authorization: `Bearer ${config.jwt}` };
|
|
96
|
+
}
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
async function apiRequest(config, apiPath, init = {}) {
|
|
100
|
+
const response = await fetch(`${config.apiUrl}${apiPath}`, {
|
|
101
|
+
method: init.method || "GET",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
...authHeaders(config)
|
|
105
|
+
},
|
|
106
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
110
|
+
}
|
|
111
|
+
const json = await response.json();
|
|
112
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
113
|
+
return json.data;
|
|
114
|
+
}
|
|
115
|
+
return json;
|
|
116
|
+
}
|
|
117
|
+
async function postMemoryEvent(config, title, content, tags, eventType = "operation") {
|
|
118
|
+
if (!isConfigured(config) || !config.workspaceId) return;
|
|
119
|
+
await apiRequest(config, "/memory/events", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: {
|
|
122
|
+
workspace_id: config.workspaceId,
|
|
123
|
+
project_id: config.projectId || void 0,
|
|
124
|
+
event_type: eventType,
|
|
125
|
+
title,
|
|
126
|
+
content: typeof content === "string" ? content : JSON.stringify(content),
|
|
127
|
+
metadata: {
|
|
128
|
+
tags,
|
|
129
|
+
source: "mcp_hook",
|
|
130
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/hooks/notification.ts
|
|
137
|
+
async function runNotificationHook() {
|
|
138
|
+
const input = readHookInput();
|
|
139
|
+
const cwd = extractCwd(input);
|
|
140
|
+
const config = loadHookConfig(cwd);
|
|
141
|
+
if (isConfigured(config)) {
|
|
142
|
+
const message = typeof input.title === "string" && input.title || typeof input.message === "string" && input.message || "Notification event";
|
|
143
|
+
await postMemoryEvent(
|
|
144
|
+
config,
|
|
145
|
+
`Notification: ${message}`,
|
|
146
|
+
{
|
|
147
|
+
notification: input,
|
|
148
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
149
|
+
},
|
|
150
|
+
["hook", "notification"]
|
|
151
|
+
).catch(() => {
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
writeHookOutput();
|
|
155
|
+
}
|
|
156
|
+
var isDirectRun = process.argv[1]?.includes("notification") || process.argv[2] === "notification";
|
|
157
|
+
if (isDirectRun) {
|
|
158
|
+
runNotificationHook().catch(() => process.exit(0));
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
runNotificationHook
|
|
162
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/common.ts
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
var DEFAULT_API_URL = "https://api.contextstream.io";
|
|
8
|
+
var HOOK_SPECIFIC_OUTPUT_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit", "PostToolUse"]);
|
|
9
|
+
function readHookInput() {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function writeHookOutput(output) {
|
|
17
|
+
const eventName = output?.hookEventName || (typeof process.env.HOOK_EVENT_NAME === "string" ? process.env.HOOK_EVENT_NAME.trim() : "");
|
|
18
|
+
const canUseHookSpecificOutput = HOOK_SPECIFIC_OUTPUT_EVENTS.has(eventName);
|
|
19
|
+
const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
|
|
20
|
+
hookSpecificOutput: output.additionalContext && canUseHookSpecificOutput ? {
|
|
21
|
+
hookEventName: eventName,
|
|
22
|
+
additionalContext: output.additionalContext
|
|
23
|
+
} : void 0,
|
|
24
|
+
additionalContext: output.additionalContext,
|
|
25
|
+
blocked: output.blocked,
|
|
26
|
+
reason: output.reason
|
|
27
|
+
} : {};
|
|
28
|
+
console.log(JSON.stringify(payload));
|
|
29
|
+
}
|
|
30
|
+
function extractCwd(input) {
|
|
31
|
+
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
|
|
32
|
+
return cwd;
|
|
33
|
+
}
|
|
34
|
+
function loadHookConfig(cwd) {
|
|
35
|
+
let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
|
|
36
|
+
let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
37
|
+
let jwt = process.env.CONTEXTSTREAM_JWT || "";
|
|
38
|
+
let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
|
|
39
|
+
let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
|
|
40
|
+
let searchDir = path.resolve(cwd);
|
|
41
|
+
for (let i = 0; i < 6; i++) {
|
|
42
|
+
if (!apiKey && !jwt) {
|
|
43
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
44
|
+
if (fs.existsSync(mcpPath)) {
|
|
45
|
+
try {
|
|
46
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
47
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
48
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
49
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
50
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
51
|
+
if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
|
|
52
|
+
if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!workspaceId || !projectId) {
|
|
58
|
+
const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
59
|
+
if (fs.existsSync(localConfigPath)) {
|
|
60
|
+
try {
|
|
61
|
+
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
|
|
62
|
+
if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
|
|
63
|
+
if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const parentDir = path.dirname(searchDir);
|
|
69
|
+
if (parentDir === searchDir) break;
|
|
70
|
+
searchDir = parentDir;
|
|
71
|
+
}
|
|
72
|
+
if (!apiKey && !jwt) {
|
|
73
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
74
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
|
|
77
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
78
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
79
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
80
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { apiUrl, apiKey, jwt, workspaceId, projectId };
|
|
86
|
+
}
|
|
87
|
+
function isConfigured(config) {
|
|
88
|
+
return Boolean(config.apiKey || config.jwt);
|
|
89
|
+
}
|
|
90
|
+
function authHeaders(config) {
|
|
91
|
+
if (config.apiKey) {
|
|
92
|
+
return { "X-API-Key": config.apiKey };
|
|
93
|
+
}
|
|
94
|
+
if (config.jwt) {
|
|
95
|
+
return { Authorization: `Bearer ${config.jwt}` };
|
|
96
|
+
}
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
async function apiRequest(config, apiPath, init = {}) {
|
|
100
|
+
const response = await fetch(`${config.apiUrl}${apiPath}`, {
|
|
101
|
+
method: init.method || "GET",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
...authHeaders(config)
|
|
105
|
+
},
|
|
106
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
110
|
+
}
|
|
111
|
+
const json = await response.json();
|
|
112
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
113
|
+
return json.data;
|
|
114
|
+
}
|
|
115
|
+
return json;
|
|
116
|
+
}
|
|
117
|
+
async function postMemoryEvent(config, title, content, tags, eventType = "operation") {
|
|
118
|
+
if (!isConfigured(config) || !config.workspaceId) return;
|
|
119
|
+
await apiRequest(config, "/memory/events", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: {
|
|
122
|
+
workspace_id: config.workspaceId,
|
|
123
|
+
project_id: config.projectId || void 0,
|
|
124
|
+
event_type: eventType,
|
|
125
|
+
title,
|
|
126
|
+
content: typeof content === "string" ? content : JSON.stringify(content),
|
|
127
|
+
metadata: {
|
|
128
|
+
tags,
|
|
129
|
+
source: "mcp_hook",
|
|
130
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/hooks/permission-request.ts
|
|
137
|
+
function isHighRiskCommand(command) {
|
|
138
|
+
const lower = command.toLowerCase();
|
|
139
|
+
return ["rm -rf", "git reset --hard", "mkfs", "dd if=", "shutdown", "reboot"].some(
|
|
140
|
+
(pattern) => lower.includes(pattern)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
async function runPermissionRequestHook() {
|
|
144
|
+
const input = readHookInput();
|
|
145
|
+
const cwd = extractCwd(input);
|
|
146
|
+
const config = loadHookConfig(cwd);
|
|
147
|
+
const command = typeof input.command === "string" && input.command || typeof input.cmd === "string" && input.cmd || "";
|
|
148
|
+
if (isConfigured(config)) {
|
|
149
|
+
await postMemoryEvent(
|
|
150
|
+
config,
|
|
151
|
+
"Permission request",
|
|
152
|
+
{
|
|
153
|
+
request: input,
|
|
154
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
155
|
+
},
|
|
156
|
+
["hook", "permission_request"]
|
|
157
|
+
).catch(() => {
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (isHighRiskCommand(command)) {
|
|
161
|
+
writeHookOutput({
|
|
162
|
+
additionalContext: "High-risk command detected. Confirm scope and prefer least-privilege execution."
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
writeHookOutput();
|
|
167
|
+
}
|
|
168
|
+
var isDirectRun = process.argv[1]?.includes("permission-request") || process.argv[2] === "permission-request";
|
|
169
|
+
if (isDirectRun) {
|
|
170
|
+
runPermissionRequestHook().catch(() => process.exit(0));
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
runPermissionRequestHook
|
|
174
|
+
};
|