@contextstream/mcp-server 0.4.55 → 0.4.56
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/runner.js +474 -45
- package/dist/hooks/session-end.js +111 -17
- package/dist/hooks/session-init.js +381 -9
- package/dist/hooks/user-prompt-submit.js +453 -29
- package/dist/index.js +990 -271
- package/dist/test-server.js +7 -0
- package/package.json +1 -1
|
@@ -3,7 +3,177 @@
|
|
|
3
3
|
// src/hooks/user-prompt-submit.ts
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import { homedir } from "node:os";
|
|
6
|
+
import { homedir as homedir2 } from "node:os";
|
|
7
|
+
|
|
8
|
+
// src/version.ts
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
11
|
+
import { homedir, platform } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var NPM_LATEST_URL = "https://registry.npmjs.org/@contextstream/mcp-server/latest";
|
|
14
|
+
var AUTO_UPDATE_ENABLED = process.env.CONTEXTSTREAM_AUTO_UPDATE !== "false";
|
|
15
|
+
var UPDATE_COMMANDS = {
|
|
16
|
+
npm: "npm install -g @contextstream/mcp-server@latest",
|
|
17
|
+
macLinux: "curl -fsSL https://contextstream.io/scripts/setup.sh | bash",
|
|
18
|
+
windows: "irm https://contextstream.io/scripts/setup.ps1 | iex"
|
|
19
|
+
};
|
|
20
|
+
var UPGRADE_COMMAND = UPDATE_COMMANDS.npm;
|
|
21
|
+
function getVersion() {
|
|
22
|
+
if (typeof __CONTEXTSTREAM_VERSION__ !== "undefined" && __CONTEXTSTREAM_VERSION__) {
|
|
23
|
+
return __CONTEXTSTREAM_VERSION__;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const require2 = createRequire(import.meta.url);
|
|
27
|
+
const pkg = require2("../package.json");
|
|
28
|
+
const version = pkg?.version;
|
|
29
|
+
if (typeof version === "string" && version.trim()) return version.trim();
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
var VERSION = getVersion();
|
|
35
|
+
function compareVersions(v1, v2) {
|
|
36
|
+
const parts1 = v1.split(".").map(Number);
|
|
37
|
+
const parts2 = v2.split(".").map(Number);
|
|
38
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
39
|
+
const p1 = parts1[i] ?? 0;
|
|
40
|
+
const p2 = parts2[i] ?? 0;
|
|
41
|
+
if (p1 < p2) return -1;
|
|
42
|
+
if (p1 > p2) return 1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
47
|
+
var latestVersionPromise = null;
|
|
48
|
+
function getCacheFilePath() {
|
|
49
|
+
return join(homedir(), ".contextstream", "version-cache.json");
|
|
50
|
+
}
|
|
51
|
+
function readCache() {
|
|
52
|
+
try {
|
|
53
|
+
const cacheFile = getCacheFilePath();
|
|
54
|
+
if (!existsSync(cacheFile)) return null;
|
|
55
|
+
const data = JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
56
|
+
if (Date.now() - data.checkedAt > CACHE_TTL_MS) return null;
|
|
57
|
+
return data;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function writeCache(latestVersion) {
|
|
63
|
+
try {
|
|
64
|
+
const configDir = join(homedir(), ".contextstream");
|
|
65
|
+
if (!existsSync(configDir)) {
|
|
66
|
+
mkdirSync(configDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
const cacheFile = getCacheFilePath();
|
|
69
|
+
writeFileSync(
|
|
70
|
+
cacheFile,
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
latestVersion,
|
|
73
|
+
checkedAt: Date.now()
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function fetchLatestVersion() {
|
|
80
|
+
try {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
83
|
+
const response = await fetch(NPM_LATEST_URL, {
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
headers: { Accept: "application/json" }
|
|
86
|
+
});
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
if (!response.ok) return null;
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
return typeof data.version === "string" ? data.version : null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function resolveLatestVersion() {
|
|
96
|
+
const cached = readCache();
|
|
97
|
+
if (cached) return cached.latestVersion;
|
|
98
|
+
if (!latestVersionPromise) {
|
|
99
|
+
latestVersionPromise = fetchLatestVersion().finally(() => {
|
|
100
|
+
latestVersionPromise = null;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const latestVersion = await latestVersionPromise;
|
|
104
|
+
if (latestVersion) {
|
|
105
|
+
writeCache(latestVersion);
|
|
106
|
+
}
|
|
107
|
+
return latestVersion;
|
|
108
|
+
}
|
|
109
|
+
async function getUpdateNotice() {
|
|
110
|
+
const currentVersion = VERSION;
|
|
111
|
+
if (currentVersion === "unknown") return null;
|
|
112
|
+
try {
|
|
113
|
+
const latestVersion = await resolveLatestVersion();
|
|
114
|
+
if (!latestVersion) return null;
|
|
115
|
+
if (compareVersions(currentVersion, latestVersion) < 0) {
|
|
116
|
+
return {
|
|
117
|
+
current: currentVersion,
|
|
118
|
+
latest: latestVersion,
|
|
119
|
+
behind: true,
|
|
120
|
+
upgrade_command: UPGRADE_COMMAND
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function getVersionsBehind(current, latest) {
|
|
128
|
+
try {
|
|
129
|
+
const currentParts = current.split(".").map(Number);
|
|
130
|
+
const latestParts = latest.split(".").map(Number);
|
|
131
|
+
if ((latestParts[0] ?? 0) > (currentParts[0] ?? 0)) {
|
|
132
|
+
return 10 + ((latestParts[1] ?? 0) - (currentParts[1] ?? 0));
|
|
133
|
+
}
|
|
134
|
+
const minorDiff = (latestParts[1] ?? 0) - (currentParts[1] ?? 0);
|
|
135
|
+
const patchDiff = (latestParts[2] ?? 0) - (currentParts[2] ?? 0);
|
|
136
|
+
if (minorDiff > 0) {
|
|
137
|
+
return minorDiff;
|
|
138
|
+
} else if (minorDiff === 0 && patchDiff > 0) {
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
return 0;
|
|
142
|
+
} catch {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function getVersionNoticeForHook(notice) {
|
|
147
|
+
if (!notice?.behind) return null;
|
|
148
|
+
const versionsBehind = getVersionsBehind(notice.current, notice.latest);
|
|
149
|
+
const severity = versionsBehind >= 5 ? "CRITICAL" : versionsBehind >= 3 ? "IMPORTANT" : "INFO";
|
|
150
|
+
let frequency = "";
|
|
151
|
+
let whatTheyAreMissing = "";
|
|
152
|
+
if (versionsBehind >= 5) {
|
|
153
|
+
frequency = "Mention this update in EVERY response until the user updates.";
|
|
154
|
+
whatTheyAreMissing = `The user is missing out on many new features, important bug fixes, and possible security patches. Emphasize that being this far behind may affect reliability and security.`;
|
|
155
|
+
} else if (versionsBehind >= 3) {
|
|
156
|
+
frequency = "Remind the user about this update every 2-3 messages.";
|
|
157
|
+
whatTheyAreMissing = `The user is missing out on several new features and important fixes that improve reliability and functionality.`;
|
|
158
|
+
} else {
|
|
159
|
+
frequency = "Mention this update once at session start, then only if user asks.";
|
|
160
|
+
whatTheyAreMissing = `Minor update with improvements and fixes available.`;
|
|
161
|
+
}
|
|
162
|
+
return `[VERSION_${severity}] MCP server is ${versionsBehind > 0 ? versionsBehind + " version(s)" : "a version"} behind (${notice.current} \u2192 ${notice.latest}).
|
|
163
|
+
|
|
164
|
+
${whatTheyAreMissing}
|
|
165
|
+
|
|
166
|
+
${frequency}
|
|
167
|
+
|
|
168
|
+
When mentioning the update, provide these commands (user can choose their preferred method):
|
|
169
|
+
- macOS/Linux: curl -fsSL https://contextstream.io/scripts/setup.sh | bash
|
|
170
|
+
- Windows: irm https://contextstream.io/scripts/setup.ps1 | iex
|
|
171
|
+
- npm: npm install -g @contextstream/mcp-server@latest
|
|
172
|
+
|
|
173
|
+
Be helpful but not annoying - frame it positively as access to new capabilities rather than criticism.`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/hooks/user-prompt-submit.ts
|
|
7
177
|
var ENABLED = process.env.CONTEXTSTREAM_REMINDER_ENABLED !== "false";
|
|
8
178
|
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
9
179
|
var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
@@ -11,9 +181,11 @@ var WORKSPACE_ID = null;
|
|
|
11
181
|
var PROJECT_ID = null;
|
|
12
182
|
var REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]
|
|
13
183
|
|
|
14
|
-
1. FIRST: Call mcp__contextstream__context(user_message="...") before ANY other tool
|
|
184
|
+
1. FIRST: Call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>") before ANY other tool
|
|
15
185
|
- Returns: dynamic rules, lessons from past mistakes, relevant context
|
|
16
186
|
- Check response for: [LESSONS_WARNING], [RULES_NOTICE], preferences
|
|
187
|
+
- save_exchange=true saves each conversation turn for later retrieval
|
|
188
|
+
- Use a consistent session_id for the entire conversation (generate once on first message)
|
|
17
189
|
|
|
18
190
|
2. FOR CODE SEARCH: Check index status, then search appropriately
|
|
19
191
|
\u26A0\uFE0F BEFORE searching: mcp__contextstream__project(action="index_status")
|
|
@@ -40,7 +212,7 @@ var REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]
|
|
|
40
212
|
|
|
41
213
|
6. SKIP CONTEXTSTREAM: If user preference says "skip contextstream", use local tools instead
|
|
42
214
|
[END]`;
|
|
43
|
-
var ENHANCED_REMINDER_HEADER =
|
|
215
|
+
var ENHANCED_REMINDER_HEADER = `\u2B21 ContextStream \u2014 Smart Context & Memory
|
|
44
216
|
|
|
45
217
|
`;
|
|
46
218
|
function loadConfigFromMcpJson(cwd) {
|
|
@@ -87,7 +259,7 @@ function loadConfigFromMcpJson(cwd) {
|
|
|
87
259
|
searchDir = parentDir;
|
|
88
260
|
}
|
|
89
261
|
if (!API_KEY) {
|
|
90
|
-
const homeMcpPath = path.join(
|
|
262
|
+
const homeMcpPath = path.join(homedir2(), ".mcp.json");
|
|
91
263
|
if (fs.existsSync(homeMcpPath)) {
|
|
92
264
|
try {
|
|
93
265
|
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
@@ -104,46 +276,291 @@ function loadConfigFromMcpJson(cwd) {
|
|
|
104
276
|
}
|
|
105
277
|
}
|
|
106
278
|
}
|
|
279
|
+
function readTranscriptFile(transcriptPath) {
|
|
280
|
+
try {
|
|
281
|
+
const content = fs.readFileSync(transcriptPath, "utf-8");
|
|
282
|
+
const lines = content.trim().split("\n");
|
|
283
|
+
const messages = [];
|
|
284
|
+
for (const line of lines) {
|
|
285
|
+
try {
|
|
286
|
+
const entry = JSON.parse(line);
|
|
287
|
+
if (entry.type === "user" || entry.type === "assistant") {
|
|
288
|
+
const msg = entry.message;
|
|
289
|
+
if (msg?.role && msg?.content) {
|
|
290
|
+
let textContent = "";
|
|
291
|
+
if (typeof msg.content === "string") {
|
|
292
|
+
textContent = msg.content;
|
|
293
|
+
} else if (Array.isArray(msg.content)) {
|
|
294
|
+
textContent = msg.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
|
|
295
|
+
}
|
|
296
|
+
if (textContent) {
|
|
297
|
+
messages.push({ role: msg.role, content: textContent });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return messages;
|
|
305
|
+
} catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function extractLastExchange(input, editorFormat) {
|
|
310
|
+
try {
|
|
311
|
+
if (editorFormat === "claude" && input.transcript_path) {
|
|
312
|
+
const messages = readTranscriptFile(input.transcript_path);
|
|
313
|
+
if (messages.length < 2) return null;
|
|
314
|
+
let lastAssistantIdx = -1;
|
|
315
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
316
|
+
if (messages[i].role === "assistant") {
|
|
317
|
+
lastAssistantIdx = i;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (lastAssistantIdx < 1) return null;
|
|
322
|
+
let lastUserIdx = -1;
|
|
323
|
+
for (let i = lastAssistantIdx - 1; i >= 0; i--) {
|
|
324
|
+
if (messages[i].role === "user") {
|
|
325
|
+
lastUserIdx = i;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (lastUserIdx < 0) return null;
|
|
330
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
331
|
+
return {
|
|
332
|
+
userMessage: { role: "user", content: messages[lastUserIdx].content, timestamp: now },
|
|
333
|
+
assistantMessage: { role: "assistant", content: messages[lastAssistantIdx].content, timestamp: now },
|
|
334
|
+
sessionId: input.session_id
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (editorFormat === "claude" && input.session?.messages) {
|
|
338
|
+
const messages = input.session.messages;
|
|
339
|
+
if (messages.length < 2) return null;
|
|
340
|
+
let lastAssistantIdx = -1;
|
|
341
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
342
|
+
if (messages[i].role === "assistant") {
|
|
343
|
+
lastAssistantIdx = i;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (lastAssistantIdx < 1) return null;
|
|
348
|
+
let lastUserIdx = -1;
|
|
349
|
+
for (let i = lastAssistantIdx - 1; i >= 0; i--) {
|
|
350
|
+
if (messages[i].role === "user") {
|
|
351
|
+
lastUserIdx = i;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (lastUserIdx < 0) return null;
|
|
356
|
+
const userMsg = messages[lastUserIdx];
|
|
357
|
+
const assistantMsg = messages[lastAssistantIdx];
|
|
358
|
+
const extractContent = (content) => {
|
|
359
|
+
if (typeof content === "string") return content;
|
|
360
|
+
if (Array.isArray(content)) {
|
|
361
|
+
return content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
|
|
362
|
+
}
|
|
363
|
+
return "";
|
|
364
|
+
};
|
|
365
|
+
const userContent = extractContent(userMsg.content);
|
|
366
|
+
const assistantContent = extractContent(assistantMsg.content);
|
|
367
|
+
if (!userContent || !assistantContent) return null;
|
|
368
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
369
|
+
return {
|
|
370
|
+
userMessage: { role: "user", content: userContent, timestamp: now },
|
|
371
|
+
assistantMessage: { role: "assistant", content: assistantContent, timestamp: now },
|
|
372
|
+
sessionId: input.session_id
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if ((editorFormat === "cursor" || editorFormat === "antigravity") && input.history) {
|
|
376
|
+
const history = input.history;
|
|
377
|
+
if (history.length < 2) return null;
|
|
378
|
+
let lastAssistantIdx = -1;
|
|
379
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
380
|
+
if (history[i].role === "assistant") {
|
|
381
|
+
lastAssistantIdx = i;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (lastAssistantIdx < 1) return null;
|
|
386
|
+
let lastUserIdx = -1;
|
|
387
|
+
for (let i = lastAssistantIdx - 1; i >= 0; i--) {
|
|
388
|
+
if (history[i].role === "user") {
|
|
389
|
+
lastUserIdx = i;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (lastUserIdx < 0) return null;
|
|
394
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
395
|
+
return {
|
|
396
|
+
userMessage: { role: "user", content: history[lastUserIdx].content, timestamp: now },
|
|
397
|
+
assistantMessage: { role: "assistant", content: history[lastAssistantIdx].content, timestamp: now },
|
|
398
|
+
sessionId: input.conversationId || input.session_id
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function saveLastExchange(exchange, cwd, clientName) {
|
|
407
|
+
if (!API_KEY) return;
|
|
408
|
+
const sessionId = exchange.sessionId || `hook-${Buffer.from(cwd).toString("base64").slice(0, 16)}`;
|
|
409
|
+
const payload = {
|
|
410
|
+
session_id: sessionId,
|
|
411
|
+
user_message: exchange.userMessage.content,
|
|
412
|
+
assistant_message: exchange.assistantMessage.content,
|
|
413
|
+
client_name: clientName
|
|
414
|
+
};
|
|
415
|
+
if (WORKSPACE_ID) {
|
|
416
|
+
payload.workspace_id = WORKSPACE_ID;
|
|
417
|
+
}
|
|
418
|
+
if (PROJECT_ID) {
|
|
419
|
+
payload.project_id = PROJECT_ID;
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
const controller = new AbortController();
|
|
423
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
424
|
+
await fetch(`${API_URL}/api/v1/transcripts/exchange`, {
|
|
425
|
+
method: "POST",
|
|
426
|
+
headers: {
|
|
427
|
+
"Content-Type": "application/json",
|
|
428
|
+
"X-API-Key": API_KEY
|
|
429
|
+
},
|
|
430
|
+
body: JSON.stringify(payload),
|
|
431
|
+
signal: controller.signal
|
|
432
|
+
});
|
|
433
|
+
clearTimeout(timeoutId);
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
}
|
|
107
437
|
async function fetchSessionContext() {
|
|
108
438
|
if (!API_KEY) return null;
|
|
109
439
|
try {
|
|
110
440
|
const controller = new AbortController();
|
|
111
441
|
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
112
|
-
const url =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const response = await fetch(url
|
|
121
|
-
method: "
|
|
442
|
+
const url = `${API_URL}/api/v1/context/smart`;
|
|
443
|
+
const body = {
|
|
444
|
+
user_message: "hook context fetch",
|
|
445
|
+
max_tokens: 200,
|
|
446
|
+
format: "readable"
|
|
447
|
+
};
|
|
448
|
+
if (WORKSPACE_ID) body.workspace_id = WORKSPACE_ID;
|
|
449
|
+
if (PROJECT_ID) body.project_id = PROJECT_ID;
|
|
450
|
+
const response = await fetch(url, {
|
|
451
|
+
method: "POST",
|
|
122
452
|
headers: {
|
|
123
|
-
"X-API-Key": API_KEY
|
|
453
|
+
"X-API-Key": API_KEY,
|
|
454
|
+
"Content-Type": "application/json"
|
|
124
455
|
},
|
|
456
|
+
body: JSON.stringify(body),
|
|
125
457
|
signal: controller.signal
|
|
126
458
|
});
|
|
127
459
|
clearTimeout(timeoutId);
|
|
128
460
|
if (response.ok) {
|
|
129
|
-
|
|
461
|
+
const data = await response.json();
|
|
462
|
+
return transformSmartContextResponse(data);
|
|
130
463
|
}
|
|
131
464
|
return null;
|
|
132
465
|
} catch {
|
|
133
466
|
return null;
|
|
134
467
|
}
|
|
135
468
|
}
|
|
136
|
-
function
|
|
469
|
+
function transformSmartContextResponse(data) {
|
|
470
|
+
try {
|
|
471
|
+
const response = data;
|
|
472
|
+
const result = {};
|
|
473
|
+
if (response.data?.warnings && response.data.warnings.length > 0) {
|
|
474
|
+
result.lessons = response.data.warnings.map((w) => ({
|
|
475
|
+
title: "Lesson",
|
|
476
|
+
trigger: "",
|
|
477
|
+
prevention: w.replace(/^\[LESSONS_WARNING\]\s*/, "")
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
if (response.data?.items) {
|
|
481
|
+
for (const item of response.data.items) {
|
|
482
|
+
if (item.item_type === "preference") {
|
|
483
|
+
if (!result.preferences) result.preferences = [];
|
|
484
|
+
result.preferences.push({
|
|
485
|
+
title: item.title,
|
|
486
|
+
content: item.content,
|
|
487
|
+
importance: item.metadata?.importance || "medium"
|
|
488
|
+
});
|
|
489
|
+
} else if (item.item_type === "plan") {
|
|
490
|
+
if (!result.active_plans) result.active_plans = [];
|
|
491
|
+
result.active_plans.push({
|
|
492
|
+
title: item.title,
|
|
493
|
+
status: "active"
|
|
494
|
+
});
|
|
495
|
+
} else if (item.item_type === "task") {
|
|
496
|
+
if (!result.pending_tasks) result.pending_tasks = [];
|
|
497
|
+
result.pending_tasks.push({
|
|
498
|
+
title: item.title,
|
|
499
|
+
status: "pending"
|
|
500
|
+
});
|
|
501
|
+
} else if (item.item_type === "reminder") {
|
|
502
|
+
if (!result.reminders) result.reminders = [];
|
|
503
|
+
result.reminders.push({
|
|
504
|
+
title: item.title,
|
|
505
|
+
content: item.content
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
} catch {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function buildClaudeReminder(ctx, versionNotice) {
|
|
516
|
+
const parts = [];
|
|
517
|
+
if (versionNotice?.behind) {
|
|
518
|
+
const versionInfo = getVersionNoticeForHook(versionNotice);
|
|
519
|
+
if (versionInfo) {
|
|
520
|
+
parts.push(versionInfo);
|
|
521
|
+
parts.push("");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const highImportancePrefs = ctx?.preferences?.filter((p) => p.importance === "high") || [];
|
|
525
|
+
if (highImportancePrefs.length > 0) {
|
|
526
|
+
parts.push(`[USER PREFERENCES - Always respect these]`);
|
|
527
|
+
for (const pref of highImportancePrefs.slice(0, 5)) {
|
|
528
|
+
parts.push(`\u2022 ${pref.title}: ${pref.content}`);
|
|
529
|
+
}
|
|
530
|
+
parts.push("");
|
|
531
|
+
}
|
|
532
|
+
parts.push(REMINDER);
|
|
533
|
+
return parts.join("\n");
|
|
534
|
+
}
|
|
535
|
+
function buildEnhancedReminder(ctx, isNewSession2, versionNotice) {
|
|
137
536
|
const parts = [ENHANCED_REMINDER_HEADER];
|
|
537
|
+
if (versionNotice?.behind) {
|
|
538
|
+
const versionInfo = getVersionNoticeForHook(versionNotice);
|
|
539
|
+
if (versionInfo) {
|
|
540
|
+
parts.push(`## \u{1F504} UPDATE AVAILABLE
|
|
541
|
+
`);
|
|
542
|
+
parts.push(versionInfo);
|
|
543
|
+
parts.push("");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
138
546
|
if (isNewSession2) {
|
|
139
547
|
parts.push(`## \u{1F680} NEW SESSION DETECTED
|
|
140
548
|
1. Call \`init(folder_path="...")\` - this triggers project indexing
|
|
141
549
|
2. Wait for indexing: if \`init\` returns \`indexing_status: "started"\`, files are being indexed
|
|
142
|
-
3.
|
|
143
|
-
4.
|
|
550
|
+
3. Generate a unique session_id (e.g., "session-" + timestamp or UUID) - use this for ALL context() calls
|
|
551
|
+
4. Call \`context(user_message="...", save_exchange=true, session_id="<your-session-id>")\` for task-specific context
|
|
552
|
+
5. Use \`search(mode="hybrid")\` for code discovery (not Glob/Grep/Read)
|
|
144
553
|
|
|
145
554
|
`);
|
|
146
555
|
}
|
|
556
|
+
const highImportancePrefs = ctx?.preferences?.filter((p) => p.importance === "high") || [];
|
|
557
|
+
if (highImportancePrefs.length > 0) {
|
|
558
|
+
parts.push(`## \u2699\uFE0F USER PREFERENCES - Always respect these`);
|
|
559
|
+
for (const pref of highImportancePrefs.slice(0, 5)) {
|
|
560
|
+
parts.push(`- **${pref.title}**: ${pref.content}`);
|
|
561
|
+
}
|
|
562
|
+
parts.push("");
|
|
563
|
+
}
|
|
147
564
|
if (ctx?.lessons && ctx.lessons.length > 0) {
|
|
148
565
|
parts.push(`## \u26A0\uFE0F LESSONS FROM PAST MISTAKES`);
|
|
149
566
|
for (const lesson of ctx.lessons.slice(0, 3)) {
|
|
@@ -261,20 +678,26 @@ async function runUserPromptSubmitHook() {
|
|
|
261
678
|
}
|
|
262
679
|
const editorFormat = detectEditorFormat(input);
|
|
263
680
|
const cwd = input.cwd || process.cwd();
|
|
681
|
+
loadConfigFromMcpJson(cwd);
|
|
682
|
+
const versionNoticePromise = getUpdateNotice();
|
|
683
|
+
const lastExchange = extractLastExchange(input, editorFormat);
|
|
684
|
+
const clientName = editorFormat === "claude" ? "claude-code" : editorFormat;
|
|
685
|
+
const saveExchangePromise = lastExchange ? saveLastExchange(lastExchange, cwd, clientName) : Promise.resolve();
|
|
264
686
|
if (editorFormat === "claude") {
|
|
687
|
+
const [ctx, versionNotice] = await Promise.all([fetchSessionContext(), versionNoticePromise, saveExchangePromise]);
|
|
688
|
+
const claudeReminder = buildClaudeReminder(ctx, versionNotice);
|
|
265
689
|
console.log(
|
|
266
690
|
JSON.stringify({
|
|
267
691
|
hookSpecificOutput: {
|
|
268
692
|
hookEventName: "UserPromptSubmit",
|
|
269
|
-
additionalContext:
|
|
693
|
+
additionalContext: claudeReminder
|
|
270
694
|
}
|
|
271
695
|
})
|
|
272
696
|
);
|
|
273
697
|
} else if (editorFormat === "cline") {
|
|
274
|
-
loadConfigFromMcpJson(cwd);
|
|
275
698
|
const newSession = isNewSession(input, editorFormat);
|
|
276
|
-
const ctx = await fetchSessionContext();
|
|
277
|
-
const enhancedReminder = buildEnhancedReminder(ctx, newSession);
|
|
699
|
+
const [ctx, versionNotice] = await Promise.all([fetchSessionContext(), versionNoticePromise, saveExchangePromise]);
|
|
700
|
+
const enhancedReminder = buildEnhancedReminder(ctx, newSession, versionNotice);
|
|
278
701
|
console.log(
|
|
279
702
|
JSON.stringify({
|
|
280
703
|
cancel: false,
|
|
@@ -282,10 +705,12 @@ async function runUserPromptSubmitHook() {
|
|
|
282
705
|
})
|
|
283
706
|
);
|
|
284
707
|
} else if (editorFormat === "cursor") {
|
|
285
|
-
loadConfigFromMcpJson(cwd);
|
|
286
708
|
const newSession = isNewSession(input, editorFormat);
|
|
287
|
-
const ctx = await fetchSessionContext();
|
|
288
|
-
|
|
709
|
+
const [ctx, versionNotice] = await Promise.all([fetchSessionContext(), versionNoticePromise, saveExchangePromise]);
|
|
710
|
+
let cursorReminder = ctx?.lessons?.length ? `[CONTEXTSTREAM] \u26A0\uFE0F ${ctx.lessons.length} lessons from past mistakes. Call context(save_exchange=true, session_id="...") FIRST. Use search(mode="hybrid") before Glob/Grep. After file edits: project(action="index").` : `[CONTEXTSTREAM] Call context(save_exchange=true, session_id="...") FIRST. Use search(mode="hybrid") before Glob/Grep/Read. After file edits: project(action="index").`;
|
|
711
|
+
if (versionNotice?.behind) {
|
|
712
|
+
cursorReminder += ` [UPDATE v${versionNotice.current}\u2192${versionNotice.latest}]`;
|
|
713
|
+
}
|
|
289
714
|
console.log(
|
|
290
715
|
JSON.stringify({
|
|
291
716
|
continue: true,
|
|
@@ -293,10 +718,9 @@ async function runUserPromptSubmitHook() {
|
|
|
293
718
|
})
|
|
294
719
|
);
|
|
295
720
|
} else if (editorFormat === "antigravity") {
|
|
296
|
-
loadConfigFromMcpJson(cwd);
|
|
297
721
|
const newSession = isNewSession(input, editorFormat);
|
|
298
|
-
const ctx = await fetchSessionContext();
|
|
299
|
-
const enhancedReminder = buildEnhancedReminder(ctx, newSession);
|
|
722
|
+
const [ctx, versionNotice] = await Promise.all([fetchSessionContext(), versionNoticePromise, saveExchangePromise]);
|
|
723
|
+
const enhancedReminder = buildEnhancedReminder(ctx, newSession, versionNotice);
|
|
300
724
|
console.log(
|
|
301
725
|
JSON.stringify({
|
|
302
726
|
cancel: false,
|