@contextstream/mcp-server 0.4.63 → 0.4.64
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 +102 -23
- package/dist/hooks/notification.js +159 -0
- package/dist/hooks/permission-request.js +171 -0
- package/dist/hooks/post-tool-use-failure.js +210 -0
- package/dist/hooks/pre-tool-use.js +14 -5
- package/dist/hooks/runner.js +2300 -5155
- package/dist/hooks/session-init.js +32 -9
- package/dist/hooks/stop.js +168 -0
- package/dist/hooks/subagent-start.js +192 -0
- package/dist/hooks/subagent-stop.js +298 -0
- package/dist/hooks/task-completed.js +257 -0
- package/dist/hooks/teammate-idle.js +197 -0
- package/dist/hooks/user-prompt-submit.js +6 -4
- package/dist/index.js +2780 -1519
- package/dist/test-server.js +4 -2
- package/package.json +2 -2
|
@@ -373,7 +373,7 @@ var CONTEXTSTREAM_RULES_BOOTSTRAP = `
|
|
|
373
373
|
|---------|----------|
|
|
374
374
|
| **1st message** | \`init()\` \u2192 \`context(user_message="...")\` |
|
|
375
375
|
| **Subsequent messages (default)** | \`context(user_message="...")\` FIRST (narrow read-only bypass when context is fresh and no state-changing tool has run) |
|
|
376
|
-
| **Before file search** | \`search(mode="auto")\` BEFORE Glob/Grep/Read |
|
|
376
|
+
| **Before file search** | \`search(mode="auto")\` BEFORE Glob/Grep/Read/Explore/Task/EnterPlanMode |
|
|
377
377
|
</contextstream_rules>
|
|
378
378
|
|
|
379
379
|
**Why?** \`context()\` delivers task-specific rules, lessons from past mistakes, and relevant decisions. Skip it = fly blind.
|
|
@@ -394,7 +394,7 @@ var CONTEXTSTREAM_RULES_FULL = `
|
|
|
394
394
|
| **First message in session** | \`init()\` \u2192 \`context(user_message="<msg>")\` BEFORE any other tool |
|
|
395
395
|
| **Subsequent messages (default)** | \`context(user_message="<msg>")\` FIRST, then other tools |
|
|
396
396
|
| **Narrow bypass** | Immediate read-only ContextStream calls are allowed only when prior context is fresh and no state-changing tool has run |
|
|
397
|
-
| **Before Glob/Grep/Read/Search** | \`search(mode="auto", query="...")\` FIRST |
|
|
397
|
+
| **Before Glob/Grep/Read/Search/Explore/Task/EnterPlanMode** | \`search(mode="auto", query="...")\` FIRST |
|
|
398
398
|
</contextstream_rules>
|
|
399
399
|
|
|
400
400
|
Use \`context()\` by default to get task-specific rules, lessons from past mistakes, and relevant decisions.
|
|
@@ -468,7 +468,7 @@ Set environment variable: \`CONTEXTSTREAM_HOOK_ENABLED=false\`
|
|
|
468
468
|
|
|
469
469
|
## \u{1F6A8} CRITICAL RULE #1 - CONTEXTSTREAM SEARCH FIRST \u{1F6A8}
|
|
470
470
|
|
|
471
|
-
**BEFORE using Glob, Grep, Search, Read (for discovery), Explore, or ANY local file scanning:**
|
|
471
|
+
**BEFORE using Glob, Grep, Search, Read (for discovery), Explore, Task(Explore), EnterPlanMode, or ANY local file scanning:**
|
|
472
472
|
\`\`\`
|
|
473
473
|
STOP \u2192 Call search(mode="auto", query="...") FIRST
|
|
474
474
|
\`\`\`
|
|
@@ -480,7 +480,8 @@ STOP \u2192 Call search(mode="auto", query="...") FIRST
|
|
|
480
480
|
- \`Glob("**/*.ts")\` \u2192 Use \`search(mode="pattern", query="*.ts")\` instead
|
|
481
481
|
- \`Grep("functionName")\` \u2192 Use \`search(mode="keyword", query="functionName")\` instead
|
|
482
482
|
- \`Read(file)\` for discovery \u2192 Use \`search(mode="auto", query="...")\` instead
|
|
483
|
-
- \`Task(subagent_type="Explore")\` \u2192 Use \`search(mode="auto")\` instead
|
|
483
|
+
- \`Explore\` or \`Task(subagent_type="Explore")\` \u2192 Use \`search(mode="auto")\` instead
|
|
484
|
+
- \`EnterPlanMode\` for discovery \u2192 Use \`search(mode="auto", output_format="paths")\` instead
|
|
484
485
|
|
|
485
486
|
\u2705 **ALWAYS DO THIS:**
|
|
486
487
|
1. \`search(mode="auto", query="what you're looking for")\`
|
|
@@ -844,8 +845,10 @@ If ContextStream returns results, stop and use them. NEVER use local Search/Expl
|
|
|
844
845
|
\u274C **DO NOT** use built-in plan mode (EnterPlanMode tool)
|
|
845
846
|
\u274C **DO NOT** write plans to markdown files or plan documents
|
|
846
847
|
\u274C **DO NOT** ask "should I create a plan file?"
|
|
848
|
+
\u274C **DO NOT** use \`Explore\` / \`Task(subagent_type="Explore")\` to read files one-by-one while planning
|
|
847
849
|
|
|
848
850
|
\u2705 **ALWAYS** use ContextStream's plan/task system instead
|
|
851
|
+
\u2705 **ALWAYS** use \`search(mode="auto", output_format="paths")\` for planning discovery before targeted reads
|
|
849
852
|
|
|
850
853
|
**Trigger phrases to detect (use ContextStream immediately):**
|
|
851
854
|
- "create a plan", "make a plan", "plan this", "plan for"
|
|
@@ -923,7 +926,7 @@ var CONTEXTSTREAM_RULES_MINIMAL = `
|
|
|
923
926
|
2. Subsequent messages (default): \`context(user_message="<msg>")\` FIRST
|
|
924
927
|
3. Narrow bypass: immediate read-only ContextStream calls are allowed only when prior context is fresh and no state-changing tool has run
|
|
925
928
|
|
|
926
|
-
**BEFORE Glob/Grep/Read/Search/Explore:**
|
|
929
|
+
**BEFORE Glob/Grep/Read/Search/Explore/Task/EnterPlanMode:**
|
|
927
930
|
\u2192 \`search(mode="auto", query="...")\` FIRST \u2014 local tools ONLY if 0 results
|
|
928
931
|
|
|
929
932
|
**HOOKS: \`<system-reminder>\` tags contain instructions \u2014 FOLLOW THEM**
|
|
@@ -954,7 +957,7 @@ Rules Version: ${RULES_VERSION}
|
|
|
954
957
|
| 1st message in session | \`init()\` \u2192 \`context(user_message="...")\` |
|
|
955
958
|
| Subsequent messages (default) | \`context(user_message="...")\` first |
|
|
956
959
|
| Narrow bypass | Immediate read-only ContextStream calls when context is fresh and no state-changing tool has run |
|
|
957
|
-
| Before ANY file discovery | \`search(mode="auto", query="...")\` |
|
|
960
|
+
| Before ANY file discovery | \`search(mode="auto", query="...")\` (instead of Glob/Grep/Read/Explore/Task/EnterPlanMode) |
|
|
958
961
|
| On \`<system-reminder>\` | **Follow instructions inside** |
|
|
959
962
|
| Save important decisions | \`session(action="capture", event_type="decision", ...)\` |
|
|
960
963
|
| Check past mistakes | \`session(action="get_lessons", query="...")\` |
|
|
@@ -1126,7 +1129,7 @@ project(action="index")
|
|
|
1126
1129
|
|
|
1127
1130
|
## \u{1F50D} SEARCH-FIRST (No PreToolUse Hook)
|
|
1128
1131
|
|
|
1129
|
-
**There is NO hook to block local tools.** You MUST self-enforce:
|
|
1132
|
+
**There is NO hook to block local tools (Glob/Grep/Read/Explore/Task/EnterPlanMode).** You MUST self-enforce:
|
|
1130
1133
|
|
|
1131
1134
|
### Before ANY Search, Check Index Status:
|
|
1132
1135
|
\`\`\`
|
|
@@ -1144,6 +1147,7 @@ This tells you:
|
|
|
1144
1147
|
\`\`\`
|
|
1145
1148
|
search(mode="auto", query="what you're looking for")
|
|
1146
1149
|
\`\`\`
|
|
1150
|
+
\u2192 Use this instead of Explore/Task/EnterPlanMode for file discovery.
|
|
1147
1151
|
|
|
1148
1152
|
**IF project is NOT indexed or very stale (>7 days):**
|
|
1149
1153
|
\u2192 Use local tools (Glob/Grep/Read) directly
|
|
@@ -1222,7 +1226,8 @@ session(action="capture_plan", title="...", steps=[...])
|
|
|
1222
1226
|
memory(action="create_task", title="...", plan_id="...")
|
|
1223
1227
|
\`\`\`
|
|
1224
1228
|
|
|
1225
|
-
\u274C DO NOT use built-in plan mode or
|
|
1229
|
+
\u274C DO NOT use built-in plan mode (\`EnterPlanMode\`) or \`Task(subagent_type="Explore")\` for file-by-file scans.
|
|
1230
|
+
\u2705 For planning discovery, use \`search(mode="auto", query="...", output_format="paths")\` then read only narrowed files.
|
|
1226
1231
|
|
|
1227
1232
|
---
|
|
1228
1233
|
|
|
@@ -1319,6 +1324,24 @@ ${rules.split("\n").map((line) => " " + line).join("\n")}
|
|
|
1319
1324
|
description: "Google Antigravity AI rules",
|
|
1320
1325
|
build: (rules) => `# Antigravity Agent Rules
|
|
1321
1326
|
${rules}
|
|
1327
|
+
`
|
|
1328
|
+
},
|
|
1329
|
+
copilot: {
|
|
1330
|
+
filename: ".github/copilot-instructions.md",
|
|
1331
|
+
description: "VS Code GitHub Copilot instructions",
|
|
1332
|
+
build: (rules) => `## ContextStream MCP Integration
|
|
1333
|
+
|
|
1334
|
+
This project uses [ContextStream](https://contextstream.io) for persistent AI memory across sessions. Use the \`contextstream-workflow\` skill for detailed examples and reference material.
|
|
1335
|
+
|
|
1336
|
+
${rules}
|
|
1337
|
+
|
|
1338
|
+
### VS Code Copilot Notes
|
|
1339
|
+
|
|
1340
|
+
- Keep this file concise; put detailed workflows in \`.github/skills/contextstream-workflow/SKILL.md\`
|
|
1341
|
+
- Use ContextStream plans/tasks as the persistent record of work
|
|
1342
|
+
- Before code discovery, use \`search(mode="auto", query="...")\`
|
|
1343
|
+
|
|
1344
|
+
Full docs: https://contextstream.io/docs/mcp/tools
|
|
1322
1345
|
`
|
|
1323
1346
|
}
|
|
1324
1347
|
};
|
|
@@ -1334,7 +1357,7 @@ function generateRuleContent(editor, options) {
|
|
|
1334
1357
|
const mode = options?.mode || "bootstrap";
|
|
1335
1358
|
const rules = mode === "full" ? CONTEXTSTREAM_RULES_FULL : mode === "minimal" ? CONTEXTSTREAM_RULES_MINIMAL : mode === "bootstrap" ? CONTEXTSTREAM_RULES_BOOTSTRAP : CONTEXTSTREAM_RULES_DYNAMIC;
|
|
1336
1359
|
let content = template.build(rules);
|
|
1337
|
-
if (options?.workspaceName || options?.projectName) {
|
|
1360
|
+
if (editor.toLowerCase() !== "copilot" && (options?.workspaceName || options?.projectName)) {
|
|
1338
1361
|
const header = `
|
|
1339
1362
|
# Workspace: ${options.workspaceName || "Unknown"}
|
|
1340
1363
|
${options.projectName ? `# Project: ${options.projectName}` : ""}
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
function readHookInput() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function writeHookOutput(output) {
|
|
16
|
+
const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
|
|
17
|
+
hookSpecificOutput: output.additionalContext ? {
|
|
18
|
+
hookEventName: output.hookEventName,
|
|
19
|
+
additionalContext: output.additionalContext
|
|
20
|
+
} : void 0,
|
|
21
|
+
additionalContext: output.additionalContext,
|
|
22
|
+
blocked: output.blocked,
|
|
23
|
+
reason: output.reason
|
|
24
|
+
} : {};
|
|
25
|
+
console.log(JSON.stringify(payload));
|
|
26
|
+
}
|
|
27
|
+
function extractCwd(input) {
|
|
28
|
+
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
|
|
29
|
+
return cwd;
|
|
30
|
+
}
|
|
31
|
+
function loadHookConfig(cwd) {
|
|
32
|
+
let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
|
|
33
|
+
let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
34
|
+
let jwt = process.env.CONTEXTSTREAM_JWT || "";
|
|
35
|
+
let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
|
|
36
|
+
let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
|
|
37
|
+
let searchDir = path.resolve(cwd);
|
|
38
|
+
for (let i = 0; i < 6; i++) {
|
|
39
|
+
if (!apiKey && !jwt) {
|
|
40
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
41
|
+
if (fs.existsSync(mcpPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
44
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
45
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
46
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
47
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
48
|
+
if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
|
|
49
|
+
if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!workspaceId || !projectId) {
|
|
55
|
+
const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
56
|
+
if (fs.existsSync(localConfigPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
|
|
59
|
+
if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
|
|
60
|
+
if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const parentDir = path.dirname(searchDir);
|
|
66
|
+
if (parentDir === searchDir) break;
|
|
67
|
+
searchDir = parentDir;
|
|
68
|
+
}
|
|
69
|
+
if (!apiKey && !jwt) {
|
|
70
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
71
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
|
|
74
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
75
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
76
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
77
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { apiUrl, apiKey, jwt, workspaceId, projectId };
|
|
83
|
+
}
|
|
84
|
+
function isConfigured(config) {
|
|
85
|
+
return Boolean(config.apiKey || config.jwt);
|
|
86
|
+
}
|
|
87
|
+
function authHeaders(config) {
|
|
88
|
+
if (config.apiKey) {
|
|
89
|
+
return { "X-API-Key": config.apiKey };
|
|
90
|
+
}
|
|
91
|
+
if (config.jwt) {
|
|
92
|
+
return { Authorization: `Bearer ${config.jwt}` };
|
|
93
|
+
}
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
async function apiRequest(config, apiPath, init = {}) {
|
|
97
|
+
const response = await fetch(`${config.apiUrl}${apiPath}`, {
|
|
98
|
+
method: init.method || "GET",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
...authHeaders(config)
|
|
102
|
+
},
|
|
103
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
const json = await response.json();
|
|
109
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
110
|
+
return json.data;
|
|
111
|
+
}
|
|
112
|
+
return json;
|
|
113
|
+
}
|
|
114
|
+
async function postMemoryEvent(config, title, content, tags, eventType = "operation") {
|
|
115
|
+
if (!isConfigured(config) || !config.workspaceId) return;
|
|
116
|
+
await apiRequest(config, "/memory/events", {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: {
|
|
119
|
+
workspace_id: config.workspaceId,
|
|
120
|
+
project_id: config.projectId || void 0,
|
|
121
|
+
event_type: eventType,
|
|
122
|
+
title,
|
|
123
|
+
content: typeof content === "string" ? content : JSON.stringify(content),
|
|
124
|
+
metadata: {
|
|
125
|
+
tags,
|
|
126
|
+
source: "mcp_hook",
|
|
127
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/hooks/stop.ts
|
|
134
|
+
async function runStopHook() {
|
|
135
|
+
if (process.env.CONTEXTSTREAM_STOP_ENABLED === "false") {
|
|
136
|
+
writeHookOutput();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const input = readHookInput();
|
|
140
|
+
const cwd = extractCwd(input);
|
|
141
|
+
const config = loadHookConfig(cwd);
|
|
142
|
+
if (isConfigured(config)) {
|
|
143
|
+
const sessionId = typeof input.session_id === "string" && input.session_id || "unknown";
|
|
144
|
+
const reason = typeof input.reason === "string" && input.reason || typeof input.stop_reason === "string" && input.stop_reason || "response_complete";
|
|
145
|
+
await postMemoryEvent(
|
|
146
|
+
config,
|
|
147
|
+
"Stop checkpoint",
|
|
148
|
+
{
|
|
149
|
+
session_id: sessionId,
|
|
150
|
+
reason,
|
|
151
|
+
hook: "stop",
|
|
152
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
153
|
+
tool_name: typeof input.tool_name === "string" ? input.tool_name : null,
|
|
154
|
+
model: typeof input.model === "string" ? input.model : null
|
|
155
|
+
},
|
|
156
|
+
["hook", "stop", "checkpoint"]
|
|
157
|
+
).catch(() => {
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
writeHookOutput();
|
|
161
|
+
}
|
|
162
|
+
var isDirectRun = process.argv[1]?.includes("stop") || process.argv[2] === "stop";
|
|
163
|
+
if (isDirectRun) {
|
|
164
|
+
runStopHook().catch(() => process.exit(0));
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
runStopHook
|
|
168
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
function readHookInput() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function writeHookOutput(output) {
|
|
16
|
+
const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
|
|
17
|
+
hookSpecificOutput: output.additionalContext ? {
|
|
18
|
+
hookEventName: output.hookEventName,
|
|
19
|
+
additionalContext: output.additionalContext
|
|
20
|
+
} : void 0,
|
|
21
|
+
additionalContext: output.additionalContext,
|
|
22
|
+
blocked: output.blocked,
|
|
23
|
+
reason: output.reason
|
|
24
|
+
} : {};
|
|
25
|
+
console.log(JSON.stringify(payload));
|
|
26
|
+
}
|
|
27
|
+
function extractCwd(input) {
|
|
28
|
+
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : process.cwd();
|
|
29
|
+
return cwd;
|
|
30
|
+
}
|
|
31
|
+
function loadHookConfig(cwd) {
|
|
32
|
+
let apiUrl = process.env.CONTEXTSTREAM_API_URL || DEFAULT_API_URL;
|
|
33
|
+
let apiKey = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
34
|
+
let jwt = process.env.CONTEXTSTREAM_JWT || "";
|
|
35
|
+
let workspaceId = process.env.CONTEXTSTREAM_WORKSPACE_ID || null;
|
|
36
|
+
let projectId = process.env.CONTEXTSTREAM_PROJECT_ID || null;
|
|
37
|
+
let searchDir = path.resolve(cwd);
|
|
38
|
+
for (let i = 0; i < 6; i++) {
|
|
39
|
+
if (!apiKey && !jwt) {
|
|
40
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
41
|
+
if (fs.existsSync(mcpPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
44
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
45
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
46
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
47
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
48
|
+
if (env?.CONTEXTSTREAM_WORKSPACE_ID && !workspaceId) workspaceId = env.CONTEXTSTREAM_WORKSPACE_ID;
|
|
49
|
+
if (env?.CONTEXTSTREAM_PROJECT_ID && !projectId) projectId = env.CONTEXTSTREAM_PROJECT_ID;
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!workspaceId || !projectId) {
|
|
55
|
+
const localConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
56
|
+
if (fs.existsSync(localConfigPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf8"));
|
|
59
|
+
if (localConfig.workspace_id && !workspaceId) workspaceId = localConfig.workspace_id;
|
|
60
|
+
if (localConfig.project_id && !projectId) projectId = localConfig.project_id;
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const parentDir = path.dirname(searchDir);
|
|
66
|
+
if (parentDir === searchDir) break;
|
|
67
|
+
searchDir = parentDir;
|
|
68
|
+
}
|
|
69
|
+
if (!apiKey && !jwt) {
|
|
70
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
71
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const config = JSON.parse(fs.readFileSync(homeMcpPath, "utf8"));
|
|
74
|
+
const env = config.mcpServers?.contextstream?.env;
|
|
75
|
+
if (env?.CONTEXTSTREAM_API_KEY) apiKey = env.CONTEXTSTREAM_API_KEY;
|
|
76
|
+
if (env?.CONTEXTSTREAM_JWT) jwt = env.CONTEXTSTREAM_JWT;
|
|
77
|
+
if (env?.CONTEXTSTREAM_API_URL) apiUrl = env.CONTEXTSTREAM_API_URL;
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { apiUrl, apiKey, jwt, workspaceId, projectId };
|
|
83
|
+
}
|
|
84
|
+
function isConfigured(config) {
|
|
85
|
+
return Boolean(config.apiKey || config.jwt);
|
|
86
|
+
}
|
|
87
|
+
function authHeaders(config) {
|
|
88
|
+
if (config.apiKey) {
|
|
89
|
+
return { "X-API-Key": config.apiKey };
|
|
90
|
+
}
|
|
91
|
+
if (config.jwt) {
|
|
92
|
+
return { Authorization: `Bearer ${config.jwt}` };
|
|
93
|
+
}
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
async function apiRequest(config, apiPath, init = {}) {
|
|
97
|
+
const response = await fetch(`${config.apiUrl}${apiPath}`, {
|
|
98
|
+
method: init.method || "GET",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
...authHeaders(config)
|
|
102
|
+
},
|
|
103
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
const json = await response.json();
|
|
109
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
110
|
+
return json.data;
|
|
111
|
+
}
|
|
112
|
+
return json;
|
|
113
|
+
}
|
|
114
|
+
async function fetchFastContext(config, body) {
|
|
115
|
+
if (!isConfigured(config)) return null;
|
|
116
|
+
try {
|
|
117
|
+
const result = await apiRequest(config, "/context/hook", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
body: {
|
|
120
|
+
workspace_id: config.workspaceId || void 0,
|
|
121
|
+
project_id: config.projectId || void 0,
|
|
122
|
+
...body
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (typeof result?.context === "string") return result.context;
|
|
126
|
+
if (typeof result?.data?.context === "string") return result.data.context;
|
|
127
|
+
return null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/hooks/subagent-start.ts
|
|
134
|
+
var SEARCH_PROTOCOL = `[CONTEXTSTREAM SEARCH]
|
|
135
|
+
When searching code, prefer ContextStream search tools first:
|
|
136
|
+
- mcp__contextstream__search(mode="auto", query="...")
|
|
137
|
+
- mcp__contextstream__search(mode="keyword", query="...", include_content=true)
|
|
138
|
+
- mcp__contextstream__graph(...) for dependency analysis
|
|
139
|
+
Fall back to local discovery tools only if ContextStream search returns no results.`;
|
|
140
|
+
var EXPLORE_SEARCH_FIRST = `[CRITICAL: SEARCH-FIRST PROTOCOL]
|
|
141
|
+
You MUST call mcp__contextstream__search(mode="auto", query="...") BEFORE reading source files.
|
|
142
|
+
Read only the small set of files and line ranges identified by search.`;
|
|
143
|
+
var PLAN_SEARCH_FIRST = `[PLAN MODE: SEARCH-FIRST]
|
|
144
|
+
Plan mode does NOT justify file-by-file repository scans.
|
|
145
|
+
Start with ContextStream search, then read only the narrowed files and ranges.`;
|
|
146
|
+
function fallbackContext() {
|
|
147
|
+
return `${SEARCH_PROTOCOL}
|
|
148
|
+
|
|
149
|
+
[CONTEXTSTREAM] Call mcp__contextstream__context(user_message="...") for task-specific context.`;
|
|
150
|
+
}
|
|
151
|
+
async function runSubagentStartHook() {
|
|
152
|
+
if (process.env.CONTEXTSTREAM_SUBAGENT_CONTEXT_ENABLED === "false") {
|
|
153
|
+
writeHookOutput();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const input = readHookInput();
|
|
157
|
+
const cwd = extractCwd(input);
|
|
158
|
+
const config = loadHookConfig(cwd);
|
|
159
|
+
const agentType = typeof input.agent_type === "string" && input.agent_type || typeof input.subagent_type === "string" && input.subagent_type || "unknown";
|
|
160
|
+
if (!isConfigured(config)) {
|
|
161
|
+
writeHookOutput({ additionalContext: fallbackContext(), hookEventName: "SubagentStart" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const context = await fetchFastContext(config, {
|
|
165
|
+
session_id: typeof input.session_id === "string" && input.session_id || typeof input.sessionId === "string" && input.sessionId || void 0,
|
|
166
|
+
user_message: `SubagentStart:${agentType}`
|
|
167
|
+
});
|
|
168
|
+
const parts = [];
|
|
169
|
+
if (context) parts.push(context);
|
|
170
|
+
if (agentType.toLowerCase() === "explore") {
|
|
171
|
+
parts.push(EXPLORE_SEARCH_FIRST);
|
|
172
|
+
} else if (agentType.toLowerCase() === "plan") {
|
|
173
|
+
parts.push(PLAN_SEARCH_FIRST);
|
|
174
|
+
parts.push(
|
|
175
|
+
`[CONTEXTSTREAM PLAN]
|
|
176
|
+
Save plans to ContextStream with mcp__contextstream__session(action="capture_plan", ...).
|
|
177
|
+
Create tasks with mcp__contextstream__memory(action="create_task", ...).`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
parts.push(SEARCH_PROTOCOL);
|
|
181
|
+
writeHookOutput({
|
|
182
|
+
additionalContext: parts.filter(Boolean).join("\n\n") || fallbackContext(),
|
|
183
|
+
hookEventName: "SubagentStart"
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
var isDirectRun = process.argv[1]?.includes("subagent-start") || process.argv[2] === "subagent-start";
|
|
187
|
+
if (isDirectRun) {
|
|
188
|
+
runSubagentStartHook().catch(() => process.exit(0));
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
runSubagentStartHook
|
|
192
|
+
};
|