smart-context-mcp 1.16.1 → 1.16.4

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/README.md CHANGED
@@ -56,7 +56,7 @@ Restart your AI client. Done.
56
56
  # Check installed version
57
57
  npm list -g smart-context-mcp
58
58
 
59
- # Should show: smart-context-mcp@1.16.1 (or later)
59
+ # Should show: smart-context-mcp@1.16.4 (or later)
60
60
 
61
61
  # Update to latest version
62
62
  npm update -g smart-context-mcp
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "smart-context-mcp",
3
3
  "mcpName": "io.github.Arrayo/smart-context-mcp",
4
- "version": "1.16.1",
4
+ "version": "1.16.4",
5
5
  "description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
6
6
  "author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
7
7
  "type": "module",
@@ -369,16 +369,18 @@ const updateCodexConfig = (targetDir, serverConfig, dryRun) => {
369
369
  const HOOK_SECTION_START = '# devctx:start';
370
370
  const HOOK_SECTION_END = '# devctx:end';
371
371
 
372
- const buildPreCommitHookSection = (targetDir) => {
373
- const scriptPath = normalizeCommandPath(path.relative(targetDir, path.join(devctxDir, 'scripts', 'check-repo-safety.js')));
372
+ const buildPreCommitHookSection = () => {
374
373
  return `${HOOK_SECTION_START}
375
374
  # Prevent committing project-local devctx state.
376
375
  repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
377
- "${process.execPath}" "${scriptPath}" --project-root "$repo_root"
378
- status=$?
379
- if [ "$status" -ne 0 ]; then
380
- echo "devctx: commit blocked by repo safety checks." >&2
381
- exit "$status"
376
+ devctx_script="$(npm root -g 2>/dev/null)/smart-context-mcp/scripts/check-repo-safety.js"
377
+ if [ -f "$devctx_script" ]; then
378
+ node "$devctx_script" --project-root "$repo_root"
379
+ status=$?
380
+ if [ "$status" -ne 0 ]; then
381
+ echo "devctx: commit blocked by repo safety checks." >&2
382
+ exit "$status"
383
+ fi
382
384
  fi
383
385
  ${HOOK_SECTION_END}`;
384
386
  };
@@ -392,7 +394,7 @@ const updatePreCommitHook = (targetDir, dryRun) => {
392
394
 
393
395
  const filePath = path.resolve(targetDir, hookPathResult.stdout);
394
396
  const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
395
- const nextBody = upsertSentinelSection(current, HOOK_SECTION_START, HOOK_SECTION_END, buildPreCommitHookSection(targetDir));
397
+ const nextBody = upsertSentinelSection(current, HOOK_SECTION_START, HOOK_SECTION_END, buildPreCommitHookSection());
396
398
  const nextContent = nextBody.startsWith('#!') ? nextBody : `#!/bin/sh\n\n${nextBody}`;
397
399
  writeFile(filePath, nextContent, dryRun);
398
400
 
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/Arrayo/smart-context-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.16.1",
9
+ "version": "1.16.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "smart-context-mcp",
14
- "version": "1.16.1",
14
+ "version": "1.16.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -74,11 +74,17 @@ export const buildOperationalContextLines = (
74
74
  const summary = result?.summary;
75
75
  const continuityState = result?.continuity?.state;
76
76
  const storageIssue = result?.storageHealth?.issue;
77
+ const task = result?.task ?? null;
78
+ const handoff = result?.handoff ?? null;
77
79
 
78
80
  if (result?.found && summary) {
79
81
  const label = sessionStart ? 'resume' : continuityState ?? 'resume';
80
82
  lines.push(`devctx ${label}: session ${result.sessionId}`);
81
83
 
84
+ if (task?.taskId) {
85
+ lines.push(`task: ${truncate(task.taskId, maxLineLength)}`);
86
+ }
87
+
82
88
  if (summary.goal) {
83
89
  lines.push(`goal: ${truncate(summary.goal, maxLineLength)}`);
84
90
  }
@@ -106,10 +112,20 @@ export const buildOperationalContextLines = (
106
112
  lines.push(`devctx new task session: ${truncate(summary.goal, maxLineLength)}`);
107
113
  }
108
114
 
109
- if (result?.continuity?.reason) {
115
+ if (!mutationSafety?.blocked && result?.continuity?.reason) {
110
116
  lines.push(`context status: ${truncate(result.continuity.reason, maxLineLength)}`);
111
117
  }
112
118
 
119
+ if (handoff?.fromAgentId || handoff?.summary?.nextStep || handoff?.summary?.pending?.length > 0) {
120
+ lines.push(`handoff: ${truncate(`from ${handoff?.fromAgentId ?? 'previous agent'} via ${handoff?.trigger ?? 'handoff'}`, maxLineLength)}`);
121
+ if (handoff?.summary?.pending?.[0]) {
122
+ lines.push(`pending: ${truncate(handoff.summary.pending[0], maxLineLength)}`);
123
+ }
124
+ if (handoff?.summary?.nextStep) {
125
+ lines.push(`handoff next: ${truncate(handoff.summary.nextStep, maxLineLength)}`);
126
+ }
127
+ }
128
+
113
129
  if (mutationSafety?.blocked) {
114
130
  const reasons = mutationSafety.blockedBy?.join(' and ') || 'blocked';
115
131
  lines.push(`repo safety: ${mutationSafety.stateDbPath} is ${reasons}; context writes are blocked.`);
@@ -7,7 +7,9 @@ import { smartTurn } from '../../tools/smart-turn.js';
7
7
  import {
8
8
  deleteHookTurnState,
9
9
  getHookTurnState,
10
+ persistTaskHandoff,
10
11
  setHookTurnState,
12
+ upsertAgentRun,
11
13
  } from '../../storage/sqlite.js';
12
14
  import { DEFAULT_START_MAX_TOKENS, resolveManagedStart } from '../base-orchestrator.js';
13
15
  import { extractNextStep, normalizeWhitespace, truncate } from '../policy/event-policy.js';
@@ -25,8 +27,20 @@ export const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
25
27
 
26
28
  const uniq = (values) => [...new Set(values.filter((value) => typeof value === 'string' && value.trim().length > 0))];
27
29
 
30
+ const resolveAgentId = (input = {}) => {
31
+ const value = input.agent_id ?? input.agentId ?? input.assistant_id ?? input.worker_id ?? null;
32
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : 'main';
33
+ };
34
+
35
+ const resolveParentAgentId = (input = {}) => {
36
+ const value = input.parent_agent_id ?? input.parentAgentId ?? null;
37
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
38
+ };
39
+
28
40
  export const buildClaudeHookKey = ({ sessionId, agentId = null }) =>
29
- agentId ? `${HOOK_CLIENT}:subagent:${sessionId}:${agentId}` : `${HOOK_CLIENT}:main:${sessionId}`;
41
+ agentId && agentId !== 'main'
42
+ ? `${HOOK_CLIENT}:subagent:${sessionId}:${agentId}`
43
+ : `${HOOK_CLIENT}:main:${sessionId}`;
30
44
 
31
45
  const countPromptTerms = (value) =>
32
46
  normalizeWhitespace(value)
@@ -148,6 +162,8 @@ export const createClaudeAdapter = ({
148
162
  summaryTool = smartSummary,
149
163
  resolveStart = resolveManagedStart,
150
164
  persistMetric = persistMetrics,
165
+ writeAgentRun = upsertAgentRun,
166
+ writeTaskHandoff = persistTaskHandoff,
151
167
  getMutationSafety = getRepoMutationSafety,
152
168
  readHookState = null,
153
169
  writeHookState = ({ hookKey, state }) => setHookTurnState({ hookKey, state }),
@@ -215,7 +231,11 @@ export const createClaudeAdapter = ({
215
231
  const maybeTrackTurn = async ({
216
232
  hookKey,
217
233
  claudeSessionId,
234
+ conversationId,
218
235
  projectSessionId,
236
+ taskId,
237
+ agentId,
238
+ parentAgentId,
219
239
  prompt,
220
240
  continuityState,
221
241
  }) => {
@@ -232,7 +252,11 @@ export const createClaudeAdapter = ({
232
252
  state: {
233
253
  client: HOOK_CLIENT,
234
254
  claudeSessionId,
255
+ conversationId,
235
256
  projectSessionId,
257
+ taskId,
258
+ agentId,
259
+ parentAgentId,
236
260
  turnId: `${claudeSessionId}:${Date.now()}`,
237
261
  promptPreview: truncate(prompt, MAX_PROMPT_PREVIEW),
238
262
  continuityState,
@@ -262,6 +286,8 @@ export const createClaudeAdapter = ({
262
286
  };
263
287
 
264
288
  const handleUserPromptSubmit = async (input) => {
289
+ const agentId = resolveAgentId(input);
290
+ const parentAgentId = resolveParentAgentId(input);
265
291
  const startResolution = await resolveStart({
266
292
  prompt: input.prompt,
267
293
  ensureSession: true,
@@ -272,10 +298,27 @@ export const createClaudeAdapter = ({
272
298
  });
273
299
  const result = startResolution.startResult;
274
300
 
301
+ if (!getMutationSafety().shouldBlock) {
302
+ await writeAgentRun({
303
+ runId: buildClaudeHookKey({ sessionId: input.session_id, agentId }),
304
+ taskId: result.task?.taskId ?? null,
305
+ agentId,
306
+ parentAgentId,
307
+ client: HOOK_CLIENT,
308
+ conversationId: input.session_id,
309
+ sessionId: result.sessionId ?? null,
310
+ role: agentId === 'main' ? 'main' : 'subagent',
311
+ });
312
+ }
313
+
275
314
  const trackedState = await maybeTrackTurn({
276
- hookKey: buildClaudeHookKey({ sessionId: input.session_id }),
315
+ hookKey: buildClaudeHookKey({ sessionId: input.session_id, agentId }),
277
316
  claudeSessionId: input.session_id,
317
+ conversationId: input.session_id,
278
318
  projectSessionId: result.sessionId ?? null,
319
+ taskId: result.task?.taskId ?? null,
320
+ agentId,
321
+ parentAgentId,
279
322
  prompt: input.prompt,
280
323
  continuityState: result.continuity?.state ?? '',
281
324
  });
@@ -291,7 +334,7 @@ export const createClaudeAdapter = ({
291
334
  };
292
335
 
293
336
  const handlePostToolUse = async (input) => {
294
- const hookKey = buildClaudeHookKey({ sessionId: input.session_id });
337
+ const hookKey = buildClaudeHookKey({ sessionId: input.session_id, agentId: resolveAgentId(input) });
295
338
  const existing = await readTrackedState(hookKey);
296
339
  if (!existing) {
297
340
  return null;
@@ -329,7 +372,7 @@ export const createClaudeAdapter = ({
329
372
  };
330
373
 
331
374
  const handleStop = async (input) => {
332
- const hookKey = buildClaudeHookKey({ sessionId: input.session_id });
375
+ const hookKey = buildClaudeHookKey({ sessionId: input.session_id, agentId: resolveAgentId(input) });
333
376
  const state = await readTrackedState(hookKey);
334
377
  if (!state) {
335
378
  return null;
@@ -371,6 +414,23 @@ export const createClaudeAdapter = ({
371
414
  });
372
415
  }
373
416
 
417
+ if (state.taskId) {
418
+ await writeTaskHandoff({
419
+ taskId: state.taskId,
420
+ sessionId: state.projectSessionId,
421
+ fromAgentId: state.agentId ?? null,
422
+ toAgentId: null,
423
+ trigger: state.agentId && state.agentId !== 'main' ? 'subagent_delegate' : 'stop',
424
+ summary: {
425
+ currentFocus: update.currentFocus ?? state.promptPreview,
426
+ touchedFiles: state.touchedFiles,
427
+ pending: update.nextStep ? [update.nextStep] : [],
428
+ nextStep: update.nextStep ?? null,
429
+ evidence: state.touchedFiles.length > 0 ? [`Touched files: ${state.touchedFiles.join(', ')}`] : [],
430
+ },
431
+ });
432
+ }
433
+
374
434
  await recordHookMetrics({
375
435
  action: 'Stop',
376
436
  sessionId: state.projectSessionId,
@@ -7,7 +7,9 @@ import { smartTurn } from '../../tools/smart-turn.js';
7
7
  import {
8
8
  deleteHookTurnState,
9
9
  getHookTurnState,
10
+ persistTaskHandoff,
10
11
  setHookTurnState,
12
+ upsertAgentRun,
11
13
  } from '../../storage/sqlite.js';
12
14
  import { DEFAULT_START_MAX_TOKENS, resolveManagedStart } from '../base-orchestrator.js';
13
15
  import { extractNextStep, normalizeWhitespace, truncate } from '../policy/event-policy.js';
@@ -25,8 +27,20 @@ export const WRITE_TOOLS = new Set(['Write', 'StrReplace', 'Delete', 'EditNotebo
25
27
 
26
28
  const uniq = (values) => [...new Set(values.filter((value) => typeof value === 'string' && value.trim().length > 0))];
27
29
 
30
+ const resolveAgentId = (input = {}) => {
31
+ const value = input.agent_id ?? input.agentId ?? input.worker_id ?? null;
32
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : 'main';
33
+ };
34
+
35
+ const resolveParentAgentId = (input = {}) => {
36
+ const value = input.parent_agent_id ?? input.parentAgentId ?? null;
37
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
38
+ };
39
+
28
40
  export const buildCursorHookKey = ({ conversationId, agentId = null }) =>
29
- agentId ? `${HOOK_CLIENT}:subagent:${conversationId}:${agentId}` : `${HOOK_CLIENT}:main:${conversationId}`;
41
+ agentId && agentId !== 'main'
42
+ ? `${HOOK_CLIENT}:subagent:${conversationId}:${agentId}`
43
+ : `${HOOK_CLIENT}:main:${conversationId}`;
30
44
 
31
45
  const countPromptTerms = (value) =>
32
46
  normalizeWhitespace(value)
@@ -151,6 +165,8 @@ export const createCursorAdapter = ({
151
165
  summaryTool = smartSummary,
152
166
  resolveStart = resolveManagedStart,
153
167
  persistMetric = persistMetrics,
168
+ writeAgentRun = upsertAgentRun,
169
+ writeTaskHandoff = persistTaskHandoff,
154
170
  getMutationSafety = getRepoMutationSafety,
155
171
  readHookState = null,
156
172
  writeHookState = ({ hookKey, state }) => setHookTurnState({ hookKey, state }),
@@ -218,7 +234,11 @@ export const createCursorAdapter = ({
218
234
  const maybeTrackTurn = async ({
219
235
  hookKey,
220
236
  cursorConversationId,
237
+ conversationId,
221
238
  projectSessionId,
239
+ taskId,
240
+ agentId,
241
+ parentAgentId,
222
242
  prompt,
223
243
  continuityState,
224
244
  }) => {
@@ -235,7 +255,11 @@ export const createCursorAdapter = ({
235
255
  state: {
236
256
  client: HOOK_CLIENT,
237
257
  cursorConversationId,
258
+ conversationId,
238
259
  projectSessionId,
260
+ taskId,
261
+ agentId,
262
+ parentAgentId,
239
263
  turnId: `${cursorConversationId}:${Date.now()}`,
240
264
  promptPreview: truncate(prompt, MAX_PROMPT_PREVIEW),
241
265
  continuityState,
@@ -265,6 +289,8 @@ export const createCursorAdapter = ({
265
289
  };
266
290
 
267
291
  const handleUserMessageSubmit = async (input) => {
292
+ const agentId = resolveAgentId(input);
293
+ const parentAgentId = resolveParentAgentId(input);
268
294
  const startResolution = await resolveStart({
269
295
  prompt: input.user_message,
270
296
  ensureSession: true,
@@ -275,10 +301,27 @@ export const createCursorAdapter = ({
275
301
  });
276
302
  const result = startResolution.startResult;
277
303
 
304
+ if (!getMutationSafety().shouldBlock) {
305
+ await writeAgentRun({
306
+ runId: buildCursorHookKey({ conversationId: input.conversation_id, agentId }),
307
+ taskId: result.task?.taskId ?? null,
308
+ agentId,
309
+ parentAgentId,
310
+ client: HOOK_CLIENT,
311
+ conversationId: input.conversation_id,
312
+ sessionId: result.sessionId ?? null,
313
+ role: agentId === 'main' ? 'main' : 'subagent',
314
+ });
315
+ }
316
+
278
317
  const trackedState = await maybeTrackTurn({
279
- hookKey: buildCursorHookKey({ conversationId: input.conversation_id }),
318
+ hookKey: buildCursorHookKey({ conversationId: input.conversation_id, agentId }),
280
319
  cursorConversationId: input.conversation_id,
320
+ conversationId: input.conversation_id,
281
321
  projectSessionId: result.sessionId ?? null,
322
+ taskId: result.task?.taskId ?? null,
323
+ agentId,
324
+ parentAgentId,
282
325
  prompt: input.user_message,
283
326
  continuityState: result.continuity?.state ?? '',
284
327
  });
@@ -294,7 +337,7 @@ export const createCursorAdapter = ({
294
337
  };
295
338
 
296
339
  const handlePostToolUse = async (input) => {
297
- const hookKey = buildCursorHookKey({ conversationId: input.conversation_id });
340
+ const hookKey = buildCursorHookKey({ conversationId: input.conversation_id, agentId: resolveAgentId(input) });
298
341
  const existing = await readTrackedState(hookKey);
299
342
  if (!existing) {
300
343
  return null;
@@ -332,7 +375,7 @@ export const createCursorAdapter = ({
332
375
  };
333
376
 
334
377
  const handleConversationEnd = async (input) => {
335
- const hookKey = buildCursorHookKey({ conversationId: input.conversation_id });
378
+ const hookKey = buildCursorHookKey({ conversationId: input.conversation_id, agentId: resolveAgentId(input) });
336
379
  const state = await readTrackedState(hookKey);
337
380
  if (!state) {
338
381
  return null;
@@ -374,6 +417,23 @@ export const createCursorAdapter = ({
374
417
  });
375
418
  }
376
419
 
420
+ if (state.taskId) {
421
+ await writeTaskHandoff({
422
+ taskId: state.taskId,
423
+ sessionId: state.projectSessionId,
424
+ fromAgentId: state.agentId ?? null,
425
+ toAgentId: null,
426
+ trigger: state.agentId && state.agentId !== 'main' ? 'subagent_delegate' : 'session_end',
427
+ summary: {
428
+ currentFocus: update.currentFocus ?? state.promptPreview,
429
+ touchedFiles: state.touchedFiles,
430
+ pending: update.nextStep ? [update.nextStep] : [],
431
+ nextStep: update.nextStep ?? null,
432
+ evidence: state.touchedFiles.length > 0 ? [`Touched files: ${state.touchedFiles.join(', ')}`] : [],
433
+ },
434
+ });
435
+ }
436
+
377
437
  await recordHookMetrics({
378
438
  action: 'ConversationEnd',
379
439
  sessionId: state.projectSessionId,
package/src/server.js CHANGED
@@ -35,6 +35,17 @@ import {
35
35
  const require = createRequire(import.meta.url);
36
36
  const { version } = require('../package.json');
37
37
 
38
+ const SERVER_INSTRUCTIONS = `devctx — compressed context, search, and session handoff for long work (migrations, multi-file refactors, multi-session tasks).
39
+
40
+ smart_turn (session continuity — read this before calling):
41
+ - START: phase "start". Pass userPrompt (current goal). ensureSession true recommended when you want persistence. Use at the beginning of substantial work or when resuming after a break — not for one-line fixes or single-shot questions.
42
+ - END: phase "end". Pass event: milestone | blocker | task_complete. Pass sessionId if you have it; include update (nextStep, completed, etc.) when checkpointing progress. Call after a meaningful slice of work (close a phase), not after every trivial edit.
43
+ - SKIP smart_turn entirely for trivial or same-session point tasks (the tool schema also warns about this).
44
+
45
+ Source of truth: devctx does not replace git history, PRs, or repo docs (e.g. MIGRATION.md). If end was not called or work was not committed, those remain authoritative.
46
+
47
+ Other entry points: smart_context for curated multi-file context; smart_search with intent for exploration; smart_read in outline|signatures|symbol before full reads; smart_shell for safe git/npm diagnostics.`;
48
+
38
49
  export const asTextResult = (result) => ({
39
50
  content: [
40
51
  {
@@ -52,10 +63,10 @@ export const createDevctxServer = () => {
52
63
  process.exit(1);
53
64
  }
54
65
 
55
- const server = new McpServer({
56
- name: 'devctx',
57
- version,
58
- });
66
+ const server = new McpServer(
67
+ { name: 'devctx', version },
68
+ { instructions: SERVER_INSTRUCTIONS },
69
+ );
59
70
 
60
71
  setServerForStreaming(server);
61
72