@contextstream/mcp-server 0.4.60 → 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.
@@ -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 fs from "node:fs";
11
- import * as path from "node:path";
12
- import { homedir as homedir2 } from "node:os";
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((resolve2, reject) => {
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
- resolve2();
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(() => resolve2(), 1e3);
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
- | **Every message** | \`context(user_message="...")\` FIRST |
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: CALL CONTEXT EVERY MESSAGE \u{1F6A8}
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
- | 1st message | \`init()\` \u2192 \`context(user_message="<msg>")\` |
397
- | **EVERY message after** | \`context(user_message="<msg>")\` **FIRST** |
398
-
399
- **BEFORE Glob/Grep/Read/Search:** \u2192 \`search(mode="auto")\` FIRST
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
- **This block MUST appear at the start of EVERY response.** Failing to call \`context()\` means missing rules, lessons, and relevant context.
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 \`context()\` is MANDATORY Every Message
404
+ ## Why Default Context-First
407
405
 
408
- \u274C **WRONG:** "I already called init, I don't need context"
409
- \u2705 **CORRECT:** \`context()\` is required EVERY message, not just the first
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()\` gives recent items by TIME. \`context()\` finds items RELEVANT to THIS message.**
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
- **EVERY coding response MUST start with:**
924
- 1. \`init()\` (1st message only) \u2192 then \`context(user_message="<msg>")\`
925
- 2. \`context(user_message="<msg>")\` (EVERY subsequent message)
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
- | Every message after | \`context(user_message="...")\` |
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
- ## Fast Path (Simple Utilities Only)
979
+ ## Read-Only Examples
980
980
 
981
- Skip init/context ONLY for: "list workspaces", "show version", "list reminders"
982
- \u2192 Just call: \`workspace(action="list")\`, \`help(action="version")\`, etc.
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. **NEVER skip init/context** - you will miss critical context
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 EVERY message (including the first):
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 = path.resolve(cwd);
1462
+ let searchDir = path2.resolve(cwd);
1349
1463
  for (let i = 0; i < 5; i++) {
1350
1464
  if (!API_KEY) {
1351
- const mcpPath = path.join(searchDir, ".mcp.json");
1352
- if (fs.existsSync(mcpPath)) {
1465
+ const mcpPath = path2.join(searchDir, ".mcp.json");
1466
+ if (fs2.existsSync(mcpPath)) {
1353
1467
  try {
1354
- const content = fs.readFileSync(mcpPath, "utf-8");
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 = path.join(searchDir, ".contextstream", "config.json");
1372
- if (fs.existsSync(csConfigPath)) {
1485
+ const csConfigPath = path2.join(searchDir, ".contextstream", "config.json");
1486
+ if (fs2.existsSync(csConfigPath)) {
1373
1487
  try {
1374
- const content = fs.readFileSync(csConfigPath, "utf-8");
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 = path.dirname(searchDir);
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 = path.join(homedir2(), ".mcp.json");
1392
- if (fs.existsSync(homeMcpPath)) {
1505
+ const homeMcpPath = path2.join(homedir3(), ".mcp.json");
1506
+ if (fs2.existsSync(homeMcpPath)) {
1393
1507
  try {
1394
- const content = fs.readFileSync(homeMcpPath, "utf-8");
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('\nNo stored context found. Call `mcp__contextstream__context(user_message="starting new session")` to initialize.');
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('Call `mcp__contextstream__context(user_message="...")` for task-specific context.');
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 = path.join(folderPath, rule.filename);
1512
- if (!fs.existsSync(filePath)) continue;
1629
+ const filePath = path2.join(folderPath, rule.filename);
1630
+ if (!fs2.existsSync(filePath)) continue;
1513
1631
  try {
1514
- const existing = fs.readFileSync(filePath, "utf8");
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
- fs.writeFileSync(filePath, merged.trim() + "\n", "utf8");
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 fs from "node:fs";
5
- import * as path from "node:path";
6
- import { homedir as homedir2 } from "node:os";
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] Call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>") FIRST before any other tool. Response contains dynamic rules, lessons, preferences. For search: use search(mode="auto") if indexed, else local tools.
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>") before ANY other tool
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 = path.resolve(cwd);
326
+ let searchDir = path2.resolve(cwd);
222
327
  for (let i = 0; i < 5; i++) {
223
328
  if (!API_KEY) {
224
- const mcpPath = path.join(searchDir, ".mcp.json");
225
- if (fs.existsSync(mcpPath)) {
329
+ const mcpPath = path2.join(searchDir, ".mcp.json");
330
+ if (fs2.existsSync(mcpPath)) {
226
331
  try {
227
- const content = fs.readFileSync(mcpPath, "utf-8");
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 = path.join(searchDir, ".contextstream", "config.json");
245
- if (fs.existsSync(csConfigPath)) {
349
+ const csConfigPath = path2.join(searchDir, ".contextstream", "config.json");
350
+ if (fs2.existsSync(csConfigPath)) {
246
351
  try {
247
- const content = fs.readFileSync(csConfigPath, "utf-8");
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 = path.dirname(searchDir);
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 = path.join(homedir2(), ".mcp.json");
265
- if (fs.existsSync(homeMcpPath)) {
369
+ const homeMcpPath = path2.join(homedir3(), ".mcp.json");
370
+ if (fs2.existsSync(homeMcpPath)) {
266
371
  try {
267
- const content = fs.readFileSync(homeMcpPath, "utf-8");
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 = fs.readFileSync(transcriptPath, "utf-8");
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;