@contextstream/mcp-server 0.4.44 → 0.4.45
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 +92 -211
- package/dist/hooks/auto-rules.js +912 -0
- package/dist/hooks/media-aware.js +103 -0
- package/dist/hooks/post-write.js +341 -0
- package/dist/hooks/pre-compact.js +229 -0
- package/dist/hooks/pre-tool-use.js +236 -0
- package/dist/hooks/user-prompt-submit.js +69 -0
- package/dist/index.js +11659 -10452
- package/package.json +3 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/media-aware.ts
|
|
4
|
+
var ENABLED = process.env.CONTEXTSTREAM_MEDIA_HOOK_ENABLED !== "false";
|
|
5
|
+
var PATTERNS = [
|
|
6
|
+
/\b(video|videos|clip|clips|footage|keyframe)s?\b/i,
|
|
7
|
+
/\b(remotion|timeline|video\s*edit)\b/i,
|
|
8
|
+
/\b(image|images|photo|photos|picture|thumbnail)s?\b/i,
|
|
9
|
+
/\b(audio|podcast|transcript|transcription|voice)\b/i,
|
|
10
|
+
/\b(media|asset|assets|creative|b-roll)\b/i,
|
|
11
|
+
/\b(find|search|show).*(clip|video|image|audio|footage|media)\b/i
|
|
12
|
+
];
|
|
13
|
+
var MEDIA_CONTEXT = `[MEDIA TOOLS AVAILABLE]
|
|
14
|
+
Your workspace may have indexed media. Use ContextStream media tools:
|
|
15
|
+
|
|
16
|
+
- **Search**: \`mcp__contextstream__media(action="search", query="description")\`
|
|
17
|
+
- **Get clip**: \`mcp__contextstream__media(action="get_clip", content_id="...", start="1:34", end="2:15", output_format="remotion|ffmpeg|raw")\`
|
|
18
|
+
- **List assets**: \`mcp__contextstream__media(action="list")\`
|
|
19
|
+
- **Index**: \`mcp__contextstream__media(action="index", file_path="...", content_type="video|audio|image|document")\`
|
|
20
|
+
|
|
21
|
+
For Remotion: use \`output_format="remotion"\` to get frame-based props.
|
|
22
|
+
[END MEDIA TOOLS]`;
|
|
23
|
+
function matchesMediaPattern(text) {
|
|
24
|
+
return PATTERNS.some((pattern) => pattern.test(text));
|
|
25
|
+
}
|
|
26
|
+
function extractPrompt(input) {
|
|
27
|
+
if (input.prompt) {
|
|
28
|
+
return input.prompt;
|
|
29
|
+
}
|
|
30
|
+
if (input.session?.messages) {
|
|
31
|
+
for (let i = input.session.messages.length - 1; i >= 0; i--) {
|
|
32
|
+
const msg = input.session.messages[i];
|
|
33
|
+
if (msg.role === "user") {
|
|
34
|
+
if (typeof msg.content === "string") {
|
|
35
|
+
return msg.content;
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(msg.content)) {
|
|
38
|
+
for (const block of msg.content) {
|
|
39
|
+
if (block.type === "text" && block.text) {
|
|
40
|
+
return block.text;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
function detectEditorFormat(input) {
|
|
51
|
+
if (input.hookName !== void 0) {
|
|
52
|
+
return "cline";
|
|
53
|
+
}
|
|
54
|
+
return "claude";
|
|
55
|
+
}
|
|
56
|
+
async function runMediaAwareHook() {
|
|
57
|
+
if (!ENABLED) {
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
let inputData = "";
|
|
61
|
+
for await (const chunk of process.stdin) {
|
|
62
|
+
inputData += chunk;
|
|
63
|
+
}
|
|
64
|
+
if (!inputData.trim()) {
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
let input;
|
|
68
|
+
try {
|
|
69
|
+
input = JSON.parse(inputData);
|
|
70
|
+
} catch {
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
const prompt = extractPrompt(input);
|
|
74
|
+
if (!prompt || !matchesMediaPattern(prompt)) {
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
const editorFormat = detectEditorFormat(input);
|
|
78
|
+
if (editorFormat === "claude") {
|
|
79
|
+
console.log(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
hookSpecificOutput: {
|
|
82
|
+
hookEventName: "UserPromptSubmit",
|
|
83
|
+
additionalContext: MEDIA_CONTEXT
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
console.log(
|
|
89
|
+
JSON.stringify({
|
|
90
|
+
cancel: false,
|
|
91
|
+
contextModification: MEDIA_CONTEXT
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
var isDirectRun = process.argv[1]?.includes("media-aware") || process.argv[2] === "media-aware";
|
|
98
|
+
if (isDirectRun) {
|
|
99
|
+
runMediaAwareHook().catch(() => process.exit(0));
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
runMediaAwareHook
|
|
103
|
+
};
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/post-write.ts
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
8
|
+
var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
9
|
+
var ENABLED = process.env.CONTEXTSTREAM_POSTWRITE_ENABLED !== "false";
|
|
10
|
+
var INDEXABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
11
|
+
".ts",
|
|
12
|
+
".tsx",
|
|
13
|
+
".js",
|
|
14
|
+
".jsx",
|
|
15
|
+
".mjs",
|
|
16
|
+
".cjs",
|
|
17
|
+
".py",
|
|
18
|
+
".pyw",
|
|
19
|
+
".rs",
|
|
20
|
+
".go",
|
|
21
|
+
".java",
|
|
22
|
+
".kt",
|
|
23
|
+
".scala",
|
|
24
|
+
".c",
|
|
25
|
+
".cpp",
|
|
26
|
+
".cc",
|
|
27
|
+
".cxx",
|
|
28
|
+
".h",
|
|
29
|
+
".hpp",
|
|
30
|
+
".cs",
|
|
31
|
+
".fs",
|
|
32
|
+
".vb",
|
|
33
|
+
".rb",
|
|
34
|
+
".php",
|
|
35
|
+
".pl",
|
|
36
|
+
".pm",
|
|
37
|
+
".swift",
|
|
38
|
+
".m",
|
|
39
|
+
".mm",
|
|
40
|
+
".lua",
|
|
41
|
+
".r",
|
|
42
|
+
".jl",
|
|
43
|
+
".sh",
|
|
44
|
+
".bash",
|
|
45
|
+
".zsh",
|
|
46
|
+
".fish",
|
|
47
|
+
".sql",
|
|
48
|
+
".graphql",
|
|
49
|
+
".gql",
|
|
50
|
+
".html",
|
|
51
|
+
".htm",
|
|
52
|
+
".css",
|
|
53
|
+
".scss",
|
|
54
|
+
".sass",
|
|
55
|
+
".less",
|
|
56
|
+
".json",
|
|
57
|
+
".yaml",
|
|
58
|
+
".yml",
|
|
59
|
+
".toml",
|
|
60
|
+
".xml",
|
|
61
|
+
".ini",
|
|
62
|
+
".cfg",
|
|
63
|
+
".md",
|
|
64
|
+
".mdx",
|
|
65
|
+
".txt",
|
|
66
|
+
".rst",
|
|
67
|
+
".vue",
|
|
68
|
+
".svelte",
|
|
69
|
+
".astro",
|
|
70
|
+
".tf",
|
|
71
|
+
".hcl",
|
|
72
|
+
".dockerfile",
|
|
73
|
+
".containerfile",
|
|
74
|
+
".prisma",
|
|
75
|
+
".proto"
|
|
76
|
+
]);
|
|
77
|
+
var MAX_FILE_SIZE = 1024 * 1024;
|
|
78
|
+
function extractFilePath(input) {
|
|
79
|
+
if (input.tool_input) {
|
|
80
|
+
const filePath = input.tool_input.file_path || input.tool_input.notebook_path || input.tool_input.path;
|
|
81
|
+
if (filePath) return filePath;
|
|
82
|
+
}
|
|
83
|
+
if (input.parameters) {
|
|
84
|
+
const filePath = input.parameters.path || input.parameters.file_path;
|
|
85
|
+
if (filePath) return filePath;
|
|
86
|
+
}
|
|
87
|
+
if (input.toolParameters?.path) {
|
|
88
|
+
return input.toolParameters.path;
|
|
89
|
+
}
|
|
90
|
+
if (input.file_path) {
|
|
91
|
+
return input.file_path;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function extractCwd(input) {
|
|
96
|
+
if (input.cwd) return input.cwd;
|
|
97
|
+
if (input.workspace_roots?.length) return input.workspace_roots[0];
|
|
98
|
+
if (input.workspaceRoots?.length) return input.workspaceRoots[0];
|
|
99
|
+
return process.cwd();
|
|
100
|
+
}
|
|
101
|
+
function findLocalConfig(startDir) {
|
|
102
|
+
let currentDir = path.resolve(startDir);
|
|
103
|
+
for (let i = 0; i < 10; i++) {
|
|
104
|
+
const configPath = path.join(currentDir, ".contextstream", "config.json");
|
|
105
|
+
if (fs.existsSync(configPath)) {
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
108
|
+
return JSON.parse(content);
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const parentDir = path.dirname(currentDir);
|
|
113
|
+
if (parentDir === currentDir) break;
|
|
114
|
+
currentDir = parentDir;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function loadApiConfig(startDir) {
|
|
119
|
+
let apiUrl = API_URL;
|
|
120
|
+
let apiKey = API_KEY;
|
|
121
|
+
if (apiKey) {
|
|
122
|
+
return { apiUrl, apiKey };
|
|
123
|
+
}
|
|
124
|
+
let currentDir = path.resolve(startDir);
|
|
125
|
+
for (let i = 0; i < 10; i++) {
|
|
126
|
+
const mcpPath = path.join(currentDir, ".mcp.json");
|
|
127
|
+
if (fs.existsSync(mcpPath)) {
|
|
128
|
+
try {
|
|
129
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
130
|
+
const config = JSON.parse(content);
|
|
131
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
132
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
133
|
+
apiKey = csEnv.CONTEXTSTREAM_API_KEY;
|
|
134
|
+
}
|
|
135
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
136
|
+
apiUrl = csEnv.CONTEXTSTREAM_API_URL;
|
|
137
|
+
}
|
|
138
|
+
if (apiKey) break;
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const parentDir = path.dirname(currentDir);
|
|
143
|
+
if (parentDir === currentDir) break;
|
|
144
|
+
currentDir = parentDir;
|
|
145
|
+
}
|
|
146
|
+
if (!apiKey) {
|
|
147
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
148
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
149
|
+
try {
|
|
150
|
+
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
151
|
+
const config = JSON.parse(content);
|
|
152
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
153
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
154
|
+
apiKey = csEnv.CONTEXTSTREAM_API_KEY;
|
|
155
|
+
}
|
|
156
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
157
|
+
apiUrl = csEnv.CONTEXTSTREAM_API_URL;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { apiUrl, apiKey };
|
|
164
|
+
}
|
|
165
|
+
function shouldIndexFile(filePath) {
|
|
166
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
167
|
+
if (!INDEXABLE_EXTENSIONS.has(ext)) {
|
|
168
|
+
const basename2 = path.basename(filePath).toLowerCase();
|
|
169
|
+
if (!["dockerfile", "makefile", "rakefile", "gemfile", "procfile"].includes(basename2)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const stats = fs.statSync(filePath);
|
|
175
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
function detectLanguage(filePath) {
|
|
184
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
185
|
+
const langMap = {
|
|
186
|
+
".ts": "typescript",
|
|
187
|
+
".tsx": "typescript",
|
|
188
|
+
".js": "javascript",
|
|
189
|
+
".jsx": "javascript",
|
|
190
|
+
".mjs": "javascript",
|
|
191
|
+
".cjs": "javascript",
|
|
192
|
+
".py": "python",
|
|
193
|
+
".pyw": "python",
|
|
194
|
+
".rs": "rust",
|
|
195
|
+
".go": "go",
|
|
196
|
+
".java": "java",
|
|
197
|
+
".kt": "kotlin",
|
|
198
|
+
".scala": "scala",
|
|
199
|
+
".c": "c",
|
|
200
|
+
".cpp": "cpp",
|
|
201
|
+
".cc": "cpp",
|
|
202
|
+
".cxx": "cpp",
|
|
203
|
+
".h": "c",
|
|
204
|
+
".hpp": "cpp",
|
|
205
|
+
".cs": "csharp",
|
|
206
|
+
".fs": "fsharp",
|
|
207
|
+
".vb": "vb",
|
|
208
|
+
".rb": "ruby",
|
|
209
|
+
".php": "php",
|
|
210
|
+
".pl": "perl",
|
|
211
|
+
".pm": "perl",
|
|
212
|
+
".swift": "swift",
|
|
213
|
+
".m": "objective-c",
|
|
214
|
+
".mm": "objective-cpp",
|
|
215
|
+
".lua": "lua",
|
|
216
|
+
".r": "r",
|
|
217
|
+
".jl": "julia",
|
|
218
|
+
".sh": "shell",
|
|
219
|
+
".bash": "shell",
|
|
220
|
+
".zsh": "shell",
|
|
221
|
+
".fish": "shell",
|
|
222
|
+
".sql": "sql",
|
|
223
|
+
".graphql": "graphql",
|
|
224
|
+
".gql": "graphql",
|
|
225
|
+
".html": "html",
|
|
226
|
+
".htm": "html",
|
|
227
|
+
".css": "css",
|
|
228
|
+
".scss": "scss",
|
|
229
|
+
".sass": "sass",
|
|
230
|
+
".less": "less",
|
|
231
|
+
".json": "json",
|
|
232
|
+
".yaml": "yaml",
|
|
233
|
+
".yml": "yaml",
|
|
234
|
+
".toml": "toml",
|
|
235
|
+
".xml": "xml",
|
|
236
|
+
".ini": "ini",
|
|
237
|
+
".cfg": "ini",
|
|
238
|
+
".md": "markdown",
|
|
239
|
+
".mdx": "mdx",
|
|
240
|
+
".txt": "text",
|
|
241
|
+
".rst": "rst",
|
|
242
|
+
".vue": "vue",
|
|
243
|
+
".svelte": "svelte",
|
|
244
|
+
".astro": "astro",
|
|
245
|
+
".tf": "terraform",
|
|
246
|
+
".hcl": "hcl",
|
|
247
|
+
".prisma": "prisma",
|
|
248
|
+
".proto": "protobuf"
|
|
249
|
+
};
|
|
250
|
+
return langMap[ext] || "text";
|
|
251
|
+
}
|
|
252
|
+
async function indexFile(filePath, projectId, apiUrl, apiKey, projectRoot) {
|
|
253
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
254
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
255
|
+
const payload = {
|
|
256
|
+
files: [
|
|
257
|
+
{
|
|
258
|
+
path: relativePath,
|
|
259
|
+
content,
|
|
260
|
+
language: detectLanguage(filePath)
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
};
|
|
264
|
+
const response = await fetch(`${apiUrl}/api/v1/projects/${projectId}/files/ingest`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
"X-API-Key": apiKey
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify(payload),
|
|
271
|
+
signal: AbortSignal.timeout(1e4)
|
|
272
|
+
// 10 second timeout
|
|
273
|
+
});
|
|
274
|
+
if (!response.ok) {
|
|
275
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function findProjectRoot(filePath) {
|
|
279
|
+
let currentDir = path.dirname(path.resolve(filePath));
|
|
280
|
+
for (let i = 0; i < 10; i++) {
|
|
281
|
+
const configPath = path.join(currentDir, ".contextstream", "config.json");
|
|
282
|
+
if (fs.existsSync(configPath)) {
|
|
283
|
+
return currentDir;
|
|
284
|
+
}
|
|
285
|
+
const parentDir = path.dirname(currentDir);
|
|
286
|
+
if (parentDir === currentDir) break;
|
|
287
|
+
currentDir = parentDir;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
async function runPostWriteHook() {
|
|
292
|
+
if (!ENABLED) {
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
let inputData = "";
|
|
296
|
+
for await (const chunk of process.stdin) {
|
|
297
|
+
inputData += chunk;
|
|
298
|
+
}
|
|
299
|
+
if (!inputData.trim()) {
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
let input;
|
|
303
|
+
try {
|
|
304
|
+
input = JSON.parse(inputData);
|
|
305
|
+
} catch {
|
|
306
|
+
process.exit(0);
|
|
307
|
+
}
|
|
308
|
+
const filePath = extractFilePath(input);
|
|
309
|
+
if (!filePath) {
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}
|
|
312
|
+
const cwd = extractCwd(input);
|
|
313
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
314
|
+
if (!fs.existsSync(absolutePath) || !shouldIndexFile(absolutePath)) {
|
|
315
|
+
process.exit(0);
|
|
316
|
+
}
|
|
317
|
+
const projectRoot = findProjectRoot(absolutePath);
|
|
318
|
+
if (!projectRoot) {
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
const localConfig = findLocalConfig(projectRoot);
|
|
322
|
+
if (!localConfig?.project_id) {
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
const { apiUrl, apiKey } = loadApiConfig(projectRoot);
|
|
326
|
+
if (!apiKey) {
|
|
327
|
+
process.exit(0);
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
await indexFile(absolutePath, localConfig.project_id, apiUrl, apiKey, projectRoot);
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
process.exit(0);
|
|
334
|
+
}
|
|
335
|
+
var isDirectRun = process.argv[1]?.includes("post-write") || process.argv[2] === "post-write";
|
|
336
|
+
if (isDirectRun) {
|
|
337
|
+
runPostWriteHook().catch(() => process.exit(0));
|
|
338
|
+
}
|
|
339
|
+
export {
|
|
340
|
+
runPostWriteHook
|
|
341
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/pre-compact.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_PRECOMPACT_ENABLED !== "false";
|
|
8
|
+
var AUTO_SAVE = process.env.CONTEXTSTREAM_PRECOMPACT_AUTO_SAVE !== "false";
|
|
9
|
+
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
10
|
+
var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
11
|
+
var WORKSPACE_ID = null;
|
|
12
|
+
function loadConfigFromMcpJson(cwd) {
|
|
13
|
+
let searchDir = path.resolve(cwd);
|
|
14
|
+
for (let i = 0; i < 5; i++) {
|
|
15
|
+
if (!API_KEY) {
|
|
16
|
+
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
17
|
+
if (fs.existsSync(mcpPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
20
|
+
const config = JSON.parse(content);
|
|
21
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
22
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
23
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
24
|
+
}
|
|
25
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
26
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!WORKSPACE_ID) {
|
|
33
|
+
const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
34
|
+
if (fs.existsSync(csConfigPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(csConfigPath, "utf-8");
|
|
37
|
+
const csConfig = JSON.parse(content);
|
|
38
|
+
if (csConfig.workspace_id) {
|
|
39
|
+
WORKSPACE_ID = csConfig.workspace_id;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const parentDir = path.dirname(searchDir);
|
|
46
|
+
if (parentDir === searchDir) break;
|
|
47
|
+
searchDir = parentDir;
|
|
48
|
+
}
|
|
49
|
+
if (!API_KEY) {
|
|
50
|
+
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
51
|
+
if (fs.existsSync(homeMcpPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
54
|
+
const config = JSON.parse(content);
|
|
55
|
+
const csEnv = config.mcpServers?.contextstream?.env;
|
|
56
|
+
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
57
|
+
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
58
|
+
}
|
|
59
|
+
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
60
|
+
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parseTranscript(transcriptPath) {
|
|
68
|
+
const activeFiles = /* @__PURE__ */ new Set();
|
|
69
|
+
const recentMessages = [];
|
|
70
|
+
const toolCalls = [];
|
|
71
|
+
try {
|
|
72
|
+
const content = fs.readFileSync(transcriptPath, "utf-8");
|
|
73
|
+
const lines = content.split("\n");
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
if (!line.trim()) continue;
|
|
76
|
+
try {
|
|
77
|
+
const entry = JSON.parse(line);
|
|
78
|
+
const msgType = entry.type || "";
|
|
79
|
+
if (msgType === "tool_use") {
|
|
80
|
+
const toolName = entry.name || "";
|
|
81
|
+
const toolInput = entry.input || {};
|
|
82
|
+
toolCalls.push({ name: toolName, input: toolInput });
|
|
83
|
+
if (["Read", "Write", "Edit", "NotebookEdit"].includes(toolName)) {
|
|
84
|
+
const filePath = toolInput.file_path || toolInput.notebook_path;
|
|
85
|
+
if (filePath) {
|
|
86
|
+
activeFiles.add(filePath);
|
|
87
|
+
}
|
|
88
|
+
} else if (toolName === "Glob") {
|
|
89
|
+
const pattern = toolInput.pattern;
|
|
90
|
+
if (pattern) {
|
|
91
|
+
activeFiles.add(`[glob:${pattern}]`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (msgType === "assistant" && entry.content) {
|
|
96
|
+
const content2 = entry.content;
|
|
97
|
+
if (typeof content2 === "string" && content2.length > 50) {
|
|
98
|
+
recentMessages.push(content2.slice(0, 500));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
activeFiles: Array.from(activeFiles).slice(-20),
|
|
109
|
+
// Last 20 files
|
|
110
|
+
toolCallCount: toolCalls.length,
|
|
111
|
+
messageCount: recentMessages.length,
|
|
112
|
+
lastTools: toolCalls.slice(-10).map((t) => t.name)
|
|
113
|
+
// Last 10 tool names
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function saveSnapshot(sessionId, transcriptData, trigger) {
|
|
117
|
+
if (!API_KEY) {
|
|
118
|
+
return { success: false, message: "No API key configured" };
|
|
119
|
+
}
|
|
120
|
+
const snapshotContent = {
|
|
121
|
+
session_id: sessionId,
|
|
122
|
+
trigger,
|
|
123
|
+
captured_at: null,
|
|
124
|
+
// API will set timestamp
|
|
125
|
+
active_files: transcriptData.activeFiles,
|
|
126
|
+
tool_call_count: transcriptData.toolCallCount,
|
|
127
|
+
last_tools: transcriptData.lastTools,
|
|
128
|
+
auto_captured: true
|
|
129
|
+
};
|
|
130
|
+
const payload = {
|
|
131
|
+
event_type: "session_snapshot",
|
|
132
|
+
title: `Auto Pre-compaction Snapshot (${trigger})`,
|
|
133
|
+
content: JSON.stringify(snapshotContent),
|
|
134
|
+
importance: "high",
|
|
135
|
+
tags: ["session_snapshot", "pre_compaction", "auto_captured"],
|
|
136
|
+
source_type: "hook"
|
|
137
|
+
};
|
|
138
|
+
if (WORKSPACE_ID) {
|
|
139
|
+
payload.workspace_id = WORKSPACE_ID;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
144
|
+
const response = await fetch(`${API_URL}/api/v1/memory/events`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
"X-API-Key": API_KEY
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify(payload),
|
|
151
|
+
signal: controller.signal
|
|
152
|
+
});
|
|
153
|
+
clearTimeout(timeoutId);
|
|
154
|
+
if (response.ok) {
|
|
155
|
+
return { success: true, message: "Snapshot saved" };
|
|
156
|
+
}
|
|
157
|
+
return { success: false, message: `API error: ${response.status}` };
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return { success: false, message: String(error) };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function runPreCompactHook() {
|
|
163
|
+
if (!ENABLED) {
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
let inputData = "";
|
|
167
|
+
for await (const chunk of process.stdin) {
|
|
168
|
+
inputData += chunk;
|
|
169
|
+
}
|
|
170
|
+
if (!inputData.trim()) {
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
let input;
|
|
174
|
+
try {
|
|
175
|
+
input = JSON.parse(inputData);
|
|
176
|
+
} catch {
|
|
177
|
+
process.exit(0);
|
|
178
|
+
}
|
|
179
|
+
const cwd = input.cwd || process.cwd();
|
|
180
|
+
loadConfigFromMcpJson(cwd);
|
|
181
|
+
const sessionId = input.session_id || "unknown";
|
|
182
|
+
const transcriptPath = input.transcript_path || "";
|
|
183
|
+
const trigger = input.trigger || "unknown";
|
|
184
|
+
const customInstructions = input.custom_instructions || "";
|
|
185
|
+
let transcriptData = {
|
|
186
|
+
activeFiles: [],
|
|
187
|
+
toolCallCount: 0,
|
|
188
|
+
messageCount: 0,
|
|
189
|
+
lastTools: []
|
|
190
|
+
};
|
|
191
|
+
if (transcriptPath && fs.existsSync(transcriptPath)) {
|
|
192
|
+
transcriptData = parseTranscript(transcriptPath);
|
|
193
|
+
}
|
|
194
|
+
let autoSaveStatus = "";
|
|
195
|
+
if (AUTO_SAVE && API_KEY) {
|
|
196
|
+
const { success, message } = await saveSnapshot(sessionId, transcriptData, trigger);
|
|
197
|
+
if (success) {
|
|
198
|
+
autoSaveStatus = `
|
|
199
|
+
[ContextStream: Auto-saved snapshot with ${transcriptData.activeFiles.length} active files]`;
|
|
200
|
+
} else {
|
|
201
|
+
autoSaveStatus = `
|
|
202
|
+
[ContextStream: Auto-save failed - ${message}]`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const filesList = transcriptData.activeFiles.slice(0, 5).join(", ") || "none detected";
|
|
206
|
+
const context = `[CONTEXT COMPACTION - ${trigger.toUpperCase()}]${autoSaveStatus}
|
|
207
|
+
|
|
208
|
+
Active files detected: ${filesList}
|
|
209
|
+
Tool calls in session: ${transcriptData.toolCallCount}
|
|
210
|
+
|
|
211
|
+
After compaction, call session_init(is_post_compact=true) to restore context.${customInstructions ? `
|
|
212
|
+
User instructions: ${customInstructions}` : ""}`;
|
|
213
|
+
console.log(
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
hookSpecificOutput: {
|
|
216
|
+
hookEventName: "PreCompact",
|
|
217
|
+
additionalContext: context
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
var isDirectRun = process.argv[1]?.includes("pre-compact") || process.argv[2] === "pre-compact";
|
|
224
|
+
if (isDirectRun) {
|
|
225
|
+
runPreCompactHook().catch(() => process.exit(0));
|
|
226
|
+
}
|
|
227
|
+
export {
|
|
228
|
+
runPreCompactHook
|
|
229
|
+
};
|