smart-context-mcp 1.16.2 → 1.16.5
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 +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/client-contract.js +17 -1
- package/src/orchestration/adapters/claude-adapter.js +115 -5
- package/src/orchestration/adapters/cursor-adapter.js +115 -5
- package/src/server.js +15 -4
- package/src/storage/sqlite.js +459 -5
- package/src/tools/smart-summary.js +306 -5
- package/src/tools/smart-turn.js +11 -0
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.
|
|
59
|
+
# Should show: smart-context-mcp@1.16.5 (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.
|
|
4
|
+
"version": "1.16.5",
|
|
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",
|
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.
|
|
9
|
+
"version": "1.16.5",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "smart-context-mcp",
|
|
14
|
-
"version": "1.16.
|
|
14
|
+
"version": "1.16.5",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/src/client-contract.js
CHANGED
|
@@ -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
|
|
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,
|
|
@@ -246,6 +270,45 @@ export const createClaudeAdapter = ({
|
|
|
246
270
|
});
|
|
247
271
|
};
|
|
248
272
|
|
|
273
|
+
const maybeAutoCheckpointFromState = async (state, { trigger = 'post_tool_use' } = {}) => {
|
|
274
|
+
if (!state?.projectSessionId || getMutationSafety().shouldBlock) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const update = {
|
|
279
|
+
...(state.promptPreview ? { currentFocus: state.promptPreview } : {}),
|
|
280
|
+
...(state.touchedFiles.length > 0 ? { touchedFiles: state.touchedFiles } : {}),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
await summaryTool({
|
|
284
|
+
action: 'checkpoint',
|
|
285
|
+
sessionId: state.projectSessionId,
|
|
286
|
+
event: 'milestone',
|
|
287
|
+
update,
|
|
288
|
+
maxTokens: STOP_MAX_TOKENS,
|
|
289
|
+
force: true,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (state.taskId) {
|
|
293
|
+
await writeTaskHandoff({
|
|
294
|
+
taskId: state.taskId,
|
|
295
|
+
sessionId: state.projectSessionId,
|
|
296
|
+
fromAgentId: state.agentId ?? null,
|
|
297
|
+
toAgentId: null,
|
|
298
|
+
trigger,
|
|
299
|
+
summary: {
|
|
300
|
+
currentFocus: state.promptPreview,
|
|
301
|
+
touchedFiles: state.touchedFiles,
|
|
302
|
+
pending: [],
|
|
303
|
+
nextStep: null,
|
|
304
|
+
evidence: state.touchedFiles.length > 0 ? [`Touched files: ${state.touchedFiles.join(', ')}`] : [],
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return true;
|
|
310
|
+
};
|
|
311
|
+
|
|
249
312
|
const handleSessionStart = async () => {
|
|
250
313
|
const result = await startTurn({
|
|
251
314
|
phase: 'start',
|
|
@@ -262,6 +325,8 @@ export const createClaudeAdapter = ({
|
|
|
262
325
|
};
|
|
263
326
|
|
|
264
327
|
const handleUserPromptSubmit = async (input) => {
|
|
328
|
+
const agentId = resolveAgentId(input);
|
|
329
|
+
const parentAgentId = resolveParentAgentId(input);
|
|
265
330
|
const startResolution = await resolveStart({
|
|
266
331
|
prompt: input.prompt,
|
|
267
332
|
ensureSession: true,
|
|
@@ -272,10 +337,27 @@ export const createClaudeAdapter = ({
|
|
|
272
337
|
});
|
|
273
338
|
const result = startResolution.startResult;
|
|
274
339
|
|
|
340
|
+
if (!getMutationSafety().shouldBlock) {
|
|
341
|
+
await writeAgentRun({
|
|
342
|
+
runId: buildClaudeHookKey({ sessionId: input.session_id, agentId }),
|
|
343
|
+
taskId: result.task?.taskId ?? null,
|
|
344
|
+
agentId,
|
|
345
|
+
parentAgentId,
|
|
346
|
+
client: HOOK_CLIENT,
|
|
347
|
+
conversationId: input.session_id,
|
|
348
|
+
sessionId: result.sessionId ?? null,
|
|
349
|
+
role: agentId === 'main' ? 'main' : 'subagent',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
275
353
|
const trackedState = await maybeTrackTurn({
|
|
276
|
-
hookKey: buildClaudeHookKey({ sessionId: input.session_id }),
|
|
354
|
+
hookKey: buildClaudeHookKey({ sessionId: input.session_id, agentId }),
|
|
277
355
|
claudeSessionId: input.session_id,
|
|
356
|
+
conversationId: input.session_id,
|
|
278
357
|
projectSessionId: result.sessionId ?? null,
|
|
358
|
+
taskId: result.task?.taskId ?? null,
|
|
359
|
+
agentId,
|
|
360
|
+
parentAgentId,
|
|
279
361
|
prompt: input.prompt,
|
|
280
362
|
continuityState: result.continuity?.state ?? '',
|
|
281
363
|
});
|
|
@@ -291,7 +373,7 @@ export const createClaudeAdapter = ({
|
|
|
291
373
|
};
|
|
292
374
|
|
|
293
375
|
const handlePostToolUse = async (input) => {
|
|
294
|
-
const hookKey = buildClaudeHookKey({ sessionId: input.session_id });
|
|
376
|
+
const hookKey = buildClaudeHookKey({ sessionId: input.session_id, agentId: resolveAgentId(input) });
|
|
295
377
|
const existing = await readTrackedState(hookKey);
|
|
296
378
|
if (!existing) {
|
|
297
379
|
return null;
|
|
@@ -307,7 +389,7 @@ export const createClaudeAdapter = ({
|
|
|
307
389
|
toolResponse: input.tool_response,
|
|
308
390
|
});
|
|
309
391
|
|
|
310
|
-
|
|
392
|
+
let nextState = {
|
|
311
393
|
...existing,
|
|
312
394
|
checkpointed: checkpoint.matched ? true : existing.checkpointed,
|
|
313
395
|
checkpointEvent: checkpoint.matched ? checkpoint.event : existing.checkpointEvent,
|
|
@@ -316,6 +398,17 @@ export const createClaudeAdapter = ({
|
|
|
316
398
|
updatedAt: new Date().toISOString(),
|
|
317
399
|
};
|
|
318
400
|
|
|
401
|
+
if (!checkpoint.matched && touchedFiles.length > 0) {
|
|
402
|
+
const autoCheckpointed = await maybeAutoCheckpointFromState(nextState, { trigger: 'post_tool_use' });
|
|
403
|
+
if (autoCheckpointed) {
|
|
404
|
+
nextState = {
|
|
405
|
+
...nextState,
|
|
406
|
+
checkpointed: true,
|
|
407
|
+
checkpointEvent: 'milestone',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
319
412
|
await maybeSetTrackedTurnState({ hookKey, state: nextState });
|
|
320
413
|
if (checkpoint.matched || touchedFiles.length > 0) {
|
|
321
414
|
await recordHookMetrics({
|
|
@@ -329,7 +422,7 @@ export const createClaudeAdapter = ({
|
|
|
329
422
|
};
|
|
330
423
|
|
|
331
424
|
const handleStop = async (input) => {
|
|
332
|
-
const hookKey = buildClaudeHookKey({ sessionId: input.session_id });
|
|
425
|
+
const hookKey = buildClaudeHookKey({ sessionId: input.session_id, agentId: resolveAgentId(input) });
|
|
333
426
|
const state = await readTrackedState(hookKey);
|
|
334
427
|
if (!state) {
|
|
335
428
|
return null;
|
|
@@ -371,6 +464,23 @@ export const createClaudeAdapter = ({
|
|
|
371
464
|
});
|
|
372
465
|
}
|
|
373
466
|
|
|
467
|
+
if (state.taskId) {
|
|
468
|
+
await writeTaskHandoff({
|
|
469
|
+
taskId: state.taskId,
|
|
470
|
+
sessionId: state.projectSessionId,
|
|
471
|
+
fromAgentId: state.agentId ?? null,
|
|
472
|
+
toAgentId: null,
|
|
473
|
+
trigger: state.agentId && state.agentId !== 'main' ? 'subagent_delegate' : 'stop',
|
|
474
|
+
summary: {
|
|
475
|
+
currentFocus: update.currentFocus ?? state.promptPreview,
|
|
476
|
+
touchedFiles: state.touchedFiles,
|
|
477
|
+
pending: update.nextStep ? [update.nextStep] : [],
|
|
478
|
+
nextStep: update.nextStep ?? null,
|
|
479
|
+
evidence: state.touchedFiles.length > 0 ? [`Touched files: ${state.touchedFiles.join(', ')}`] : [],
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
374
484
|
await recordHookMetrics({
|
|
375
485
|
action: 'Stop',
|
|
376
486
|
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
|
|
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,
|
|
@@ -249,6 +273,45 @@ export const createCursorAdapter = ({
|
|
|
249
273
|
});
|
|
250
274
|
};
|
|
251
275
|
|
|
276
|
+
const maybeAutoCheckpointFromState = async (state, { trigger = 'post_tool_use' } = {}) => {
|
|
277
|
+
if (!state?.projectSessionId || getMutationSafety().shouldBlock) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const update = {
|
|
282
|
+
...(state.promptPreview ? { currentFocus: state.promptPreview } : {}),
|
|
283
|
+
...(state.touchedFiles.length > 0 ? { touchedFiles: state.touchedFiles } : {}),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
await summaryTool({
|
|
287
|
+
action: 'checkpoint',
|
|
288
|
+
sessionId: state.projectSessionId,
|
|
289
|
+
event: 'milestone',
|
|
290
|
+
update,
|
|
291
|
+
maxTokens: STOP_MAX_TOKENS,
|
|
292
|
+
force: true,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (state.taskId) {
|
|
296
|
+
await writeTaskHandoff({
|
|
297
|
+
taskId: state.taskId,
|
|
298
|
+
sessionId: state.projectSessionId,
|
|
299
|
+
fromAgentId: state.agentId ?? null,
|
|
300
|
+
toAgentId: null,
|
|
301
|
+
trigger,
|
|
302
|
+
summary: {
|
|
303
|
+
currentFocus: state.promptPreview,
|
|
304
|
+
touchedFiles: state.touchedFiles,
|
|
305
|
+
pending: [],
|
|
306
|
+
nextStep: null,
|
|
307
|
+
evidence: state.touchedFiles.length > 0 ? [`Touched files: ${state.touchedFiles.join(', ')}`] : [],
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return true;
|
|
313
|
+
};
|
|
314
|
+
|
|
252
315
|
const handleConversationStart = async () => {
|
|
253
316
|
const result = await startTurn({
|
|
254
317
|
phase: 'start',
|
|
@@ -265,6 +328,8 @@ export const createCursorAdapter = ({
|
|
|
265
328
|
};
|
|
266
329
|
|
|
267
330
|
const handleUserMessageSubmit = async (input) => {
|
|
331
|
+
const agentId = resolveAgentId(input);
|
|
332
|
+
const parentAgentId = resolveParentAgentId(input);
|
|
268
333
|
const startResolution = await resolveStart({
|
|
269
334
|
prompt: input.user_message,
|
|
270
335
|
ensureSession: true,
|
|
@@ -275,10 +340,27 @@ export const createCursorAdapter = ({
|
|
|
275
340
|
});
|
|
276
341
|
const result = startResolution.startResult;
|
|
277
342
|
|
|
343
|
+
if (!getMutationSafety().shouldBlock) {
|
|
344
|
+
await writeAgentRun({
|
|
345
|
+
runId: buildCursorHookKey({ conversationId: input.conversation_id, agentId }),
|
|
346
|
+
taskId: result.task?.taskId ?? null,
|
|
347
|
+
agentId,
|
|
348
|
+
parentAgentId,
|
|
349
|
+
client: HOOK_CLIENT,
|
|
350
|
+
conversationId: input.conversation_id,
|
|
351
|
+
sessionId: result.sessionId ?? null,
|
|
352
|
+
role: agentId === 'main' ? 'main' : 'subagent',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
278
356
|
const trackedState = await maybeTrackTurn({
|
|
279
|
-
hookKey: buildCursorHookKey({ conversationId: input.conversation_id }),
|
|
357
|
+
hookKey: buildCursorHookKey({ conversationId: input.conversation_id, agentId }),
|
|
280
358
|
cursorConversationId: input.conversation_id,
|
|
359
|
+
conversationId: input.conversation_id,
|
|
281
360
|
projectSessionId: result.sessionId ?? null,
|
|
361
|
+
taskId: result.task?.taskId ?? null,
|
|
362
|
+
agentId,
|
|
363
|
+
parentAgentId,
|
|
282
364
|
prompt: input.user_message,
|
|
283
365
|
continuityState: result.continuity?.state ?? '',
|
|
284
366
|
});
|
|
@@ -294,7 +376,7 @@ export const createCursorAdapter = ({
|
|
|
294
376
|
};
|
|
295
377
|
|
|
296
378
|
const handlePostToolUse = async (input) => {
|
|
297
|
-
const hookKey = buildCursorHookKey({ conversationId: input.conversation_id });
|
|
379
|
+
const hookKey = buildCursorHookKey({ conversationId: input.conversation_id, agentId: resolveAgentId(input) });
|
|
298
380
|
const existing = await readTrackedState(hookKey);
|
|
299
381
|
if (!existing) {
|
|
300
382
|
return null;
|
|
@@ -310,7 +392,7 @@ export const createCursorAdapter = ({
|
|
|
310
392
|
toolResponse: input.tool_response,
|
|
311
393
|
});
|
|
312
394
|
|
|
313
|
-
|
|
395
|
+
let nextState = {
|
|
314
396
|
...existing,
|
|
315
397
|
checkpointed: checkpoint.matched ? true : existing.checkpointed,
|
|
316
398
|
checkpointEvent: checkpoint.matched ? checkpoint.event : existing.checkpointEvent,
|
|
@@ -319,6 +401,17 @@ export const createCursorAdapter = ({
|
|
|
319
401
|
updatedAt: new Date().toISOString(),
|
|
320
402
|
};
|
|
321
403
|
|
|
404
|
+
if (!checkpoint.matched && touchedFiles.length > 0) {
|
|
405
|
+
const autoCheckpointed = await maybeAutoCheckpointFromState(nextState, { trigger: 'post_tool_use' });
|
|
406
|
+
if (autoCheckpointed) {
|
|
407
|
+
nextState = {
|
|
408
|
+
...nextState,
|
|
409
|
+
checkpointed: true,
|
|
410
|
+
checkpointEvent: 'milestone',
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
322
415
|
await maybeSetTrackedTurnState({ hookKey, state: nextState });
|
|
323
416
|
if (checkpoint.matched || touchedFiles.length > 0) {
|
|
324
417
|
await recordHookMetrics({
|
|
@@ -332,7 +425,7 @@ export const createCursorAdapter = ({
|
|
|
332
425
|
};
|
|
333
426
|
|
|
334
427
|
const handleConversationEnd = async (input) => {
|
|
335
|
-
const hookKey = buildCursorHookKey({ conversationId: input.conversation_id });
|
|
428
|
+
const hookKey = buildCursorHookKey({ conversationId: input.conversation_id, agentId: resolveAgentId(input) });
|
|
336
429
|
const state = await readTrackedState(hookKey);
|
|
337
430
|
if (!state) {
|
|
338
431
|
return null;
|
|
@@ -374,6 +467,23 @@ export const createCursorAdapter = ({
|
|
|
374
467
|
});
|
|
375
468
|
}
|
|
376
469
|
|
|
470
|
+
if (state.taskId) {
|
|
471
|
+
await writeTaskHandoff({
|
|
472
|
+
taskId: state.taskId,
|
|
473
|
+
sessionId: state.projectSessionId,
|
|
474
|
+
fromAgentId: state.agentId ?? null,
|
|
475
|
+
toAgentId: null,
|
|
476
|
+
trigger: state.agentId && state.agentId !== 'main' ? 'subagent_delegate' : 'session_end',
|
|
477
|
+
summary: {
|
|
478
|
+
currentFocus: update.currentFocus ?? state.promptPreview,
|
|
479
|
+
touchedFiles: state.touchedFiles,
|
|
480
|
+
pending: update.nextStep ? [update.nextStep] : [],
|
|
481
|
+
nextStep: update.nextStep ?? null,
|
|
482
|
+
evidence: state.touchedFiles.length > 0 ? [`Touched files: ${state.touchedFiles.join(', ')}`] : [],
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
377
487
|
await recordHookMetrics({
|
|
378
488
|
action: 'ConversationEnd',
|
|
379
489
|
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
|
-
|
|
58
|
-
|
|
66
|
+
const server = new McpServer(
|
|
67
|
+
{ name: 'devctx', version },
|
|
68
|
+
{ instructions: SERVER_INSTRUCTIONS },
|
|
69
|
+
);
|
|
59
70
|
|
|
60
71
|
setServerForStreaming(server);
|
|
61
72
|
|