@contextstream/mcp-server 0.4.55 → 0.4.57
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 +1604 -637
- package/dist/test-server.js +7 -0
- package/package.json +1 -1
package/dist/hooks/runner.js
CHANGED
|
@@ -282,6 +282,180 @@ var init_pre_tool_use = __esm({
|
|
|
282
282
|
}
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
+
// src/version.ts
|
|
286
|
+
import { createRequire } from "module";
|
|
287
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
288
|
+
import { homedir as homedir2, platform } from "os";
|
|
289
|
+
import { join as join2 } from "path";
|
|
290
|
+
function getVersion() {
|
|
291
|
+
if (typeof __CONTEXTSTREAM_VERSION__ !== "undefined" && __CONTEXTSTREAM_VERSION__) {
|
|
292
|
+
return __CONTEXTSTREAM_VERSION__;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const require2 = createRequire(import.meta.url);
|
|
296
|
+
const pkg = require2("../package.json");
|
|
297
|
+
const version = pkg?.version;
|
|
298
|
+
if (typeof version === "string" && version.trim()) return version.trim();
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
return "unknown";
|
|
302
|
+
}
|
|
303
|
+
function compareVersions(v1, v2) {
|
|
304
|
+
const parts1 = v1.split(".").map(Number);
|
|
305
|
+
const parts2 = v2.split(".").map(Number);
|
|
306
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
307
|
+
const p1 = parts1[i] ?? 0;
|
|
308
|
+
const p2 = parts2[i] ?? 0;
|
|
309
|
+
if (p1 < p2) return -1;
|
|
310
|
+
if (p1 > p2) return 1;
|
|
311
|
+
}
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
function getCacheFilePath() {
|
|
315
|
+
return join2(homedir2(), ".contextstream", "version-cache.json");
|
|
316
|
+
}
|
|
317
|
+
function readCache() {
|
|
318
|
+
try {
|
|
319
|
+
const cacheFile = getCacheFilePath();
|
|
320
|
+
if (!existsSync2(cacheFile)) return null;
|
|
321
|
+
const data = JSON.parse(readFileSync2(cacheFile, "utf-8"));
|
|
322
|
+
if (Date.now() - data.checkedAt > CACHE_TTL_MS) return null;
|
|
323
|
+
return data;
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function writeCache(latestVersion) {
|
|
329
|
+
try {
|
|
330
|
+
const configDir = join2(homedir2(), ".contextstream");
|
|
331
|
+
if (!existsSync2(configDir)) {
|
|
332
|
+
mkdirSync(configDir, { recursive: true });
|
|
333
|
+
}
|
|
334
|
+
const cacheFile = getCacheFilePath();
|
|
335
|
+
writeFileSync(
|
|
336
|
+
cacheFile,
|
|
337
|
+
JSON.stringify({
|
|
338
|
+
latestVersion,
|
|
339
|
+
checkedAt: Date.now()
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function fetchLatestVersion() {
|
|
346
|
+
try {
|
|
347
|
+
const controller = new AbortController();
|
|
348
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
349
|
+
const response = await fetch(NPM_LATEST_URL, {
|
|
350
|
+
signal: controller.signal,
|
|
351
|
+
headers: { Accept: "application/json" }
|
|
352
|
+
});
|
|
353
|
+
clearTimeout(timeout);
|
|
354
|
+
if (!response.ok) return null;
|
|
355
|
+
const data = await response.json();
|
|
356
|
+
return typeof data.version === "string" ? data.version : null;
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function resolveLatestVersion() {
|
|
362
|
+
const cached = readCache();
|
|
363
|
+
if (cached) return cached.latestVersion;
|
|
364
|
+
if (!latestVersionPromise) {
|
|
365
|
+
latestVersionPromise = fetchLatestVersion().finally(() => {
|
|
366
|
+
latestVersionPromise = null;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const latestVersion = await latestVersionPromise;
|
|
370
|
+
if (latestVersion) {
|
|
371
|
+
writeCache(latestVersion);
|
|
372
|
+
}
|
|
373
|
+
return latestVersion;
|
|
374
|
+
}
|
|
375
|
+
async function getUpdateNotice() {
|
|
376
|
+
const currentVersion = VERSION;
|
|
377
|
+
if (currentVersion === "unknown") return null;
|
|
378
|
+
try {
|
|
379
|
+
const latestVersion = await resolveLatestVersion();
|
|
380
|
+
if (!latestVersion) return null;
|
|
381
|
+
if (compareVersions(currentVersion, latestVersion) < 0) {
|
|
382
|
+
return {
|
|
383
|
+
current: currentVersion,
|
|
384
|
+
latest: latestVersion,
|
|
385
|
+
behind: true,
|
|
386
|
+
upgrade_command: UPGRADE_COMMAND
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
function getVersionsBehind(current, latest) {
|
|
394
|
+
try {
|
|
395
|
+
const currentParts = current.split(".").map(Number);
|
|
396
|
+
const latestParts = latest.split(".").map(Number);
|
|
397
|
+
if ((latestParts[0] ?? 0) > (currentParts[0] ?? 0)) {
|
|
398
|
+
return 10 + ((latestParts[1] ?? 0) - (currentParts[1] ?? 0));
|
|
399
|
+
}
|
|
400
|
+
const minorDiff = (latestParts[1] ?? 0) - (currentParts[1] ?? 0);
|
|
401
|
+
const patchDiff = (latestParts[2] ?? 0) - (currentParts[2] ?? 0);
|
|
402
|
+
if (minorDiff > 0) {
|
|
403
|
+
return minorDiff;
|
|
404
|
+
} else if (minorDiff === 0 && patchDiff > 0) {
|
|
405
|
+
return 1;
|
|
406
|
+
}
|
|
407
|
+
return 0;
|
|
408
|
+
} catch {
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function getVersionNoticeForHook(notice) {
|
|
413
|
+
if (!notice?.behind) return null;
|
|
414
|
+
const versionsBehind = getVersionsBehind(notice.current, notice.latest);
|
|
415
|
+
const severity = versionsBehind >= 5 ? "CRITICAL" : versionsBehind >= 3 ? "IMPORTANT" : "INFO";
|
|
416
|
+
let frequency = "";
|
|
417
|
+
let whatTheyAreMissing = "";
|
|
418
|
+
if (versionsBehind >= 5) {
|
|
419
|
+
frequency = "Mention this update in EVERY response until the user updates.";
|
|
420
|
+
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.`;
|
|
421
|
+
} else if (versionsBehind >= 3) {
|
|
422
|
+
frequency = "Remind the user about this update every 2-3 messages.";
|
|
423
|
+
whatTheyAreMissing = `The user is missing out on several new features and important fixes that improve reliability and functionality.`;
|
|
424
|
+
} else {
|
|
425
|
+
frequency = "Mention this update once at session start, then only if user asks.";
|
|
426
|
+
whatTheyAreMissing = `Minor update with improvements and fixes available.`;
|
|
427
|
+
}
|
|
428
|
+
return `[VERSION_${severity}] MCP server is ${versionsBehind > 0 ? versionsBehind + " version(s)" : "a version"} behind (${notice.current} \u2192 ${notice.latest}).
|
|
429
|
+
|
|
430
|
+
${whatTheyAreMissing}
|
|
431
|
+
|
|
432
|
+
${frequency}
|
|
433
|
+
|
|
434
|
+
When mentioning the update, provide these commands (user can choose their preferred method):
|
|
435
|
+
- macOS/Linux: curl -fsSL https://contextstream.io/scripts/setup.sh | bash
|
|
436
|
+
- Windows: irm https://contextstream.io/scripts/setup.ps1 | iex
|
|
437
|
+
- npm: npm install -g @contextstream/mcp-server@latest
|
|
438
|
+
|
|
439
|
+
Be helpful but not annoying - frame it positively as access to new capabilities rather than criticism.`;
|
|
440
|
+
}
|
|
441
|
+
var NPM_LATEST_URL, AUTO_UPDATE_ENABLED, UPDATE_COMMANDS, UPGRADE_COMMAND, VERSION, CACHE_TTL_MS, latestVersionPromise;
|
|
442
|
+
var init_version = __esm({
|
|
443
|
+
"src/version.ts"() {
|
|
444
|
+
"use strict";
|
|
445
|
+
NPM_LATEST_URL = "https://registry.npmjs.org/@contextstream/mcp-server/latest";
|
|
446
|
+
AUTO_UPDATE_ENABLED = process.env.CONTEXTSTREAM_AUTO_UPDATE !== "false";
|
|
447
|
+
UPDATE_COMMANDS = {
|
|
448
|
+
npm: "npm install -g @contextstream/mcp-server@latest",
|
|
449
|
+
macLinux: "curl -fsSL https://contextstream.io/scripts/setup.sh | bash",
|
|
450
|
+
windows: "irm https://contextstream.io/scripts/setup.ps1 | iex"
|
|
451
|
+
};
|
|
452
|
+
UPGRADE_COMMAND = UPDATE_COMMANDS.npm;
|
|
453
|
+
VERSION = getVersion();
|
|
454
|
+
CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
455
|
+
latestVersionPromise = null;
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
285
459
|
// src/hooks/user-prompt-submit.ts
|
|
286
460
|
var user_prompt_submit_exports = {};
|
|
287
461
|
__export(user_prompt_submit_exports, {
|
|
@@ -289,7 +463,7 @@ __export(user_prompt_submit_exports, {
|
|
|
289
463
|
});
|
|
290
464
|
import * as fs2 from "node:fs";
|
|
291
465
|
import * as path2 from "node:path";
|
|
292
|
-
import { homedir as
|
|
466
|
+
import { homedir as homedir3 } from "node:os";
|
|
293
467
|
function loadConfigFromMcpJson(cwd) {
|
|
294
468
|
let searchDir = path2.resolve(cwd);
|
|
295
469
|
for (let i = 0; i < 5; i++) {
|
|
@@ -334,7 +508,7 @@ function loadConfigFromMcpJson(cwd) {
|
|
|
334
508
|
searchDir = parentDir;
|
|
335
509
|
}
|
|
336
510
|
if (!API_KEY) {
|
|
337
|
-
const homeMcpPath = path2.join(
|
|
511
|
+
const homeMcpPath = path2.join(homedir3(), ".mcp.json");
|
|
338
512
|
if (fs2.existsSync(homeMcpPath)) {
|
|
339
513
|
try {
|
|
340
514
|
const content = fs2.readFileSync(homeMcpPath, "utf-8");
|
|
@@ -351,46 +525,291 @@ function loadConfigFromMcpJson(cwd) {
|
|
|
351
525
|
}
|
|
352
526
|
}
|
|
353
527
|
}
|
|
528
|
+
function readTranscriptFile(transcriptPath) {
|
|
529
|
+
try {
|
|
530
|
+
const content = fs2.readFileSync(transcriptPath, "utf-8");
|
|
531
|
+
const lines = content.trim().split("\n");
|
|
532
|
+
const messages = [];
|
|
533
|
+
for (const line of lines) {
|
|
534
|
+
try {
|
|
535
|
+
const entry = JSON.parse(line);
|
|
536
|
+
if (entry.type === "user" || entry.type === "assistant") {
|
|
537
|
+
const msg = entry.message;
|
|
538
|
+
if (msg?.role && msg?.content) {
|
|
539
|
+
let textContent = "";
|
|
540
|
+
if (typeof msg.content === "string") {
|
|
541
|
+
textContent = msg.content;
|
|
542
|
+
} else if (Array.isArray(msg.content)) {
|
|
543
|
+
textContent = msg.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
|
|
544
|
+
}
|
|
545
|
+
if (textContent) {
|
|
546
|
+
messages.push({ role: msg.role, content: textContent });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch {
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return messages;
|
|
554
|
+
} catch {
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function extractLastExchange(input, editorFormat) {
|
|
559
|
+
try {
|
|
560
|
+
if (editorFormat === "claude" && input.transcript_path) {
|
|
561
|
+
const messages = readTranscriptFile(input.transcript_path);
|
|
562
|
+
if (messages.length < 2) return null;
|
|
563
|
+
let lastAssistantIdx = -1;
|
|
564
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
565
|
+
if (messages[i].role === "assistant") {
|
|
566
|
+
lastAssistantIdx = i;
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (lastAssistantIdx < 1) return null;
|
|
571
|
+
let lastUserIdx = -1;
|
|
572
|
+
for (let i = lastAssistantIdx - 1; i >= 0; i--) {
|
|
573
|
+
if (messages[i].role === "user") {
|
|
574
|
+
lastUserIdx = i;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (lastUserIdx < 0) return null;
|
|
579
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
580
|
+
return {
|
|
581
|
+
userMessage: { role: "user", content: messages[lastUserIdx].content, timestamp: now },
|
|
582
|
+
assistantMessage: { role: "assistant", content: messages[lastAssistantIdx].content, timestamp: now },
|
|
583
|
+
sessionId: input.session_id
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (editorFormat === "claude" && input.session?.messages) {
|
|
587
|
+
const messages = input.session.messages;
|
|
588
|
+
if (messages.length < 2) return null;
|
|
589
|
+
let lastAssistantIdx = -1;
|
|
590
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
591
|
+
if (messages[i].role === "assistant") {
|
|
592
|
+
lastAssistantIdx = i;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (lastAssistantIdx < 1) return null;
|
|
597
|
+
let lastUserIdx = -1;
|
|
598
|
+
for (let i = lastAssistantIdx - 1; i >= 0; i--) {
|
|
599
|
+
if (messages[i].role === "user") {
|
|
600
|
+
lastUserIdx = i;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (lastUserIdx < 0) return null;
|
|
605
|
+
const userMsg = messages[lastUserIdx];
|
|
606
|
+
const assistantMsg = messages[lastAssistantIdx];
|
|
607
|
+
const extractContent = (content) => {
|
|
608
|
+
if (typeof content === "string") return content;
|
|
609
|
+
if (Array.isArray(content)) {
|
|
610
|
+
return content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
|
|
611
|
+
}
|
|
612
|
+
return "";
|
|
613
|
+
};
|
|
614
|
+
const userContent = extractContent(userMsg.content);
|
|
615
|
+
const assistantContent = extractContent(assistantMsg.content);
|
|
616
|
+
if (!userContent || !assistantContent) return null;
|
|
617
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
618
|
+
return {
|
|
619
|
+
userMessage: { role: "user", content: userContent, timestamp: now },
|
|
620
|
+
assistantMessage: { role: "assistant", content: assistantContent, timestamp: now },
|
|
621
|
+
sessionId: input.session_id
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
if ((editorFormat === "cursor" || editorFormat === "antigravity") && input.history) {
|
|
625
|
+
const history = input.history;
|
|
626
|
+
if (history.length < 2) return null;
|
|
627
|
+
let lastAssistantIdx = -1;
|
|
628
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
629
|
+
if (history[i].role === "assistant") {
|
|
630
|
+
lastAssistantIdx = i;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (lastAssistantIdx < 1) return null;
|
|
635
|
+
let lastUserIdx = -1;
|
|
636
|
+
for (let i = lastAssistantIdx - 1; i >= 0; i--) {
|
|
637
|
+
if (history[i].role === "user") {
|
|
638
|
+
lastUserIdx = i;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (lastUserIdx < 0) return null;
|
|
643
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
644
|
+
return {
|
|
645
|
+
userMessage: { role: "user", content: history[lastUserIdx].content, timestamp: now },
|
|
646
|
+
assistantMessage: { role: "assistant", content: history[lastAssistantIdx].content, timestamp: now },
|
|
647
|
+
sessionId: input.conversationId || input.session_id
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
} catch {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async function saveLastExchange(exchange, cwd, clientName) {
|
|
656
|
+
if (!API_KEY) return;
|
|
657
|
+
const sessionId = exchange.sessionId || `hook-${Buffer.from(cwd).toString("base64").slice(0, 16)}`;
|
|
658
|
+
const payload = {
|
|
659
|
+
session_id: sessionId,
|
|
660
|
+
user_message: exchange.userMessage.content,
|
|
661
|
+
assistant_message: exchange.assistantMessage.content,
|
|
662
|
+
client_name: clientName
|
|
663
|
+
};
|
|
664
|
+
if (WORKSPACE_ID) {
|
|
665
|
+
payload.workspace_id = WORKSPACE_ID;
|
|
666
|
+
}
|
|
667
|
+
if (PROJECT_ID) {
|
|
668
|
+
payload.project_id = PROJECT_ID;
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
const controller = new AbortController();
|
|
672
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
673
|
+
await fetch(`${API_URL}/api/v1/transcripts/exchange`, {
|
|
674
|
+
method: "POST",
|
|
675
|
+
headers: {
|
|
676
|
+
"Content-Type": "application/json",
|
|
677
|
+
"X-API-Key": API_KEY
|
|
678
|
+
},
|
|
679
|
+
body: JSON.stringify(payload),
|
|
680
|
+
signal: controller.signal
|
|
681
|
+
});
|
|
682
|
+
clearTimeout(timeoutId);
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
}
|
|
354
686
|
async function fetchSessionContext() {
|
|
355
687
|
if (!API_KEY) return null;
|
|
356
688
|
try {
|
|
357
689
|
const controller = new AbortController();
|
|
358
690
|
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
359
|
-
const url =
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const response = await fetch(url
|
|
368
|
-
method: "
|
|
691
|
+
const url = `${API_URL}/api/v1/context/smart`;
|
|
692
|
+
const body = {
|
|
693
|
+
user_message: "hook context fetch",
|
|
694
|
+
max_tokens: 200,
|
|
695
|
+
format: "readable"
|
|
696
|
+
};
|
|
697
|
+
if (WORKSPACE_ID) body.workspace_id = WORKSPACE_ID;
|
|
698
|
+
if (PROJECT_ID) body.project_id = PROJECT_ID;
|
|
699
|
+
const response = await fetch(url, {
|
|
700
|
+
method: "POST",
|
|
369
701
|
headers: {
|
|
370
|
-
"X-API-Key": API_KEY
|
|
702
|
+
"X-API-Key": API_KEY,
|
|
703
|
+
"Content-Type": "application/json"
|
|
371
704
|
},
|
|
705
|
+
body: JSON.stringify(body),
|
|
372
706
|
signal: controller.signal
|
|
373
707
|
});
|
|
374
708
|
clearTimeout(timeoutId);
|
|
375
709
|
if (response.ok) {
|
|
376
|
-
|
|
710
|
+
const data = await response.json();
|
|
711
|
+
return transformSmartContextResponse(data);
|
|
377
712
|
}
|
|
378
713
|
return null;
|
|
379
714
|
} catch {
|
|
380
715
|
return null;
|
|
381
716
|
}
|
|
382
717
|
}
|
|
383
|
-
function
|
|
718
|
+
function transformSmartContextResponse(data) {
|
|
719
|
+
try {
|
|
720
|
+
const response = data;
|
|
721
|
+
const result = {};
|
|
722
|
+
if (response.data?.warnings && response.data.warnings.length > 0) {
|
|
723
|
+
result.lessons = response.data.warnings.map((w) => ({
|
|
724
|
+
title: "Lesson",
|
|
725
|
+
trigger: "",
|
|
726
|
+
prevention: w.replace(/^\[LESSONS_WARNING\]\s*/, "")
|
|
727
|
+
}));
|
|
728
|
+
}
|
|
729
|
+
if (response.data?.items) {
|
|
730
|
+
for (const item of response.data.items) {
|
|
731
|
+
if (item.item_type === "preference") {
|
|
732
|
+
if (!result.preferences) result.preferences = [];
|
|
733
|
+
result.preferences.push({
|
|
734
|
+
title: item.title,
|
|
735
|
+
content: item.content,
|
|
736
|
+
importance: item.metadata?.importance || "medium"
|
|
737
|
+
});
|
|
738
|
+
} else if (item.item_type === "plan") {
|
|
739
|
+
if (!result.active_plans) result.active_plans = [];
|
|
740
|
+
result.active_plans.push({
|
|
741
|
+
title: item.title,
|
|
742
|
+
status: "active"
|
|
743
|
+
});
|
|
744
|
+
} else if (item.item_type === "task") {
|
|
745
|
+
if (!result.pending_tasks) result.pending_tasks = [];
|
|
746
|
+
result.pending_tasks.push({
|
|
747
|
+
title: item.title,
|
|
748
|
+
status: "pending"
|
|
749
|
+
});
|
|
750
|
+
} else if (item.item_type === "reminder") {
|
|
751
|
+
if (!result.reminders) result.reminders = [];
|
|
752
|
+
result.reminders.push({
|
|
753
|
+
title: item.title,
|
|
754
|
+
content: item.content
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return result;
|
|
760
|
+
} catch {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function buildClaudeReminder(ctx, versionNotice) {
|
|
765
|
+
const parts = [];
|
|
766
|
+
if (versionNotice?.behind) {
|
|
767
|
+
const versionInfo = getVersionNoticeForHook(versionNotice);
|
|
768
|
+
if (versionInfo) {
|
|
769
|
+
parts.push(versionInfo);
|
|
770
|
+
parts.push("");
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const highImportancePrefs = ctx?.preferences?.filter((p) => p.importance === "high") || [];
|
|
774
|
+
if (highImportancePrefs.length > 0) {
|
|
775
|
+
parts.push(`[USER PREFERENCES - Always respect these]`);
|
|
776
|
+
for (const pref of highImportancePrefs.slice(0, 5)) {
|
|
777
|
+
parts.push(`\u2022 ${pref.title}: ${pref.content}`);
|
|
778
|
+
}
|
|
779
|
+
parts.push("");
|
|
780
|
+
}
|
|
781
|
+
parts.push(REMINDER);
|
|
782
|
+
return parts.join("\n");
|
|
783
|
+
}
|
|
784
|
+
function buildEnhancedReminder(ctx, isNewSession2, versionNotice) {
|
|
384
785
|
const parts = [ENHANCED_REMINDER_HEADER];
|
|
786
|
+
if (versionNotice?.behind) {
|
|
787
|
+
const versionInfo = getVersionNoticeForHook(versionNotice);
|
|
788
|
+
if (versionInfo) {
|
|
789
|
+
parts.push(`## \u{1F504} UPDATE AVAILABLE
|
|
790
|
+
`);
|
|
791
|
+
parts.push(versionInfo);
|
|
792
|
+
parts.push("");
|
|
793
|
+
}
|
|
794
|
+
}
|
|
385
795
|
if (isNewSession2) {
|
|
386
796
|
parts.push(`## \u{1F680} NEW SESSION DETECTED
|
|
387
797
|
1. Call \`init(folder_path="...")\` - this triggers project indexing
|
|
388
798
|
2. Wait for indexing: if \`init\` returns \`indexing_status: "started"\`, files are being indexed
|
|
389
|
-
3.
|
|
390
|
-
4.
|
|
799
|
+
3. Generate a unique session_id (e.g., "session-" + timestamp or UUID) - use this for ALL context() calls
|
|
800
|
+
4. Call \`context(user_message="...", save_exchange=true, session_id="<your-session-id>")\` for task-specific context
|
|
801
|
+
5. Use \`search(mode="hybrid")\` for code discovery (not Glob/Grep/Read)
|
|
391
802
|
|
|
392
803
|
`);
|
|
393
804
|
}
|
|
805
|
+
const highImportancePrefs = ctx?.preferences?.filter((p) => p.importance === "high") || [];
|
|
806
|
+
if (highImportancePrefs.length > 0) {
|
|
807
|
+
parts.push(`## \u2699\uFE0F USER PREFERENCES - Always respect these`);
|
|
808
|
+
for (const pref of highImportancePrefs.slice(0, 5)) {
|
|
809
|
+
parts.push(`- **${pref.title}**: ${pref.content}`);
|
|
810
|
+
}
|
|
811
|
+
parts.push("");
|
|
812
|
+
}
|
|
394
813
|
if (ctx?.lessons && ctx.lessons.length > 0) {
|
|
395
814
|
parts.push(`## \u26A0\uFE0F LESSONS FROM PAST MISTAKES`);
|
|
396
815
|
for (const lesson of ctx.lessons.slice(0, 3)) {
|
|
@@ -508,20 +927,26 @@ async function runUserPromptSubmitHook() {
|
|
|
508
927
|
}
|
|
509
928
|
const editorFormat = detectEditorFormat2(input);
|
|
510
929
|
const cwd = input.cwd || process.cwd();
|
|
930
|
+
loadConfigFromMcpJson(cwd);
|
|
931
|
+
const versionNoticePromise = getUpdateNotice();
|
|
932
|
+
const lastExchange = extractLastExchange(input, editorFormat);
|
|
933
|
+
const clientName = editorFormat === "claude" ? "claude-code" : editorFormat;
|
|
934
|
+
const saveExchangePromise = lastExchange ? saveLastExchange(lastExchange, cwd, clientName) : Promise.resolve();
|
|
511
935
|
if (editorFormat === "claude") {
|
|
936
|
+
const [ctx, versionNotice] = await Promise.all([fetchSessionContext(), versionNoticePromise, saveExchangePromise]);
|
|
937
|
+
const claudeReminder = buildClaudeReminder(ctx, versionNotice);
|
|
512
938
|
console.log(
|
|
513
939
|
JSON.stringify({
|
|
514
940
|
hookSpecificOutput: {
|
|
515
941
|
hookEventName: "UserPromptSubmit",
|
|
516
|
-
additionalContext:
|
|
942
|
+
additionalContext: claudeReminder
|
|
517
943
|
}
|
|
518
944
|
})
|
|
519
945
|
);
|
|
520
946
|
} else if (editorFormat === "cline") {
|
|
521
|
-
loadConfigFromMcpJson(cwd);
|
|
522
947
|
const newSession = isNewSession(input, editorFormat);
|
|
523
|
-
const ctx = await fetchSessionContext();
|
|
524
|
-
const enhancedReminder = buildEnhancedReminder(ctx, newSession);
|
|
948
|
+
const [ctx, versionNotice] = await Promise.all([fetchSessionContext(), versionNoticePromise, saveExchangePromise]);
|
|
949
|
+
const enhancedReminder = buildEnhancedReminder(ctx, newSession, versionNotice);
|
|
525
950
|
console.log(
|
|
526
951
|
JSON.stringify({
|
|
527
952
|
cancel: false,
|
|
@@ -529,10 +954,12 @@ async function runUserPromptSubmitHook() {
|
|
|
529
954
|
})
|
|
530
955
|
);
|
|
531
956
|
} else if (editorFormat === "cursor") {
|
|
532
|
-
loadConfigFromMcpJson(cwd);
|
|
533
957
|
const newSession = isNewSession(input, editorFormat);
|
|
534
|
-
const ctx = await fetchSessionContext();
|
|
535
|
-
|
|
958
|
+
const [ctx, versionNotice] = await Promise.all([fetchSessionContext(), versionNoticePromise, saveExchangePromise]);
|
|
959
|
+
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").`;
|
|
960
|
+
if (versionNotice?.behind) {
|
|
961
|
+
cursorReminder += ` [UPDATE v${versionNotice.current}\u2192${versionNotice.latest}]`;
|
|
962
|
+
}
|
|
536
963
|
console.log(
|
|
537
964
|
JSON.stringify({
|
|
538
965
|
continue: true,
|
|
@@ -540,10 +967,9 @@ async function runUserPromptSubmitHook() {
|
|
|
540
967
|
})
|
|
541
968
|
);
|
|
542
969
|
} else if (editorFormat === "antigravity") {
|
|
543
|
-
loadConfigFromMcpJson(cwd);
|
|
544
970
|
const newSession = isNewSession(input, editorFormat);
|
|
545
|
-
const ctx = await fetchSessionContext();
|
|
546
|
-
const enhancedReminder = buildEnhancedReminder(ctx, newSession);
|
|
971
|
+
const [ctx, versionNotice] = await Promise.all([fetchSessionContext(), versionNoticePromise, saveExchangePromise]);
|
|
972
|
+
const enhancedReminder = buildEnhancedReminder(ctx, newSession, versionNotice);
|
|
547
973
|
console.log(
|
|
548
974
|
JSON.stringify({
|
|
549
975
|
cancel: false,
|
|
@@ -557,6 +983,7 @@ var ENABLED2, API_URL, API_KEY, WORKSPACE_ID, PROJECT_ID, REMINDER, ENHANCED_REM
|
|
|
557
983
|
var init_user_prompt_submit = __esm({
|
|
558
984
|
"src/hooks/user-prompt-submit.ts"() {
|
|
559
985
|
"use strict";
|
|
986
|
+
init_version();
|
|
560
987
|
ENABLED2 = process.env.CONTEXTSTREAM_REMINDER_ENABLED !== "false";
|
|
561
988
|
API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
562
989
|
API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
@@ -564,9 +991,11 @@ var init_user_prompt_submit = __esm({
|
|
|
564
991
|
PROJECT_ID = null;
|
|
565
992
|
REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]
|
|
566
993
|
|
|
567
|
-
1. FIRST: Call mcp__contextstream__context(user_message="...") before ANY other tool
|
|
994
|
+
1. FIRST: Call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>") before ANY other tool
|
|
568
995
|
- Returns: dynamic rules, lessons from past mistakes, relevant context
|
|
569
996
|
- Check response for: [LESSONS_WARNING], [RULES_NOTICE], preferences
|
|
997
|
+
- save_exchange=true saves each conversation turn for later retrieval
|
|
998
|
+
- Use a consistent session_id for the entire conversation (generate once on first message)
|
|
570
999
|
|
|
571
1000
|
2. FOR CODE SEARCH: Check index status, then search appropriately
|
|
572
1001
|
\u26A0\uFE0F BEFORE searching: mcp__contextstream__project(action="index_status")
|
|
@@ -593,7 +1022,7 @@ var init_user_prompt_submit = __esm({
|
|
|
593
1022
|
|
|
594
1023
|
6. SKIP CONTEXTSTREAM: If user preference says "skip contextstream", use local tools instead
|
|
595
1024
|
[END]`;
|
|
596
|
-
ENHANCED_REMINDER_HEADER =
|
|
1025
|
+
ENHANCED_REMINDER_HEADER = `\u2B21 ContextStream \u2014 Smart Context & Memory
|
|
597
1026
|
|
|
598
1027
|
`;
|
|
599
1028
|
isDirectRun2 = process.argv[1]?.includes("user-prompt-submit") || process.argv[2] === "user-prompt-submit";
|
|
@@ -719,7 +1148,7 @@ __export(pre_compact_exports, {
|
|
|
719
1148
|
});
|
|
720
1149
|
import * as fs3 from "node:fs";
|
|
721
1150
|
import * as path3 from "node:path";
|
|
722
|
-
import { homedir as
|
|
1151
|
+
import { homedir as homedir4 } from "node:os";
|
|
723
1152
|
function loadConfigFromMcpJson2(cwd) {
|
|
724
1153
|
let searchDir = path3.resolve(cwd);
|
|
725
1154
|
for (let i = 0; i < 5; i++) {
|
|
@@ -758,7 +1187,7 @@ function loadConfigFromMcpJson2(cwd) {
|
|
|
758
1187
|
searchDir = parentDir;
|
|
759
1188
|
}
|
|
760
1189
|
if (!API_KEY2) {
|
|
761
|
-
const homeMcpPath = path3.join(
|
|
1190
|
+
const homeMcpPath = path3.join(homedir4(), ".mcp.json");
|
|
762
1191
|
if (fs3.existsSync(homeMcpPath)) {
|
|
763
1192
|
try {
|
|
764
1193
|
const content = fs3.readFileSync(homeMcpPath, "utf-8");
|
|
@@ -1045,7 +1474,7 @@ __export(post_write_exports, {
|
|
|
1045
1474
|
});
|
|
1046
1475
|
import * as fs4 from "node:fs";
|
|
1047
1476
|
import * as path4 from "node:path";
|
|
1048
|
-
import { homedir as
|
|
1477
|
+
import { homedir as homedir5 } from "node:os";
|
|
1049
1478
|
function extractFilePath(input) {
|
|
1050
1479
|
if (input.tool_input) {
|
|
1051
1480
|
const filePath = input.tool_input.file_path || input.tool_input.notebook_path || input.tool_input.path;
|
|
@@ -1115,7 +1544,7 @@ function loadApiConfig(startDir) {
|
|
|
1115
1544
|
currentDir = parentDir;
|
|
1116
1545
|
}
|
|
1117
1546
|
if (!apiKey) {
|
|
1118
|
-
const homeMcpPath = path4.join(
|
|
1547
|
+
const homeMcpPath = path4.join(homedir5(), ".mcp.json");
|
|
1119
1548
|
if (fs4.existsSync(homeMcpPath)) {
|
|
1120
1549
|
try {
|
|
1121
1550
|
const content = fs4.readFileSync(homeMcpPath, "utf-8");
|
|
@@ -1429,7 +1858,7 @@ __export(hooks_config_exports, {
|
|
|
1429
1858
|
});
|
|
1430
1859
|
import * as fs5 from "node:fs/promises";
|
|
1431
1860
|
import * as path5 from "node:path";
|
|
1432
|
-
import { homedir as
|
|
1861
|
+
import { homedir as homedir6 } from "node:os";
|
|
1433
1862
|
import { fileURLToPath } from "node:url";
|
|
1434
1863
|
function getHookCommand(hookName2) {
|
|
1435
1864
|
const fs7 = __require("node:fs");
|
|
@@ -1449,7 +1878,7 @@ function getHookCommand(hookName2) {
|
|
|
1449
1878
|
}
|
|
1450
1879
|
function getClaudeSettingsPath(scope, projectPath) {
|
|
1451
1880
|
if (scope === "user") {
|
|
1452
|
-
return path5.join(
|
|
1881
|
+
return path5.join(homedir6(), ".claude", "settings.json");
|
|
1453
1882
|
}
|
|
1454
1883
|
if (!projectPath) {
|
|
1455
1884
|
throw new Error("projectPath required for project scope");
|
|
@@ -1457,7 +1886,7 @@ function getClaudeSettingsPath(scope, projectPath) {
|
|
|
1457
1886
|
return path5.join(projectPath, ".claude", "settings.json");
|
|
1458
1887
|
}
|
|
1459
1888
|
function getHooksDir() {
|
|
1460
|
-
return path5.join(
|
|
1889
|
+
return path5.join(homedir6(), ".claude", "hooks");
|
|
1461
1890
|
}
|
|
1462
1891
|
function buildHooksConfig(options) {
|
|
1463
1892
|
const userPromptHooks = [
|
|
@@ -1812,7 +2241,7 @@ If you prefer to configure manually, add to \`~/.claude/settings.json\`:
|
|
|
1812
2241
|
`.trim();
|
|
1813
2242
|
}
|
|
1814
2243
|
function getIndexStatusPath() {
|
|
1815
|
-
return path5.join(
|
|
2244
|
+
return path5.join(homedir6(), ".contextstream", "indexed-projects.json");
|
|
1816
2245
|
}
|
|
1817
2246
|
async function readIndexStatus() {
|
|
1818
2247
|
const statusPath = getIndexStatusPath();
|
|
@@ -1847,7 +2276,7 @@ async function unmarkProjectIndexed(projectPath) {
|
|
|
1847
2276
|
}
|
|
1848
2277
|
function getClineHooksDir(scope, projectPath) {
|
|
1849
2278
|
if (scope === "global") {
|
|
1850
|
-
return path5.join(
|
|
2279
|
+
return path5.join(homedir6(), "Documents", "Cline", "Rules", "Hooks");
|
|
1851
2280
|
}
|
|
1852
2281
|
if (!projectPath) {
|
|
1853
2282
|
throw new Error("projectPath required for project scope");
|
|
@@ -1874,7 +2303,7 @@ async function installClineHookScripts(options) {
|
|
|
1874
2303
|
}
|
|
1875
2304
|
function getRooCodeHooksDir(scope, projectPath) {
|
|
1876
2305
|
if (scope === "global") {
|
|
1877
|
-
return path5.join(
|
|
2306
|
+
return path5.join(homedir6(), ".roo", "hooks");
|
|
1878
2307
|
}
|
|
1879
2308
|
if (!projectPath) {
|
|
1880
2309
|
throw new Error("projectPath required for project scope");
|
|
@@ -1901,7 +2330,7 @@ async function installRooCodeHookScripts(options) {
|
|
|
1901
2330
|
}
|
|
1902
2331
|
function getKiloCodeHooksDir(scope, projectPath) {
|
|
1903
2332
|
if (scope === "global") {
|
|
1904
|
-
return path5.join(
|
|
2333
|
+
return path5.join(homedir6(), ".kilocode", "hooks");
|
|
1905
2334
|
}
|
|
1906
2335
|
if (!projectPath) {
|
|
1907
2336
|
throw new Error("projectPath required for project scope");
|
|
@@ -1928,7 +2357,7 @@ async function installKiloCodeHookScripts(options) {
|
|
|
1928
2357
|
}
|
|
1929
2358
|
function getCursorHooksConfigPath(scope, projectPath) {
|
|
1930
2359
|
if (scope === "global") {
|
|
1931
|
-
return path5.join(
|
|
2360
|
+
return path5.join(homedir6(), ".cursor", "hooks.json");
|
|
1932
2361
|
}
|
|
1933
2362
|
if (!projectPath) {
|
|
1934
2363
|
throw new Error("projectPath required for project scope");
|
|
@@ -1937,7 +2366,7 @@ function getCursorHooksConfigPath(scope, projectPath) {
|
|
|
1937
2366
|
}
|
|
1938
2367
|
function getCursorHooksDir(scope, projectPath) {
|
|
1939
2368
|
if (scope === "global") {
|
|
1940
|
-
return path5.join(
|
|
2369
|
+
return path5.join(homedir6(), ".cursor", "hooks");
|
|
1941
2370
|
}
|
|
1942
2371
|
if (!projectPath) {
|
|
1943
2372
|
throw new Error("projectPath required for project scope");
|
|
@@ -2986,7 +3415,7 @@ __export(auto_rules_exports, {
|
|
|
2986
3415
|
});
|
|
2987
3416
|
import * as fs6 from "node:fs";
|
|
2988
3417
|
import * as path6 from "node:path";
|
|
2989
|
-
import { homedir as
|
|
3418
|
+
import { homedir as homedir7 } from "node:os";
|
|
2990
3419
|
function hasRunRecently() {
|
|
2991
3420
|
try {
|
|
2992
3421
|
if (!fs6.existsSync(MARKER_FILE)) return false;
|
|
@@ -3057,7 +3486,7 @@ function hasPythonHooks(settingsPath) {
|
|
|
3057
3486
|
}
|
|
3058
3487
|
}
|
|
3059
3488
|
function detectPythonHooks(cwd) {
|
|
3060
|
-
const globalSettingsPath = path6.join(
|
|
3489
|
+
const globalSettingsPath = path6.join(homedir7(), ".claude", "settings.json");
|
|
3061
3490
|
const projectSettingsPath = path6.join(cwd, ".claude", "settings.json");
|
|
3062
3491
|
return {
|
|
3063
3492
|
global: hasPythonHooks(globalSettingsPath),
|
|
@@ -3123,7 +3552,7 @@ var init_auto_rules = __esm({
|
|
|
3123
3552
|
API_URL4 = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
3124
3553
|
API_KEY4 = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
3125
3554
|
ENABLED6 = process.env.CONTEXTSTREAM_AUTO_RULES !== "false";
|
|
3126
|
-
MARKER_FILE = path6.join(
|
|
3555
|
+
MARKER_FILE = path6.join(homedir7(), ".contextstream", ".auto-rules-ran");
|
|
3127
3556
|
COOLDOWN_MS = 4 * 60 * 60 * 1e3;
|
|
3128
3557
|
isDirectRun6 = process.argv[1]?.includes("auto-rules") || process.argv[2] === "auto-rules";
|
|
3129
3558
|
if (isDirectRun6) {
|