@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.
@@ -74,7 +74,7 @@ var INDEXABLE_EXTENSIONS = /* @__PURE__ */ new Set([
74
74
  ".prisma",
75
75
  ".proto"
76
76
  ]);
77
- var MAX_FILE_SIZE = 1024 * 1024;
77
+ var MAX_FILE_SIZE = 5 * 1024 * 1024;
78
78
  function extractFilePath(input) {
79
79
  if (input.tool_input) {
80
80
  const filePath = input.tool_input.file_path || input.tool_input.notebook_path || input.tool_input.path;
@@ -274,6 +274,13 @@ async function indexFile(filePath, projectId, apiUrl, apiKey, projectRoot) {
274
274
  if (!response.ok) {
275
275
  throw new Error(`API error: ${response.status} ${response.statusText}`);
276
276
  }
277
+ try {
278
+ const body = await response.json();
279
+ if (body?.data?.status === "cooldown" || body?.data?.status === "daily_limit_exceeded") {
280
+ return;
281
+ }
282
+ } catch {
283
+ }
277
284
  }
278
285
  function findProjectRoot(filePath) {
279
286
  let currentDir = path.dirname(path.resolve(filePath));
@@ -1,13 +1,159 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/hooks/pre-tool-use.ts
4
+ import * as fs2 from "node:fs";
5
+ import * as path2 from "node:path";
6
+ import { homedir as homedir2 } from "node:os";
7
+
8
+ // src/hooks/prompt-state.ts
4
9
  import * as fs from "node:fs";
5
10
  import * as path from "node:path";
6
11
  import { homedir } from "node:os";
12
+ var STATE_PATH = path.join(homedir(), ".contextstream", "prompt-state.json");
13
+ function defaultState() {
14
+ return { workspaces: {} };
15
+ }
16
+ function nowIso() {
17
+ return (/* @__PURE__ */ new Date()).toISOString();
18
+ }
19
+ function ensureStateDir() {
20
+ try {
21
+ fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
22
+ } catch {
23
+ }
24
+ }
25
+ function normalizePath(input) {
26
+ try {
27
+ return path.resolve(input);
28
+ } catch {
29
+ return input;
30
+ }
31
+ }
32
+ function workspacePathsMatch(a, b) {
33
+ const left = normalizePath(a);
34
+ const right = normalizePath(b);
35
+ return left === right || left.startsWith(`${right}${path.sep}`) || right.startsWith(`${left}${path.sep}`);
36
+ }
37
+ function readState() {
38
+ try {
39
+ const content = fs.readFileSync(STATE_PATH, "utf8");
40
+ const parsed = JSON.parse(content);
41
+ if (!parsed || typeof parsed !== "object" || !parsed.workspaces) {
42
+ return defaultState();
43
+ }
44
+ return parsed;
45
+ } catch {
46
+ return defaultState();
47
+ }
48
+ }
49
+ function writeState(state) {
50
+ try {
51
+ ensureStateDir();
52
+ fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf8");
53
+ } catch {
54
+ }
55
+ }
56
+ function getOrCreateEntry(state, cwd) {
57
+ if (!cwd.trim()) return null;
58
+ const exact = state.workspaces[cwd];
59
+ if (exact) return { key: cwd, entry: exact };
60
+ for (const [trackedCwd, trackedEntry] of Object.entries(state.workspaces)) {
61
+ if (workspacePathsMatch(trackedCwd, cwd)) {
62
+ return { key: trackedCwd, entry: trackedEntry };
63
+ }
64
+ }
65
+ const created = {
66
+ require_context: false,
67
+ require_init: false,
68
+ last_context_at: void 0,
69
+ last_state_change_at: void 0,
70
+ updated_at: nowIso()
71
+ };
72
+ state.workspaces[cwd] = created;
73
+ return { key: cwd, entry: created };
74
+ }
75
+ function cleanupStale(maxAgeSeconds) {
76
+ const state = readState();
77
+ const now = Date.now();
78
+ let changed = false;
79
+ for (const [cwd, entry] of Object.entries(state.workspaces)) {
80
+ const updated = new Date(entry.updated_at);
81
+ if (Number.isNaN(updated.getTime())) continue;
82
+ const ageSeconds = (now - updated.getTime()) / 1e3;
83
+ if (ageSeconds > maxAgeSeconds) {
84
+ delete state.workspaces[cwd];
85
+ changed = true;
86
+ }
87
+ }
88
+ if (changed) {
89
+ writeState(state);
90
+ }
91
+ }
92
+ function clearContextRequired(cwd) {
93
+ if (!cwd.trim()) return;
94
+ const state = readState();
95
+ const target = getOrCreateEntry(state, cwd);
96
+ if (!target) return;
97
+ target.entry.require_context = false;
98
+ target.entry.last_context_at = nowIso();
99
+ target.entry.updated_at = nowIso();
100
+ writeState(state);
101
+ }
102
+ function isContextRequired(cwd) {
103
+ if (!cwd.trim()) return false;
104
+ const state = readState();
105
+ const target = getOrCreateEntry(state, cwd);
106
+ return Boolean(target?.entry.require_context);
107
+ }
108
+ function clearInitRequired(cwd) {
109
+ if (!cwd.trim()) return;
110
+ const state = readState();
111
+ const target = getOrCreateEntry(state, cwd);
112
+ if (!target) return;
113
+ target.entry.require_init = false;
114
+ target.entry.updated_at = nowIso();
115
+ writeState(state);
116
+ }
117
+ function isInitRequired(cwd) {
118
+ if (!cwd.trim()) return false;
119
+ const state = readState();
120
+ const target = getOrCreateEntry(state, cwd);
121
+ return Boolean(target?.entry.require_init);
122
+ }
123
+ function markStateChanged(cwd) {
124
+ if (!cwd.trim()) return;
125
+ const state = readState();
126
+ const target = getOrCreateEntry(state, cwd);
127
+ if (!target) return;
128
+ target.entry.last_state_change_at = nowIso();
129
+ target.entry.updated_at = nowIso();
130
+ writeState(state);
131
+ }
132
+ function isContextFreshAndClean(cwd, maxAgeSeconds) {
133
+ if (!cwd.trim()) return false;
134
+ const state = readState();
135
+ const target = getOrCreateEntry(state, cwd);
136
+ const entry = target?.entry;
137
+ if (!entry?.last_context_at) return false;
138
+ const contextAt = new Date(entry.last_context_at);
139
+ if (Number.isNaN(contextAt.getTime())) return false;
140
+ const ageSeconds = (Date.now() - contextAt.getTime()) / 1e3;
141
+ if (ageSeconds < 0 || ageSeconds > maxAgeSeconds) return false;
142
+ if (entry.last_state_change_at) {
143
+ const changedAt = new Date(entry.last_state_change_at);
144
+ if (!Number.isNaN(changedAt.getTime()) && changedAt.getTime() > contextAt.getTime()) {
145
+ return false;
146
+ }
147
+ }
148
+ return true;
149
+ }
150
+
151
+ // src/hooks/pre-tool-use.ts
7
152
  var ENABLED = process.env.CONTEXTSTREAM_HOOK_ENABLED !== "false";
8
- var INDEX_STATUS_FILE = path.join(homedir(), ".contextstream", "indexed-projects.json");
153
+ var INDEX_STATUS_FILE = path2.join(homedir2(), ".contextstream", "indexed-projects.json");
9
154
  var DEBUG_FILE = "/tmp/pretooluse-hook-debug.log";
10
155
  var STALE_THRESHOLD_DAYS = 7;
156
+ var CONTEXT_FRESHNESS_SECONDS = 120;
11
157
  var DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"];
12
158
  function isDiscoveryGlob(pattern) {
13
159
  const patternLower = pattern.toLowerCase();
@@ -34,22 +180,22 @@ function isDiscoveryGrep(filePath) {
34
180
  return false;
35
181
  }
36
182
  function isProjectIndexed(cwd) {
37
- if (!fs.existsSync(INDEX_STATUS_FILE)) {
183
+ if (!fs2.existsSync(INDEX_STATUS_FILE)) {
38
184
  return { isIndexed: false, isStale: false };
39
185
  }
40
186
  let data;
41
187
  try {
42
- const content = fs.readFileSync(INDEX_STATUS_FILE, "utf-8");
188
+ const content = fs2.readFileSync(INDEX_STATUS_FILE, "utf-8");
43
189
  data = JSON.parse(content);
44
190
  } catch {
45
191
  return { isIndexed: false, isStale: false };
46
192
  }
47
193
  const projects = data.projects || {};
48
- const cwdPath = path.resolve(cwd);
194
+ const cwdPath = path2.resolve(cwd);
49
195
  for (const [projectPath, info] of Object.entries(projects)) {
50
196
  try {
51
- const indexedPath = path.resolve(projectPath);
52
- if (cwdPath === indexedPath || cwdPath.startsWith(indexedPath + path.sep)) {
197
+ const indexedPath = path2.resolve(projectPath);
198
+ if (cwdPath === indexedPath || cwdPath.startsWith(indexedPath + path2.sep)) {
53
199
  const indexedAt = info.indexed_at;
54
200
  if (indexedAt) {
55
201
  try {
@@ -82,6 +228,97 @@ function extractToolName(input) {
82
228
  function extractToolInput(input) {
83
229
  return input.tool_input || input.parameters || input.toolParameters || {};
84
230
  }
231
+ function normalizeContextstreamToolName(toolName) {
232
+ const trimmed = toolName.trim();
233
+ if (!trimmed) return null;
234
+ const lower = trimmed.toLowerCase();
235
+ const prefixed = "mcp__contextstream__";
236
+ if (lower.startsWith(prefixed)) {
237
+ return lower.slice(prefixed.length);
238
+ }
239
+ if (lower.startsWith("contextstream__")) {
240
+ return lower.slice("contextstream__".length);
241
+ }
242
+ if (lower === "init" || lower === "context") {
243
+ return lower;
244
+ }
245
+ return null;
246
+ }
247
+ function actionFromToolInput(toolInput) {
248
+ const maybeAction = toolInput?.action;
249
+ return typeof maybeAction === "string" ? maybeAction.trim().toLowerCase() : "";
250
+ }
251
+ function isContextstreamReadOnlyOperation(toolName, toolInput) {
252
+ const action = actionFromToolInput(toolInput);
253
+ switch (toolName) {
254
+ case "workspace":
255
+ return action === "list" || action === "get";
256
+ case "memory":
257
+ return action === "list_docs" || action === "list_events" || action === "list_todos" || action === "list_tasks" || action === "list_transcripts" || action === "list_nodes" || action === "decisions" || action === "get_doc" || action === "get_event" || action === "get_task" || action === "get_todo" || action === "get_transcript";
258
+ case "session":
259
+ return action === "get_lessons" || action === "get_plan" || action === "list_plans" || action === "recall";
260
+ case "help":
261
+ return action === "version" || action === "tools" || action === "auth";
262
+ case "project":
263
+ return action === "list" || action === "get" || action === "index_status";
264
+ case "reminder":
265
+ return action === "list" || action === "active";
266
+ case "context":
267
+ case "init":
268
+ return true;
269
+ default:
270
+ return false;
271
+ }
272
+ }
273
+ function isLikelyStateChangingTool(toolLower, toolInput, isContextstreamCall, normalizedContextstreamTool) {
274
+ if (isContextstreamCall && normalizedContextstreamTool) {
275
+ return !isContextstreamReadOnlyOperation(normalizedContextstreamTool, toolInput);
276
+ }
277
+ if ([
278
+ "read",
279
+ "read_file",
280
+ "grep",
281
+ "glob",
282
+ "search",
283
+ "grep_search",
284
+ "code_search",
285
+ "semanticsearch",
286
+ "codebase_search",
287
+ "list_files",
288
+ "search_files",
289
+ "search_files_content",
290
+ "find_files",
291
+ "find_by_name",
292
+ "ls",
293
+ "cat",
294
+ "view"
295
+ ].includes(toolLower)) {
296
+ return false;
297
+ }
298
+ const writeMarkers = [
299
+ "write",
300
+ "edit",
301
+ "create",
302
+ "delete",
303
+ "remove",
304
+ "rename",
305
+ "move",
306
+ "patch",
307
+ "apply",
308
+ "insert",
309
+ "append",
310
+ "replace",
311
+ "update",
312
+ "commit",
313
+ "push",
314
+ "install",
315
+ "exec",
316
+ "run",
317
+ "bash",
318
+ "shell"
319
+ ];
320
+ return writeMarkers.some((marker) => toolLower.includes(marker));
321
+ }
85
322
  function blockClaudeCode(message) {
86
323
  const response = {
87
324
  hookSpecificOutput: {
@@ -90,7 +327,7 @@ function blockClaudeCode(message) {
90
327
  additionalContext: `[CONTEXTSTREAM] ${message}`
91
328
  }
92
329
  };
93
- fs.appendFileSync(DEBUG_FILE, `[PreToolUse] REDIRECT (additionalContext): ${JSON.stringify(response)}
330
+ fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] REDIRECT (additionalContext): ${JSON.stringify(response)}
94
331
  `);
95
332
  console.log(JSON.stringify(response));
96
333
  process.exit(0);
@@ -118,6 +355,25 @@ function outputCursorAllow() {
118
355
  console.log(JSON.stringify({ decision: "allow" }));
119
356
  process.exit(0);
120
357
  }
358
+ function blockWithMessage(editorFormat, message) {
359
+ if (editorFormat === "cline") {
360
+ outputClineBlock(message, "[CONTEXTSTREAM] Follow ContextStream startup requirements.");
361
+ } else if (editorFormat === "cursor") {
362
+ outputCursorBlock(message);
363
+ }
364
+ blockClaudeCode(message);
365
+ }
366
+ function allowTool(editorFormat, cwd, recordStateChange) {
367
+ if (recordStateChange) {
368
+ markStateChanged(cwd);
369
+ }
370
+ if (editorFormat === "cline") {
371
+ outputClineAllow();
372
+ } else if (editorFormat === "cursor") {
373
+ outputCursorAllow();
374
+ }
375
+ process.exit(0);
376
+ }
121
377
  function detectEditorFormat(input) {
122
378
  if (input.hookName !== void 0 || input.toolName !== void 0) {
123
379
  return "cline";
@@ -128,11 +384,11 @@ function detectEditorFormat(input) {
128
384
  return "claude";
129
385
  }
130
386
  async function runPreToolUseHook() {
131
- fs.appendFileSync(DEBUG_FILE, `[PreToolUse] Hook invoked at ${(/* @__PURE__ */ new Date()).toISOString()}
387
+ fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] Hook invoked at ${(/* @__PURE__ */ new Date()).toISOString()}
132
388
  `);
133
389
  console.error("[PreToolUse] Hook invoked at", (/* @__PURE__ */ new Date()).toISOString());
134
390
  if (!ENABLED) {
135
- fs.appendFileSync(DEBUG_FILE, "[PreToolUse] Hook disabled, exiting\n");
391
+ fs2.appendFileSync(DEBUG_FILE, "[PreToolUse] Hook disabled, exiting\n");
136
392
  console.error("[PreToolUse] Hook disabled, exiting");
137
393
  process.exit(0);
138
394
  }
@@ -153,28 +409,52 @@ async function runPreToolUseHook() {
153
409
  const cwd = extractCwd(input);
154
410
  const tool = extractToolName(input);
155
411
  const toolInput = extractToolInput(input);
156
- fs.appendFileSync(DEBUG_FILE, `[PreToolUse] tool=${tool}, cwd=${cwd}, editorFormat=${editorFormat}
412
+ const toolLower = tool.toLowerCase();
413
+ const normalizedContextstreamTool = normalizeContextstreamToolName(tool);
414
+ const isContextstreamCall = normalizedContextstreamTool !== null;
415
+ const recordStateChange = isLikelyStateChangingTool(
416
+ toolLower,
417
+ toolInput,
418
+ isContextstreamCall,
419
+ normalizedContextstreamTool
420
+ );
421
+ fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] tool=${tool}, cwd=${cwd}, editorFormat=${editorFormat}
157
422
  `);
423
+ cleanupStale(180);
424
+ if (isInitRequired(cwd)) {
425
+ if (isContextstreamCall && normalizedContextstreamTool === "init") {
426
+ clearInitRequired(cwd);
427
+ } else {
428
+ const required = "mcp__contextstream__init(...)";
429
+ const msg = `First call required for this session: ${required}. Run it before any other MCP tool. Then call mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>").`;
430
+ blockWithMessage(editorFormat, msg);
431
+ }
432
+ }
433
+ if (isContextRequired(cwd)) {
434
+ if (isContextstreamCall && normalizedContextstreamTool === "context") {
435
+ clearContextRequired(cwd);
436
+ } else if (isContextstreamCall && normalizedContextstreamTool === "init") {
437
+ } else if (isContextstreamCall && normalizedContextstreamTool && isContextstreamReadOnlyOperation(normalizedContextstreamTool, toolInput) && isContextFreshAndClean(cwd, CONTEXT_FRESHNESS_SECONDS)) {
438
+ } else {
439
+ const msg = 'First call required for this prompt: mcp__contextstream__context(user_message="...", save_exchange=true, session_id="<session-id>"). Run it before any other MCP tool.';
440
+ blockWithMessage(editorFormat, msg);
441
+ }
442
+ }
158
443
  const { isIndexed } = isProjectIndexed(cwd);
159
- fs.appendFileSync(DEBUG_FILE, `[PreToolUse] isIndexed=${isIndexed}
444
+ fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] isIndexed=${isIndexed}
160
445
  `);
161
446
  if (!isIndexed) {
162
- fs.appendFileSync(DEBUG_FILE, `[PreToolUse] Project not indexed, allowing
447
+ fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] Project not indexed, allowing
163
448
  `);
164
- if (editorFormat === "cline") {
165
- outputClineAllow();
166
- } else if (editorFormat === "cursor") {
167
- outputCursorAllow();
168
- }
169
- process.exit(0);
449
+ allowTool(editorFormat, cwd, recordStateChange);
170
450
  }
171
451
  if (tool === "Glob") {
172
452
  const pattern = toolInput?.pattern || "";
173
- fs.appendFileSync(DEBUG_FILE, `[PreToolUse] Glob pattern=${pattern}, isDiscovery=${isDiscoveryGlob(pattern)}
453
+ fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] Glob pattern=${pattern}, isDiscovery=${isDiscoveryGlob(pattern)}
174
454
  `);
175
455
  if (isDiscoveryGlob(pattern)) {
176
- const msg = `STOP: Use mcp__contextstream__search(mode="auto", query="${pattern}") instead of Glob.`;
177
- fs.appendFileSync(DEBUG_FILE, `[PreToolUse] Intercepting discovery glob: ${msg}
456
+ const msg = `This project index is current. Use mcp__contextstream__search(mode="auto", query="${pattern}") instead of Glob for faster, richer code results.`;
457
+ fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] Intercepting discovery glob: ${msg}
178
458
  `);
179
459
  if (editorFormat === "cline") {
180
460
  outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
@@ -196,7 +476,7 @@ async function runPreToolUseHook() {
196
476
  }
197
477
  blockClaudeCode(msg);
198
478
  } else {
199
- const msg = `STOP: Use mcp__contextstream__search(mode="auto", query="${pattern}") instead of ${tool}.`;
479
+ const msg = `This project index is current. Use mcp__contextstream__search(mode="auto", query="${pattern}") instead of ${tool} for faster, richer code results.`;
200
480
  if (editorFormat === "cline") {
201
481
  outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
202
482
  } else if (editorFormat === "cursor") {
@@ -208,7 +488,7 @@ async function runPreToolUseHook() {
208
488
  } else if (tool === "Task") {
209
489
  const subagentType = toolInput?.subagent_type?.toLowerCase() || "";
210
490
  if (subagentType === "explore") {
211
- const msg = 'STOP: Use mcp__contextstream__search(mode="auto") instead of Task(Explore).';
491
+ const msg = 'Project index is current. Use mcp__contextstream__search(mode="auto") instead of Task(Explore) for broad discovery.';
212
492
  if (editorFormat === "cline") {
213
493
  outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
214
494
  } else if (editorFormat === "cursor") {
@@ -217,7 +497,7 @@ async function runPreToolUseHook() {
217
497
  blockClaudeCode(msg);
218
498
  }
219
499
  if (subagentType === "plan") {
220
- const msg = 'STOP: Use mcp__contextstream__session(action="capture_plan") for planning. ContextStream plans persist across sessions.';
500
+ const msg = 'After your plan is ready, save it with mcp__contextstream__session(action="capture_plan"). Then create tasks with mcp__contextstream__memory(action="create_task", title="...", plan_id="...").';
221
501
  if (editorFormat === "cline") {
222
502
  outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream plans for persistence.");
223
503
  } else if (editorFormat === "cursor") {
@@ -226,7 +506,7 @@ async function runPreToolUseHook() {
226
506
  blockClaudeCode(msg);
227
507
  }
228
508
  } else if (tool === "EnterPlanMode") {
229
- const msg = 'STOP: Use mcp__contextstream__session(action="capture_plan", title="...", steps=[...]) instead of EnterPlanMode. ContextStream plans persist across sessions and are searchable.';
509
+ const msg = 'After finalizing your plan, save it to ContextStream (not a local markdown file): mcp__contextstream__session(action="capture_plan", title="...", steps=[...]). Then create tasks with mcp__contextstream__memory(action="create_task", title="...", plan_id="...").';
230
510
  if (editorFormat === "cline") {
231
511
  outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream plans for persistence.");
232
512
  } else if (editorFormat === "cursor") {
@@ -237,20 +517,16 @@ async function runPreToolUseHook() {
237
517
  if (tool === "list_files" || tool === "search_files") {
238
518
  const pattern = toolInput?.path || toolInput?.regex || "";
239
519
  if (isDiscoveryGlob(pattern) || isDiscoveryGrep(pattern)) {
240
- const msg = `Use mcp__contextstream__search(mode="auto", query="${pattern}") instead of ${tool}. ContextStream search is indexed and faster.`;
520
+ const msg = `Project index is current. Use mcp__contextstream__search(mode="auto", query="${pattern}") instead of ${tool} for faster, richer code results.`;
241
521
  if (editorFormat === "cline") {
242
522
  outputClineBlock(msg, "[CONTEXTSTREAM] Use ContextStream search for code discovery.");
243
523
  } else if (editorFormat === "cursor") {
244
524
  outputCursorBlock(msg);
245
525
  }
526
+ blockClaudeCode(msg);
246
527
  }
247
528
  }
248
- if (editorFormat === "cline") {
249
- outputClineAllow();
250
- } else if (editorFormat === "cursor") {
251
- outputCursorAllow();
252
- }
253
- process.exit(0);
529
+ allowTool(editorFormat, cwd, recordStateChange);
254
530
  }
255
531
  var isDirectRun = process.argv[1]?.includes("pre-tool-use") || process.argv[2] === "pre-tool-use";
256
532
  if (isDirectRun) {