@contextstream/mcp-server 0.4.61 → 0.4.62
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 +1 -1
- package/dist/hooks/post-write.js +1 -1
- package/dist/hooks/pre-tool-use.js +308 -32
- package/dist/hooks/runner.js +714 -380
- package/dist/hooks/session-init.js +168 -48
- package/dist/hooks/user-prompt-submit.js +127 -17
- package/dist/index.js +1711 -412
- package/package.json +1 -1
|
@@ -7,9 +7,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/hooks/session-init.ts
|
|
10
|
-
import * as
|
|
11
|
-
import * as
|
|
12
|
-
import { homedir as
|
|
10
|
+
import * as fs2 from "node:fs";
|
|
11
|
+
import * as path2 from "node:path";
|
|
12
|
+
import { homedir as homedir3 } from "node:os";
|
|
13
13
|
|
|
14
14
|
// src/version.ts
|
|
15
15
|
import { createRequire } from "module";
|
|
@@ -247,7 +247,7 @@ function detectUpdateMethod() {
|
|
|
247
247
|
return "curl";
|
|
248
248
|
}
|
|
249
249
|
async function runUpdate(method) {
|
|
250
|
-
return new Promise((
|
|
250
|
+
return new Promise((resolve3, reject) => {
|
|
251
251
|
let command;
|
|
252
252
|
let args;
|
|
253
253
|
let shell;
|
|
@@ -278,13 +278,13 @@ async function runUpdate(method) {
|
|
|
278
278
|
});
|
|
279
279
|
proc.on("close", (code) => {
|
|
280
280
|
if (code === 0) {
|
|
281
|
-
|
|
281
|
+
resolve3();
|
|
282
282
|
} else {
|
|
283
283
|
reject(new Error(`Update process exited with code ${code}`));
|
|
284
284
|
}
|
|
285
285
|
});
|
|
286
286
|
proc.unref();
|
|
287
|
-
setTimeout(() =>
|
|
287
|
+
setTimeout(() => resolve3(), 1e3);
|
|
288
288
|
});
|
|
289
289
|
}
|
|
290
290
|
function writeUpdateMarker(previousVersion, newVersion) {
|
|
@@ -372,7 +372,7 @@ var CONTEXTSTREAM_RULES_BOOTSTRAP = `
|
|
|
372
372
|
| Message | Required |
|
|
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
376
|
| **Before file search** | \`search(mode="auto")\` BEFORE Glob/Grep/Read |
|
|
377
377
|
</contextstream_rules>
|
|
378
378
|
|
|
@@ -386,27 +386,25 @@ v${RULES_VERSION}
|
|
|
386
386
|
`.trim();
|
|
387
387
|
var CONTEXTSTREAM_RULES_DYNAMIC = CONTEXTSTREAM_RULES_BOOTSTRAP;
|
|
388
388
|
var CONTEXTSTREAM_RULES_FULL = `
|
|
389
|
-
## \u{1F6A8} MANDATORY:
|
|
389
|
+
## \u{1F6A8} MANDATORY STARTUP: CONTEXT-FIRST FLOW \u{1F6A8}
|
|
390
390
|
|
|
391
391
|
<contextstream_rules>
|
|
392
|
-
**EVERY response MUST start with \`context(user_message="...")\`**
|
|
393
|
-
|
|
394
392
|
| Message | What to Call |
|
|
395
393
|
|---------|--------------|
|
|
396
|
-
|
|
|
397
|
-
| **
|
|
398
|
-
|
|
399
|
-
**
|
|
394
|
+
| **First message in session** | \`init()\` \u2192 \`context(user_message="<msg>")\` BEFORE any other tool |
|
|
395
|
+
| **Subsequent messages (default)** | \`context(user_message="<msg>")\` FIRST, then other tools |
|
|
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 |
|
|
400
398
|
</contextstream_rules>
|
|
401
399
|
|
|
402
|
-
|
|
400
|
+
Use \`context()\` by default to get task-specific rules, lessons from past mistakes, and relevant decisions.
|
|
403
401
|
|
|
404
402
|
---
|
|
405
403
|
|
|
406
|
-
## Why
|
|
404
|
+
## Why Default Context-First
|
|
407
405
|
|
|
408
|
-
\u274C **
|
|
409
|
-
\u2705 **
|
|
406
|
+
\u274C **Wrong:** "I already called init, so I can skip context for everything"
|
|
407
|
+
\u2705 **Correct:** \`context()\` is the default first call for subsequent messages, with a narrow read-only bypass when context is still fresh and state is unchanged
|
|
410
408
|
|
|
411
409
|
**What you lose without \`context()\`:**
|
|
412
410
|
- Dynamic rules matched to your current task
|
|
@@ -414,7 +412,7 @@ var CONTEXTSTREAM_RULES_FULL = `
|
|
|
414
412
|
- Semantically relevant decisions and context
|
|
415
413
|
- Warnings about risky operations
|
|
416
414
|
|
|
417
|
-
**\`init()\`
|
|
415
|
+
**\`init()\` returns recent items by time. \`context()\` finds items semantically relevant to this message.**
|
|
418
416
|
|
|
419
417
|
---
|
|
420
418
|
|
|
@@ -920,9 +918,10 @@ var CONTEXTSTREAM_RULES_MINIMAL = `
|
|
|
920
918
|
## \u{1F6A8} CRITICAL: These Rules Are Non-Negotiable \u{1F6A8}
|
|
921
919
|
|
|
922
920
|
<contextstream_protocol>
|
|
923
|
-
**
|
|
924
|
-
1. \`init()\`
|
|
925
|
-
2. \`context(user_message="<msg>")\`
|
|
921
|
+
**MANDATORY STARTUP:**
|
|
922
|
+
1. First message in session: \`init()\` \u2192 then \`context(user_message="<msg>")\`
|
|
923
|
+
2. Subsequent messages (default): \`context(user_message="<msg>")\` FIRST
|
|
924
|
+
3. Narrow bypass: immediate read-only ContextStream calls are allowed only when prior context is fresh and no state-changing tool has run
|
|
926
925
|
|
|
927
926
|
**BEFORE Glob/Grep/Read/Search/Explore:**
|
|
928
927
|
\u2192 \`search(mode="auto", query="...")\` FIRST \u2014 local tools ONLY if 0 results
|
|
@@ -952,8 +951,9 @@ Rules Version: ${RULES_VERSION}
|
|
|
952
951
|
|
|
953
952
|
| When | Call |
|
|
954
953
|
|------|------|
|
|
955
|
-
| 1st message | \`init()\` \u2192 \`context(user_message="...")\` |
|
|
956
|
-
|
|
|
954
|
+
| 1st message in session | \`init()\` \u2192 \`context(user_message="...")\` |
|
|
955
|
+
| Subsequent messages (default) | \`context(user_message="...")\` first |
|
|
956
|
+
| Narrow bypass | Immediate read-only ContextStream calls when context is fresh and no state-changing tool has run |
|
|
957
957
|
| Before ANY file discovery | \`search(mode="auto", query="...")\` |
|
|
958
958
|
| On \`<system-reminder>\` | **Follow instructions inside** |
|
|
959
959
|
| Save important decisions | \`session(action="capture", event_type="decision", ...)\` |
|
|
@@ -976,12 +976,10 @@ Rules Version: ${RULES_VERSION}
|
|
|
976
976
|
- **[RULES_NOTICE]** \u2192 Run \`generate_rules()\`
|
|
977
977
|
- **[VERSION_NOTICE]** \u2192 Tell user to update MCP
|
|
978
978
|
|
|
979
|
-
##
|
|
979
|
+
## Read-Only Examples
|
|
980
980
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
Everything else = full protocol (init \u2192 context \u2192 search \u2192 work)
|
|
981
|
+
Default behavior is context-first. Narrow bypass applies only for immediate read-only ContextStream calls when context is fresh and state is unchanged.
|
|
982
|
+
Examples: \`workspace(action="list"|"get")\`, \`help(action="version"|"tools"|"auth")\`, \`project(action="index_status")\`.
|
|
985
983
|
|
|
986
984
|
### Lessons (Past Mistakes)
|
|
987
985
|
|
|
@@ -1070,7 +1068,7 @@ You MUST follow these rules manually - there is no automatic enforcement.
|
|
|
1070
1068
|
- Check for [RULES_NOTICE] - update rules if needed
|
|
1071
1069
|
- **save_exchange=true** saves each conversation turn for later retrieval
|
|
1072
1070
|
|
|
1073
|
-
4. **
|
|
1071
|
+
4. **Default behavior:** call \`context(...)\` first on each message. Narrow bypass is allowed only for immediate read-only ContextStream calls when previous context is still fresh and no state-changing tool has run.
|
|
1074
1072
|
|
|
1075
1073
|
---
|
|
1076
1074
|
|
|
@@ -1079,7 +1077,7 @@ You MUST follow these rules manually - there is no automatic enforcement.
|
|
|
1079
1077
|
**This editor does NOT have hooks to auto-save transcripts.**
|
|
1080
1078
|
You MUST save each conversation turn manually:
|
|
1081
1079
|
|
|
1082
|
-
### On
|
|
1080
|
+
### On MOST messages (including the first):
|
|
1083
1081
|
\`\`\`
|
|
1084
1082
|
context(user_message="<user's message>", save_exchange=true, session_id="<session-id>")
|
|
1085
1083
|
\`\`\`
|
|
@@ -1154,6 +1152,28 @@ search(mode="auto", query="what you're looking for")
|
|
|
1154
1152
|
**IF ContextStream search returns 0 results or errors:**
|
|
1155
1153
|
\u2192 Use local tools (Glob/Grep/Read) as fallback
|
|
1156
1154
|
|
|
1155
|
+
### Choose Search Mode Intelligently:
|
|
1156
|
+
- \`auto\` (recommended): query-aware mode selection
|
|
1157
|
+
- \`hybrid\`: mixed semantic + keyword retrieval for broad discovery
|
|
1158
|
+
- \`semantic\`: conceptual questions ("how does X work?")
|
|
1159
|
+
- \`keyword\`: exact text / quoted string
|
|
1160
|
+
- \`pattern\`: glob or regex (\`*.ts\`, \`foo\\s+bar\`)
|
|
1161
|
+
- \`refactor\`: symbol usage / rename-safe lookup
|
|
1162
|
+
- \`exhaustive\`: all occurrences / complete match coverage
|
|
1163
|
+
- \`team\`: cross-project team search
|
|
1164
|
+
|
|
1165
|
+
### Output Format Hints:
|
|
1166
|
+
- Use \`output_format="paths"\` for file listings and rename targets
|
|
1167
|
+
- Use \`output_format="count"\` for "how many" queries
|
|
1168
|
+
|
|
1169
|
+
### Two-Phase Search Pattern (for precision):
|
|
1170
|
+
- Pass 1 (discovery): \`search(mode="auto", query="<concept + module>", output_format="paths", limit=10)\`
|
|
1171
|
+
- Pass 2 (precision): use one of:
|
|
1172
|
+
- exact text/symbol: \`search(mode="keyword", query="\\"exact_text\\"", include_content=true)\`
|
|
1173
|
+
- symbol usage: \`search(mode="refactor", query="SymbolName", output_format="paths")\`
|
|
1174
|
+
- all occurrences: \`search(mode="exhaustive", query="symbol_or_text")\`
|
|
1175
|
+
- Then use local Read/Grep only on paths returned by ContextStream.
|
|
1176
|
+
|
|
1157
1177
|
### When Local Tools Are OK:
|
|
1158
1178
|
\u2705 Project is not indexed
|
|
1159
1179
|
\u2705 Index is stale/outdated (>7 days old)
|
|
@@ -1338,6 +1358,100 @@ ${options.workspaceId ? `# Workspace ID: ${options.workspaceId}` : ""}
|
|
|
1338
1358
|
};
|
|
1339
1359
|
}
|
|
1340
1360
|
|
|
1361
|
+
// src/hooks/prompt-state.ts
|
|
1362
|
+
import * as fs from "node:fs";
|
|
1363
|
+
import * as path from "node:path";
|
|
1364
|
+
import { homedir as homedir2 } from "node:os";
|
|
1365
|
+
var STATE_PATH = path.join(homedir2(), ".contextstream", "prompt-state.json");
|
|
1366
|
+
function defaultState() {
|
|
1367
|
+
return { workspaces: {} };
|
|
1368
|
+
}
|
|
1369
|
+
function nowIso() {
|
|
1370
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1371
|
+
}
|
|
1372
|
+
function ensureStateDir() {
|
|
1373
|
+
try {
|
|
1374
|
+
fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
|
|
1375
|
+
} catch {
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function normalizePath(input) {
|
|
1379
|
+
try {
|
|
1380
|
+
return path.resolve(input);
|
|
1381
|
+
} catch {
|
|
1382
|
+
return input;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
function workspacePathsMatch(a, b) {
|
|
1386
|
+
const left = normalizePath(a);
|
|
1387
|
+
const right = normalizePath(b);
|
|
1388
|
+
return left === right || left.startsWith(`${right}${path.sep}`) || right.startsWith(`${left}${path.sep}`);
|
|
1389
|
+
}
|
|
1390
|
+
function readState() {
|
|
1391
|
+
try {
|
|
1392
|
+
const content = fs.readFileSync(STATE_PATH, "utf8");
|
|
1393
|
+
const parsed = JSON.parse(content);
|
|
1394
|
+
if (!parsed || typeof parsed !== "object" || !parsed.workspaces) {
|
|
1395
|
+
return defaultState();
|
|
1396
|
+
}
|
|
1397
|
+
return parsed;
|
|
1398
|
+
} catch {
|
|
1399
|
+
return defaultState();
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function writeState(state) {
|
|
1403
|
+
try {
|
|
1404
|
+
ensureStateDir();
|
|
1405
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf8");
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function getOrCreateEntry(state, cwd) {
|
|
1410
|
+
if (!cwd.trim()) return null;
|
|
1411
|
+
const exact = state.workspaces[cwd];
|
|
1412
|
+
if (exact) return { key: cwd, entry: exact };
|
|
1413
|
+
for (const [trackedCwd, trackedEntry] of Object.entries(state.workspaces)) {
|
|
1414
|
+
if (workspacePathsMatch(trackedCwd, cwd)) {
|
|
1415
|
+
return { key: trackedCwd, entry: trackedEntry };
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
const created = {
|
|
1419
|
+
require_context: false,
|
|
1420
|
+
require_init: false,
|
|
1421
|
+
last_context_at: void 0,
|
|
1422
|
+
last_state_change_at: void 0,
|
|
1423
|
+
updated_at: nowIso()
|
|
1424
|
+
};
|
|
1425
|
+
state.workspaces[cwd] = created;
|
|
1426
|
+
return { key: cwd, entry: created };
|
|
1427
|
+
}
|
|
1428
|
+
function cleanupStale(maxAgeSeconds) {
|
|
1429
|
+
const state = readState();
|
|
1430
|
+
const now = Date.now();
|
|
1431
|
+
let changed = false;
|
|
1432
|
+
for (const [cwd, entry] of Object.entries(state.workspaces)) {
|
|
1433
|
+
const updated = new Date(entry.updated_at);
|
|
1434
|
+
if (Number.isNaN(updated.getTime())) continue;
|
|
1435
|
+
const ageSeconds = (now - updated.getTime()) / 1e3;
|
|
1436
|
+
if (ageSeconds > maxAgeSeconds) {
|
|
1437
|
+
delete state.workspaces[cwd];
|
|
1438
|
+
changed = true;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (changed) {
|
|
1442
|
+
writeState(state);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
function markInitRequired(cwd) {
|
|
1446
|
+
if (!cwd.trim()) return;
|
|
1447
|
+
const state = readState();
|
|
1448
|
+
const target = getOrCreateEntry(state, cwd);
|
|
1449
|
+
if (!target) return;
|
|
1450
|
+
target.entry.require_init = true;
|
|
1451
|
+
target.entry.updated_at = nowIso();
|
|
1452
|
+
writeState(state);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1341
1455
|
// src/hooks/session-init.ts
|
|
1342
1456
|
var ENABLED = process.env.CONTEXTSTREAM_SESSION_INIT_ENABLED !== "false";
|
|
1343
1457
|
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
@@ -1345,13 +1459,13 @@ var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
|
1345
1459
|
var WORKSPACE_ID = null;
|
|
1346
1460
|
var PROJECT_ID = null;
|
|
1347
1461
|
function loadConfigFromMcpJson(cwd) {
|
|
1348
|
-
let searchDir =
|
|
1462
|
+
let searchDir = path2.resolve(cwd);
|
|
1349
1463
|
for (let i = 0; i < 5; i++) {
|
|
1350
1464
|
if (!API_KEY) {
|
|
1351
|
-
const mcpPath =
|
|
1352
|
-
if (
|
|
1465
|
+
const mcpPath = path2.join(searchDir, ".mcp.json");
|
|
1466
|
+
if (fs2.existsSync(mcpPath)) {
|
|
1353
1467
|
try {
|
|
1354
|
-
const content =
|
|
1468
|
+
const content = fs2.readFileSync(mcpPath, "utf-8");
|
|
1355
1469
|
const config = JSON.parse(content);
|
|
1356
1470
|
const csEnv = config.mcpServers?.contextstream?.env;
|
|
1357
1471
|
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
@@ -1368,10 +1482,10 @@ function loadConfigFromMcpJson(cwd) {
|
|
|
1368
1482
|
}
|
|
1369
1483
|
}
|
|
1370
1484
|
if (!WORKSPACE_ID || !PROJECT_ID) {
|
|
1371
|
-
const csConfigPath =
|
|
1372
|
-
if (
|
|
1485
|
+
const csConfigPath = path2.join(searchDir, ".contextstream", "config.json");
|
|
1486
|
+
if (fs2.existsSync(csConfigPath)) {
|
|
1373
1487
|
try {
|
|
1374
|
-
const content =
|
|
1488
|
+
const content = fs2.readFileSync(csConfigPath, "utf-8");
|
|
1375
1489
|
const csConfig = JSON.parse(content);
|
|
1376
1490
|
if (csConfig.workspace_id && !WORKSPACE_ID) {
|
|
1377
1491
|
WORKSPACE_ID = csConfig.workspace_id;
|
|
@@ -1383,15 +1497,15 @@ function loadConfigFromMcpJson(cwd) {
|
|
|
1383
1497
|
}
|
|
1384
1498
|
}
|
|
1385
1499
|
}
|
|
1386
|
-
const parentDir =
|
|
1500
|
+
const parentDir = path2.dirname(searchDir);
|
|
1387
1501
|
if (parentDir === searchDir) break;
|
|
1388
1502
|
searchDir = parentDir;
|
|
1389
1503
|
}
|
|
1390
1504
|
if (!API_KEY) {
|
|
1391
|
-
const homeMcpPath =
|
|
1392
|
-
if (
|
|
1505
|
+
const homeMcpPath = path2.join(homedir3(), ".mcp.json");
|
|
1506
|
+
if (fs2.existsSync(homeMcpPath)) {
|
|
1393
1507
|
try {
|
|
1394
|
-
const content =
|
|
1508
|
+
const content = fs2.readFileSync(homeMcpPath, "utf-8");
|
|
1395
1509
|
const config = JSON.parse(content);
|
|
1396
1510
|
const csEnv = config.mcpServers?.contextstream?.env;
|
|
1397
1511
|
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
@@ -1469,7 +1583,9 @@ function formatContext(ctx, options = {}) {
|
|
|
1469
1583
|
}
|
|
1470
1584
|
}
|
|
1471
1585
|
if (!ctx) {
|
|
1472
|
-
parts.push(
|
|
1586
|
+
parts.push(
|
|
1587
|
+
'\nNo saved context found yet. On the first message in this session call `mcp__contextstream__init(...)` then `mcp__contextstream__context(user_message="starting new session")`.'
|
|
1588
|
+
);
|
|
1473
1589
|
return parts.join("\n");
|
|
1474
1590
|
}
|
|
1475
1591
|
if (ctx.lessons && ctx.lessons.length > 0) {
|
|
@@ -1497,7 +1613,9 @@ function formatContext(ctx, options = {}) {
|
|
|
1497
1613
|
}
|
|
1498
1614
|
}
|
|
1499
1615
|
parts.push("\n---");
|
|
1500
|
-
parts.push(
|
|
1616
|
+
parts.push(
|
|
1617
|
+
'On the first message in a new session call `mcp__contextstream__init(...)` then `mcp__contextstream__context(user_message="...")`. After that, call `mcp__contextstream__context(user_message="...")` on every message.'
|
|
1618
|
+
);
|
|
1501
1619
|
return parts.join("\n");
|
|
1502
1620
|
}
|
|
1503
1621
|
var CONTEXTSTREAM_START_MARKER = "<!-- BEGIN ContextStream -->";
|
|
@@ -1508,10 +1626,10 @@ function regenerateRuleFiles(folderPath) {
|
|
|
1508
1626
|
for (const editor of editors) {
|
|
1509
1627
|
const rule = generateRuleContent(editor, { mode: "bootstrap" });
|
|
1510
1628
|
if (!rule) continue;
|
|
1511
|
-
const filePath =
|
|
1512
|
-
if (!
|
|
1629
|
+
const filePath = path2.join(folderPath, rule.filename);
|
|
1630
|
+
if (!fs2.existsSync(filePath)) continue;
|
|
1513
1631
|
try {
|
|
1514
|
-
const existing =
|
|
1632
|
+
const existing = fs2.readFileSync(filePath, "utf8");
|
|
1515
1633
|
const startIdx = existing.indexOf(CONTEXTSTREAM_START_MARKER);
|
|
1516
1634
|
const endIdx = existing.indexOf(CONTEXTSTREAM_END_MARKER);
|
|
1517
1635
|
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) continue;
|
|
@@ -1521,7 +1639,7 @@ function regenerateRuleFiles(folderPath) {
|
|
|
1521
1639
|
${rule.content.trim()}
|
|
1522
1640
|
${CONTEXTSTREAM_END_MARKER}`;
|
|
1523
1641
|
const merged = [before, newBlock, after].filter((p) => p.length > 0).join("\n\n");
|
|
1524
|
-
|
|
1642
|
+
fs2.writeFileSync(filePath, merged.trim() + "\n", "utf8");
|
|
1525
1643
|
updated++;
|
|
1526
1644
|
} catch {
|
|
1527
1645
|
}
|
|
@@ -1546,6 +1664,8 @@ async function runSessionInitHook() {
|
|
|
1546
1664
|
process.exit(0);
|
|
1547
1665
|
}
|
|
1548
1666
|
const cwd = input.cwd || process.cwd();
|
|
1667
|
+
cleanupStale(360);
|
|
1668
|
+
markInitRequired(cwd);
|
|
1549
1669
|
loadConfigFromMcpJson(cwd);
|
|
1550
1670
|
const updateMarker = checkUpdateMarker();
|
|
1551
1671
|
if (updateMarker) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/hooks/user-prompt-submit.ts
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import { homedir as
|
|
4
|
+
import * as fs2 from "node:fs";
|
|
5
|
+
import * as path2 from "node:path";
|
|
6
|
+
import { homedir as homedir3 } from "node:os";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
9
|
import { createRequire } from "module";
|
|
@@ -173,21 +173,126 @@ When mentioning the update, provide these commands (user can choose their prefer
|
|
|
173
173
|
Be helpful but not annoying - frame it positively as access to new capabilities rather than criticism.`;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
// src/hooks/prompt-state.ts
|
|
177
|
+
import * as fs from "node:fs";
|
|
178
|
+
import * as path from "node:path";
|
|
179
|
+
import { homedir as homedir2 } from "node:os";
|
|
180
|
+
var STATE_PATH = path.join(homedir2(), ".contextstream", "prompt-state.json");
|
|
181
|
+
function defaultState() {
|
|
182
|
+
return { workspaces: {} };
|
|
183
|
+
}
|
|
184
|
+
function nowIso() {
|
|
185
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
186
|
+
}
|
|
187
|
+
function ensureStateDir() {
|
|
188
|
+
try {
|
|
189
|
+
fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function normalizePath(input) {
|
|
194
|
+
try {
|
|
195
|
+
return path.resolve(input);
|
|
196
|
+
} catch {
|
|
197
|
+
return input;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function workspacePathsMatch(a, b) {
|
|
201
|
+
const left = normalizePath(a);
|
|
202
|
+
const right = normalizePath(b);
|
|
203
|
+
return left === right || left.startsWith(`${right}${path.sep}`) || right.startsWith(`${left}${path.sep}`);
|
|
204
|
+
}
|
|
205
|
+
function readState() {
|
|
206
|
+
try {
|
|
207
|
+
const content = fs.readFileSync(STATE_PATH, "utf8");
|
|
208
|
+
const parsed = JSON.parse(content);
|
|
209
|
+
if (!parsed || typeof parsed !== "object" || !parsed.workspaces) {
|
|
210
|
+
return defaultState();
|
|
211
|
+
}
|
|
212
|
+
return parsed;
|
|
213
|
+
} catch {
|
|
214
|
+
return defaultState();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function writeState(state) {
|
|
218
|
+
try {
|
|
219
|
+
ensureStateDir();
|
|
220
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf8");
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function getOrCreateEntry(state, cwd) {
|
|
225
|
+
if (!cwd.trim()) return null;
|
|
226
|
+
const exact = state.workspaces[cwd];
|
|
227
|
+
if (exact) return { key: cwd, entry: exact };
|
|
228
|
+
for (const [trackedCwd, trackedEntry] of Object.entries(state.workspaces)) {
|
|
229
|
+
if (workspacePathsMatch(trackedCwd, cwd)) {
|
|
230
|
+
return { key: trackedCwd, entry: trackedEntry };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const created = {
|
|
234
|
+
require_context: false,
|
|
235
|
+
require_init: false,
|
|
236
|
+
last_context_at: void 0,
|
|
237
|
+
last_state_change_at: void 0,
|
|
238
|
+
updated_at: nowIso()
|
|
239
|
+
};
|
|
240
|
+
state.workspaces[cwd] = created;
|
|
241
|
+
return { key: cwd, entry: created };
|
|
242
|
+
}
|
|
243
|
+
function cleanupStale(maxAgeSeconds) {
|
|
244
|
+
const state = readState();
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
let changed = false;
|
|
247
|
+
for (const [cwd, entry] of Object.entries(state.workspaces)) {
|
|
248
|
+
const updated = new Date(entry.updated_at);
|
|
249
|
+
if (Number.isNaN(updated.getTime())) continue;
|
|
250
|
+
const ageSeconds = (now - updated.getTime()) / 1e3;
|
|
251
|
+
if (ageSeconds > maxAgeSeconds) {
|
|
252
|
+
delete state.workspaces[cwd];
|
|
253
|
+
changed = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (changed) {
|
|
257
|
+
writeState(state);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function markContextRequired(cwd) {
|
|
261
|
+
if (!cwd.trim()) return;
|
|
262
|
+
const state = readState();
|
|
263
|
+
const target = getOrCreateEntry(state, cwd);
|
|
264
|
+
if (!target) return;
|
|
265
|
+
target.entry.require_context = true;
|
|
266
|
+
target.entry.updated_at = nowIso();
|
|
267
|
+
writeState(state);
|
|
268
|
+
}
|
|
269
|
+
function markInitRequired(cwd) {
|
|
270
|
+
if (!cwd.trim()) return;
|
|
271
|
+
const state = readState();
|
|
272
|
+
const target = getOrCreateEntry(state, cwd);
|
|
273
|
+
if (!target) return;
|
|
274
|
+
target.entry.require_init = true;
|
|
275
|
+
target.entry.updated_at = nowIso();
|
|
276
|
+
writeState(state);
|
|
277
|
+
}
|
|
278
|
+
|
|
176
279
|
// src/hooks/user-prompt-submit.ts
|
|
177
280
|
var ENABLED = process.env.CONTEXTSTREAM_REMINDER_ENABLED !== "false";
|
|
178
281
|
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
179
282
|
var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
180
283
|
var WORKSPACE_ID = null;
|
|
181
284
|
var PROJECT_ID = null;
|
|
182
|
-
var REMINDER = `[CONTEXTSTREAM]
|
|
285
|
+
var REMINDER = `[CONTEXTSTREAM] On the first message in every session call mcp__contextstream__init(...), then call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>") FIRST before any other tool. On subsequent messages, default to context first. Narrow bypass is allowed only for immediate read-only ContextStream calls when prior context is fresh and no state-changing tool has run. Response contains dynamic rules, lessons, preferences.
|
|
286
|
+
COMMON MEMORY CALLS: list docs via memory(action="list_docs"), list lessons via session(action="get_lessons"), list plans via session(action="list_plans"), list tasks/todos via memory(action="list_tasks"|"list_todos").
|
|
183
287
|
[END]`;
|
|
184
288
|
var FULL_REMINDER = `[CONTEXTSTREAM RULES - MANDATORY]
|
|
185
289
|
|
|
186
|
-
1. FIRST: Call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>")
|
|
290
|
+
1. FIRST MESSAGE IN SESSION: Call mcp__contextstream__init(...) then mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>")
|
|
187
291
|
- Returns: dynamic rules, lessons from past mistakes, relevant context
|
|
188
292
|
- Check response for: [LESSONS_WARNING], [RULES_NOTICE], preferences
|
|
189
293
|
- save_exchange=true saves each conversation turn for later retrieval
|
|
190
294
|
- Use a consistent session_id for the entire conversation (generate once on first message)
|
|
295
|
+
- On subsequent messages, default to context() first. Narrow bypass: immediate read-only ContextStream calls when context is fresh and no state-changing tool has run.
|
|
191
296
|
|
|
192
297
|
2. FOR CODE SEARCH: Check index status, then search appropriately
|
|
193
298
|
\u26A0\uFE0F BEFORE searching: mcp__contextstream__project(action="index_status")
|
|
@@ -218,13 +323,13 @@ var ENHANCED_REMINDER_HEADER = `\u2B21 ContextStream \u2014 Smart Context & Memo
|
|
|
218
323
|
|
|
219
324
|
`;
|
|
220
325
|
function loadConfigFromMcpJson(cwd) {
|
|
221
|
-
let searchDir =
|
|
326
|
+
let searchDir = path2.resolve(cwd);
|
|
222
327
|
for (let i = 0; i < 5; i++) {
|
|
223
328
|
if (!API_KEY) {
|
|
224
|
-
const mcpPath =
|
|
225
|
-
if (
|
|
329
|
+
const mcpPath = path2.join(searchDir, ".mcp.json");
|
|
330
|
+
if (fs2.existsSync(mcpPath)) {
|
|
226
331
|
try {
|
|
227
|
-
const content =
|
|
332
|
+
const content = fs2.readFileSync(mcpPath, "utf-8");
|
|
228
333
|
const config = JSON.parse(content);
|
|
229
334
|
const csEnv = config.mcpServers?.contextstream?.env;
|
|
230
335
|
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
@@ -241,10 +346,10 @@ function loadConfigFromMcpJson(cwd) {
|
|
|
241
346
|
}
|
|
242
347
|
}
|
|
243
348
|
if (!WORKSPACE_ID || !PROJECT_ID) {
|
|
244
|
-
const csConfigPath =
|
|
245
|
-
if (
|
|
349
|
+
const csConfigPath = path2.join(searchDir, ".contextstream", "config.json");
|
|
350
|
+
if (fs2.existsSync(csConfigPath)) {
|
|
246
351
|
try {
|
|
247
|
-
const content =
|
|
352
|
+
const content = fs2.readFileSync(csConfigPath, "utf-8");
|
|
248
353
|
const csConfig = JSON.parse(content);
|
|
249
354
|
if (csConfig.workspace_id && !WORKSPACE_ID) {
|
|
250
355
|
WORKSPACE_ID = csConfig.workspace_id;
|
|
@@ -256,15 +361,15 @@ function loadConfigFromMcpJson(cwd) {
|
|
|
256
361
|
}
|
|
257
362
|
}
|
|
258
363
|
}
|
|
259
|
-
const parentDir =
|
|
364
|
+
const parentDir = path2.dirname(searchDir);
|
|
260
365
|
if (parentDir === searchDir) break;
|
|
261
366
|
searchDir = parentDir;
|
|
262
367
|
}
|
|
263
368
|
if (!API_KEY) {
|
|
264
|
-
const homeMcpPath =
|
|
265
|
-
if (
|
|
369
|
+
const homeMcpPath = path2.join(homedir3(), ".mcp.json");
|
|
370
|
+
if (fs2.existsSync(homeMcpPath)) {
|
|
266
371
|
try {
|
|
267
|
-
const content =
|
|
372
|
+
const content = fs2.readFileSync(homeMcpPath, "utf-8");
|
|
268
373
|
const config = JSON.parse(content);
|
|
269
374
|
const csEnv = config.mcpServers?.contextstream?.env;
|
|
270
375
|
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
@@ -280,7 +385,7 @@ function loadConfigFromMcpJson(cwd) {
|
|
|
280
385
|
}
|
|
281
386
|
function readTranscriptFile(transcriptPath) {
|
|
282
387
|
try {
|
|
283
|
-
const content =
|
|
388
|
+
const content = fs2.readFileSync(transcriptPath, "utf-8");
|
|
284
389
|
const lines = content.trim().split("\n");
|
|
285
390
|
const messages = [];
|
|
286
391
|
for (const line of lines) {
|
|
@@ -688,6 +793,11 @@ async function runUserPromptSubmitHook() {
|
|
|
688
793
|
}
|
|
689
794
|
const editorFormat = detectEditorFormat(input);
|
|
690
795
|
const cwd = input.cwd || process.cwd();
|
|
796
|
+
cleanupStale(180);
|
|
797
|
+
markContextRequired(cwd);
|
|
798
|
+
if (isNewSession(input, editorFormat)) {
|
|
799
|
+
markInitRequired(cwd);
|
|
800
|
+
}
|
|
691
801
|
if (editorFormat === "claude") {
|
|
692
802
|
loadConfigFromMcpJson(cwd);
|
|
693
803
|
let context = REMINDER;
|