smart-context-mcp 1.6.2 → 1.7.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-context-mcp",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
5
5
  "author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
6
6
  "type": "module",
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { smartMetrics } from '../src/tools/smart-metrics.js';
5
5
  import { formatAdoptionReport } from '../src/analytics/adoption.js';
6
- import { formatProductQualityReport } from '../src/analytics/product-quality.js';
6
+ import { formatProductQualityReport, hasProductQualitySignals } from '../src/analytics/product-quality.js';
7
7
 
8
8
  const requireValue = (argv, index, flag) => {
9
9
  const value = argv[index + 1];
@@ -101,7 +101,7 @@ const printHuman = (report) => {
101
101
  console.log(formatAdoptionReport(report.adoption));
102
102
  }
103
103
 
104
- if (report.productQuality?.turnsMeasured > 0) {
104
+ if (hasProductQualitySignals(report.productQuality)) {
105
105
  console.log(formatProductQualityReport(report.productQuality));
106
106
  }
107
107
  };
@@ -12,6 +12,139 @@ const isTaskRunnerQualityEntry = (entry) =>
12
12
  entry?.tool === 'task_runner'
13
13
  && entry?.metadata?.analyticsKind === TASK_RUNNER_QUALITY_ANALYTICS_KIND;
14
14
 
15
+ const roundAverage = (total, count) =>
16
+ count > 0 ? Number((total / count).toFixed(1)) : 0;
17
+
18
+ const getMetricsClient = (entry) =>
19
+ entry?.metadata?.adapterClient
20
+ ?? entry?.metadata?.client
21
+ ?? null;
22
+
23
+ const hasMeasuredClientAdapters = (stats) =>
24
+ Number(stats?.clientAdapters?.clientsMeasured ?? 0) > 0;
25
+
26
+ const appendClientAdapterSignals = (lines, stats) => {
27
+ if (!hasMeasuredClientAdapters(stats)) {
28
+ return;
29
+ }
30
+
31
+ const clients = stats.clientAdapters.byClient;
32
+ const lowestOverheadClient = clients.reduce((best, current) => {
33
+ if (!best) {
34
+ return current;
35
+ }
36
+
37
+ if (current.averageContextOverheadTokens < best.averageContextOverheadTokens) {
38
+ return current;
39
+ }
40
+
41
+ if (current.averageContextOverheadTokens === best.averageContextOverheadTokens
42
+ && current.client.localeCompare(best.client) < 0) {
43
+ return current;
44
+ }
45
+
46
+ return best;
47
+ }, null);
48
+ const highestAutoStartClient = clients.reduce((best, current) => {
49
+ if (!best) {
50
+ return current;
51
+ }
52
+
53
+ if (current.autoStartCoveragePct > best.autoStartCoveragePct) {
54
+ return current;
55
+ }
56
+
57
+ if (current.autoStartCoveragePct === best.autoStartCoveragePct
58
+ && current.client.localeCompare(best.client) < 0) {
59
+ return current;
60
+ }
61
+
62
+ return best;
63
+ }, null);
64
+
65
+ lines.push('Client Adapter Signals:');
66
+ lines.push(`Clients measured: ${stats.clientAdapters.clientsMeasured}`);
67
+ lines.push(`Adapter events: ${clients.reduce((total, client) => total + client.adapterEvents, 0)}`);
68
+ lines.push(`Overhead total: ${stats.clientAdapters.totalContextOverheadTokens} tokens`);
69
+ if (lowestOverheadClient) {
70
+ lines.push(`Lowest avg overhead: ${lowestOverheadClient.client} (${lowestOverheadClient.averageContextOverheadTokens} tokens)`);
71
+ }
72
+ if (highestAutoStartClient) {
73
+ lines.push(`Best auto-start rate: ${highestAutoStartClient.client} (${highestAutoStartClient.autoStartCoveragePct}%)`);
74
+ }
75
+ lines.push('');
76
+
77
+ for (const client of clients) {
78
+ lines.push(`${client.client}:`);
79
+ lines.push(` Entries measured: ${client.entriesMeasured}`);
80
+ lines.push(` Adapter coverage: ${client.adapterEvents}/${client.entriesMeasured} (${client.adapterCoveragePct}%)`);
81
+ lines.push(` Base orchestrated: ${client.baseOrchestratedEvents}/${client.entriesMeasured} (${client.baseOrchestratorCoveragePct}%)`);
82
+ lines.push(` Auto-started: ${client.autoStartedEvents}/${client.entriesMeasured} (${client.autoStartCoveragePct}%)`);
83
+ lines.push(` Auto-preflighted: ${client.autoPreflightedEvents}/${client.entriesMeasured} (${client.autoPreflightCoveragePct}%)`);
84
+ lines.push(` Auto-checkpointed: ${client.autoCheckpointedEvents}/${client.entriesMeasured} (${client.autoCheckpointCoveragePct}%)`);
85
+ lines.push(` Context overhead: ${client.contextOverheadTokens} tokens total (${client.averageContextOverheadTokens} avg)`);
86
+ if (client.blockedEvents > 0) {
87
+ lines.push(` Blocked events: ${client.blockedEvents}`);
88
+ }
89
+ lines.push('');
90
+ }
91
+ };
92
+
93
+ const analyzeClientAdapterQuality = (entries = []) => {
94
+ const clientEntries = entries.filter((entry) => Boolean(getMetricsClient(entry)));
95
+
96
+ const byClient = [...clientEntries.reduce((acc, entry) => {
97
+ const client = getMetricsClient(entry);
98
+ const current = acc.get(client) ?? {
99
+ client,
100
+ entriesMeasured: 0,
101
+ adapterEvents: 0,
102
+ wrapperEvents: 0,
103
+ taskRunnerEvents: 0,
104
+ baseOrchestratedEvents: 0,
105
+ autoStartedEvents: 0,
106
+ autoPreflightedEvents: 0,
107
+ autoCheckpointedEvents: 0,
108
+ blockedEvents: 0,
109
+ contextOverheadEntries: 0,
110
+ contextOverheadTokens: 0,
111
+ };
112
+
113
+ const overheadTokens = Math.max(0, Number(entry.metadata?.overheadTokens ?? 0));
114
+ current.entriesMeasured += 1;
115
+ current.adapterEvents += entry.metadata?.managedByClientAdapter ? 1 : 0;
116
+ current.wrapperEvents += entry.tool === 'agent_wrapper' ? 1 : 0;
117
+ current.taskRunnerEvents += entry.tool === 'task_runner' ? 1 : 0;
118
+ current.baseOrchestratedEvents += entry.metadata?.managedByBaseOrchestrator ? 1 : 0;
119
+ current.autoStartedEvents += (entry.metadata?.autoStartTriggered || entry.metadata?.autoStarted) ? 1 : 0;
120
+ current.autoPreflightedEvents += entry.metadata?.autoPreflightTriggered ? 1 : 0;
121
+ current.autoCheckpointedEvents += (entry.metadata?.autoCheckpointTriggered || entry.metadata?.autoAppended) ? 1 : 0;
122
+ current.blockedEvents += entry.metadata?.blocked ? 1 : 0;
123
+ current.contextOverheadEntries += overheadTokens > 0 ? 1 : 0;
124
+ current.contextOverheadTokens += overheadTokens;
125
+
126
+ acc.set(client, current);
127
+ return acc;
128
+ }, new Map()).values()]
129
+ .map((entry) => ({
130
+ ...entry,
131
+ averageContextOverheadTokens: roundAverage(entry.contextOverheadTokens, entry.contextOverheadEntries),
132
+ adapterCoveragePct: roundPct(entry.adapterEvents, entry.entriesMeasured),
133
+ baseOrchestratorCoveragePct: roundPct(entry.baseOrchestratedEvents, entry.entriesMeasured),
134
+ autoStartCoveragePct: roundPct(entry.autoStartedEvents, entry.entriesMeasured),
135
+ autoPreflightCoveragePct: roundPct(entry.autoPreflightedEvents, entry.entriesMeasured),
136
+ autoCheckpointCoveragePct: roundPct(entry.autoCheckpointedEvents, entry.entriesMeasured),
137
+ }))
138
+ .sort((a, b) => a.client.localeCompare(b.client));
139
+
140
+ return {
141
+ clientsMeasured: byClient.length,
142
+ entriesMeasured: clientEntries.length,
143
+ totalContextOverheadTokens: byClient.reduce((total, entry) => total + entry.contextOverheadTokens, 0),
144
+ byClient,
145
+ };
146
+ };
147
+
15
148
  const analyzeTaskRunnerQuality = (entries = []) => {
16
149
  const runnerEntries = entries.filter(isTaskRunnerQualityEntry);
17
150
  const workflowEntries = runnerEntries.filter((entry) => entry.metadata?.isWorkflowCommand);
@@ -25,6 +158,16 @@ const analyzeTaskRunnerQuality = (entries = []) => {
25
158
  const blockedWithDoctor = blockedEntries.filter((entry) => entry.metadata?.doctorIssued);
26
159
  const checkpointEntries = runnerEntries.filter((entry) => entry.action === 'checkpoint');
27
160
  const persistedCheckpointEntries = checkpointEntries.filter((entry) => entry.metadata?.checkpointPersisted);
161
+ const baseOrchestratedEntries = workflowEntries.filter((entry) => entry.metadata?.managedByBaseOrchestrator);
162
+ const autoStartedEntries = workflowEntries.filter((entry) => entry.metadata?.autoStartTriggered);
163
+ const autoPreflightEntries = workflowEntries.filter((entry) => entry.metadata?.autoPreflightTriggered);
164
+ const autoCheckpointEntries = runnerEntries.filter((entry) => entry.metadata?.autoCheckpointTriggered);
165
+ const isolatedWorkflowEntries = workflowEntries.filter((entry) => entry.metadata?.isolatedSession);
166
+ const contextOverheadEntries = runnerEntries.filter((entry) => Number(entry.metadata?.contextOverheadTokens ?? 0) > 0);
167
+ const contextOverheadTokens = runnerEntries.reduce(
168
+ (total, entry) => total + Number(entry.metadata?.contextOverheadTokens ?? 0),
169
+ 0,
170
+ );
28
171
 
29
172
  const commandBreakdown = [...runnerEntries.reduce((acc, entry) => {
30
173
  const key = entry.action ?? 'unknown';
@@ -71,6 +214,20 @@ const analyzeTaskRunnerQuality = (entries = []) => {
71
214
  persistedCommands: persistedCheckpointEntries.length,
72
215
  persistenceRatePct: roundPct(persistedCheckpointEntries.length, checkpointEntries.length),
73
216
  },
217
+ automaticity: {
218
+ baseOrchestratedCommands: baseOrchestratedEntries.length,
219
+ autoStartedCommands: autoStartedEntries.length,
220
+ autoPreflightedCommands: autoPreflightEntries.length,
221
+ autoCheckpointedCommands: autoCheckpointEntries.length,
222
+ isolatedWorkflowCommands: isolatedWorkflowEntries.length,
223
+ contextOverheadTokens,
224
+ averageContextOverheadTokens: contextOverheadEntries.length > 0
225
+ ? Number((contextOverheadTokens / contextOverheadEntries.length).toFixed(1))
226
+ : 0,
227
+ baseOrchestratorCoveragePct: roundPct(baseOrchestratedEntries.length, workflowEntries.length),
228
+ autoStartCoveragePct: roundPct(autoStartedEntries.length, workflowEntries.length),
229
+ autoPreflightCoveragePct: roundPct(autoPreflightEntries.length, workflowEntries.length),
230
+ },
74
231
  };
75
232
  };
76
233
 
@@ -132,15 +289,24 @@ export const analyzeProductQuality = (entries = []) => {
132
289
  blockedEnds,
133
290
  persistenceRatePct: roundPct(persistedEnds, endEntries.length),
134
291
  },
292
+ clientAdapters: analyzeClientAdapterQuality(entries),
135
293
  taskRunner: analyzeTaskRunnerQuality(entries),
136
294
  };
137
295
  };
138
296
 
297
+ export const hasProductQualitySignals = (stats) =>
298
+ Boolean(stats)
299
+ && (
300
+ Number(stats.turnsMeasured ?? 0) > 0
301
+ || Number(stats?.taskRunner?.commandsMeasured ?? 0) > 0
302
+ || hasMeasuredClientAdapters(stats)
303
+ );
304
+
139
305
  export const formatProductQualityReport = (stats) => {
140
306
  const hasSmartTurn = Number(stats?.turnsMeasured ?? 0) > 0;
141
307
  const hasTaskRunner = Number(stats?.taskRunner?.commandsMeasured ?? 0) > 0;
142
308
 
143
- if (!stats || (!hasSmartTurn && !hasTaskRunner)) {
309
+ if (!hasProductQualitySignals(stats)) {
144
310
  return '';
145
311
  }
146
312
 
@@ -193,11 +359,20 @@ export const formatProductQualityReport = (stats) => {
193
359
  lines.push('Blocked-State Routing:');
194
360
  lines.push(`Blocked with doctor: ${stats.taskRunner.blockedState.blockedWithDoctor}/${stats.taskRunner.blockedState.blockedCommands} (${stats.taskRunner.blockedState.doctorCoveragePct}%)`);
195
361
  lines.push('');
362
+ lines.push('Automaticity:');
363
+ lines.push(`Base orchestrated: ${stats.taskRunner.automaticity.baseOrchestratedCommands}/${stats.taskRunner.workflowCommands} (${stats.taskRunner.automaticity.baseOrchestratorCoveragePct}%)`);
364
+ lines.push(`Auto-started: ${stats.taskRunner.automaticity.autoStartedCommands}/${stats.taskRunner.workflowCommands} (${stats.taskRunner.automaticity.autoStartCoveragePct}%)`);
365
+ lines.push(`Auto-preflighted: ${stats.taskRunner.automaticity.autoPreflightedCommands}/${stats.taskRunner.workflowCommands} (${stats.taskRunner.automaticity.autoPreflightCoveragePct}%)`);
366
+ lines.push(`Auto-checkpointed: ${stats.taskRunner.automaticity.autoCheckpointedCommands}`);
367
+ lines.push(`Context overhead: ${stats.taskRunner.automaticity.contextOverheadTokens} tokens total (${stats.taskRunner.automaticity.averageContextOverheadTokens} avg)`);
368
+ lines.push('');
196
369
  lines.push('Checkpoint Commands:');
197
370
  lines.push(`Persisted checkpoints: ${stats.taskRunner.checkpointing.persistedCommands}/${stats.taskRunner.checkpointing.commandsMeasured} (${stats.taskRunner.checkpointing.persistenceRatePct}%)`);
198
371
  lines.push('');
199
372
  }
200
373
 
374
+ appendClientAdapterSignals(lines, stats);
375
+
201
376
  lines.push('Notes:');
202
377
  lines.push('- These are measured orchestration signals, not direct answer-quality scores.');
203
378
  lines.push('- Context refresh usefulness is proxied by whether refreshed turns surfaced top-file signals.');
@@ -1,423 +1 @@
1
- import { persistMetrics } from '../metrics.js';
2
- import { buildOperationalContextLines } from '../client-contract.js';
3
- import { getRepoMutationSafety } from '../repo-safety.js';
4
- import { countTokens } from '../tokenCounter.js';
5
- import { smartSummary } from '../tools/smart-summary.js';
6
- import { smartTurn } from '../tools/smart-turn.js';
7
- import {
8
- deleteHookTurnState,
9
- getHookTurnState,
10
- setHookTurnState,
11
- } from '../storage/sqlite.js';
12
-
13
- const HOOK_CLIENT = 'claude';
14
- const START_MAX_TOKENS = 350;
15
- const STOP_MAX_TOKENS = 300;
16
- const MAX_CONTEXT_LINES = 7;
17
- const MAX_CONTEXT_CHARS = 420;
18
- const MAX_PROMPT_PREVIEW = 160;
19
- const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
20
- const SIGNIFICANT_RESPONSE_LENGTH = 140;
21
-
22
- const normalizeWhitespace = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
23
-
24
- const truncate = (value, maxLength = MAX_PROMPT_PREVIEW) => {
25
- const normalized = normalizeWhitespace(value);
26
- if (normalized.length <= maxLength) {
27
- return normalized;
28
- }
29
-
30
- if (maxLength <= 3) {
31
- return '';
32
- }
33
-
34
- return `${normalized.slice(0, maxLength - 3)}...`;
35
- };
36
-
37
- const countPromptTerms = (value) =>
38
- normalizeWhitespace(value)
39
- .split(/[^a-z0-9_.-]+/i)
40
- .map((term) => term.trim())
41
- .filter((term) => term.length >= 3)
42
- .length;
43
-
44
- const isMeaningfulPrompt = (value) => {
45
- const normalized = normalizeWhitespace(value);
46
- return normalized.length >= 20 && countPromptTerms(normalized) >= 4;
47
- };
48
-
49
- const uniq = (values) => [...new Set(values.filter((value) => typeof value === 'string' && value.trim().length > 0))];
50
-
51
- const buildHookKey = ({ sessionId, agentId = null }) =>
52
- agentId ? `${HOOK_CLIENT}:subagent:${sessionId}:${agentId}` : `${HOOK_CLIENT}:main:${sessionId}`;
53
-
54
- const buildAdditionalContext = ({ result, sessionStart = false }) => {
55
- return buildOperationalContextLines(result, {
56
- sessionStart,
57
- maxLineLength: 110,
58
- maxLines: MAX_CONTEXT_LINES,
59
- maxChars: MAX_CONTEXT_CHARS,
60
- });
61
- };
62
-
63
- const buildHookContextResponse = (hookEventName, additionalContext) => {
64
- if (!additionalContext) {
65
- return null;
66
- }
67
-
68
- return {
69
- hookSpecificOutput: {
70
- hookEventName,
71
- additionalContext,
72
- },
73
- };
74
- };
75
-
76
- const readHookTurnState = (hookKey) =>
77
- getHookTurnState({
78
- hookKey,
79
- readOnly: getRepoMutationSafety().shouldBlock,
80
- });
81
-
82
- const maybeSetTrackedTurnState = async ({ hookKey, state }) => {
83
- if (getRepoMutationSafety().shouldBlock) {
84
- return null;
85
- }
86
-
87
- return setHookTurnState({ hookKey, state });
88
- };
89
-
90
- const maybeDeleteTrackedTurnState = async ({ hookKey }) => {
91
- if (getRepoMutationSafety().shouldBlock) {
92
- return null;
93
- }
94
-
95
- return deleteHookTurnState({ hookKey });
96
- };
97
-
98
- const recordHookMetrics = async ({
99
- action,
100
- sessionId,
101
- additionalContext = '',
102
- blocked = false,
103
- autoAppended = false,
104
- continuityState = null,
105
- } = {}) => {
106
- const overheadTokens = additionalContext ? countTokens(additionalContext) : 0;
107
-
108
- await persistMetrics({
109
- tool: 'claude_hook',
110
- action,
111
- sessionId,
112
- rawTokens: 0,
113
- compressedTokens: 0,
114
- savedTokens: 0,
115
- savingsPct: 0,
116
- metadata: {
117
- isContextOverhead: overheadTokens > 0,
118
- overheadTokens,
119
- blocked,
120
- autoAppended,
121
- continuityState,
122
- },
123
- timestamp: new Date().toISOString(),
124
- });
125
- };
126
-
127
- const isSmartTurnTool = (toolName) => /^mcp__.+__smart_turn$/i.test(toolName ?? '');
128
- const isSmartSummaryTool = (toolName) => /^mcp__.+__smart_summary$/i.test(toolName ?? '');
129
-
130
- const isCheckpointToolUse = ({ toolName, toolInput }) => {
131
- if (isSmartTurnTool(toolName)) {
132
- return toolInput?.phase === 'end'
133
- ? { matched: true, event: toolInput?.event ?? 'manual' }
134
- : { matched: false, event: null };
135
- }
136
-
137
- if (isSmartSummaryTool(toolName)) {
138
- const action = toolInput?.action;
139
- if (action === 'checkpoint') {
140
- return { matched: true, event: toolInput?.event ?? 'manual' };
141
- }
142
-
143
- if (action === 'append' || action === 'auto_append' || action === 'update') {
144
- return { matched: true, event: action };
145
- }
146
- }
147
-
148
- return { matched: false, event: null };
149
- };
150
-
151
- const extractTouchedFiles = ({ toolName, toolInput, toolResponse }) => {
152
- if (!WRITE_TOOLS.has(toolName)) {
153
- return [];
154
- }
155
-
156
- return uniq([
157
- toolInput?.file_path,
158
- toolInput?.filePath,
159
- toolResponse?.file_path,
160
- toolResponse?.filePath,
161
- ]);
162
- };
163
-
164
- const extractNextStep = (message) => {
165
- const normalized = normalizeWhitespace(message);
166
- if (!normalized) {
167
- return '';
168
- }
169
-
170
- const explicitMatch = normalized.match(/(?:next step|siguiente paso)\s*[:\-]\s*([^.;\n]{12,180})/i);
171
- if (explicitMatch?.[1]) {
172
- return truncate(explicitMatch[1], 150);
173
- }
174
-
175
- return '';
176
- };
177
-
178
- const buildCarryoverUpdate = (state, lastAssistantMessage) => {
179
- const promptPreview = truncate(state.promptPreview, 140);
180
- const nextStep = extractNextStep(lastAssistantMessage);
181
- const pinnedContext = promptPreview ? [`Uncheckpointed turn: ${promptPreview}`] : [];
182
-
183
- return {
184
- ...(promptPreview ? { currentFocus: promptPreview } : {}),
185
- ...(pinnedContext.length > 0 ? { pinnedContext } : {}),
186
- ...(state.touchedFiles.length > 0 ? { touchedFiles: state.touchedFiles } : {}),
187
- ...(nextStep ? { nextStep } : {}),
188
- };
189
- };
190
-
191
- const computeStopEnforcement = (state, lastAssistantMessage) => {
192
- const nextStep = extractNextStep(lastAssistantMessage);
193
- const responseLength = normalizeWhitespace(lastAssistantMessage).length;
194
- let score = 0;
195
-
196
- if (state.meaningfulWriteCount > 0) {
197
- score += 3;
198
- }
199
-
200
- if (state.touchedFiles.length > 0) {
201
- score += 1;
202
- }
203
-
204
- if (nextStep) {
205
- score += 2;
206
- }
207
-
208
- if (responseLength >= SIGNIFICANT_RESPONSE_LENGTH) {
209
- score += 1;
210
- }
211
-
212
- if (state.continuityState === 'task_switch' || state.continuityState === 'possible_shift') {
213
- score += 1;
214
- }
215
-
216
- return {
217
- shouldBlock: score >= 3,
218
- score,
219
- nextStep,
220
- };
221
- };
222
-
223
- const maybeTrackTurn = async ({
224
- hookKey,
225
- claudeSessionId,
226
- projectSessionId,
227
- prompt,
228
- continuityState,
229
- }) => {
230
- const promptMeaningful = isMeaningfulPrompt(prompt);
231
- const shouldTrack = Boolean(projectSessionId) && promptMeaningful;
232
-
233
- if (!shouldTrack) {
234
- await deleteHookTurnState({ hookKey });
235
- return null;
236
- }
237
-
238
- return maybeSetTrackedTurnState({
239
- hookKey,
240
- state: {
241
- client: HOOK_CLIENT,
242
- claudeSessionId,
243
- projectSessionId,
244
- turnId: `${claudeSessionId}:${Date.now()}`,
245
- promptPreview: truncate(prompt),
246
- continuityState,
247
- requireCheckpoint: true,
248
- promptMeaningful,
249
- checkpointed: false,
250
- checkpointEvent: null,
251
- touchedFiles: [],
252
- meaningfulWriteCount: 0,
253
- },
254
- });
255
- };
256
-
257
- const handleSessionStart = async (input) => {
258
- const result = await smartTurn({
259
- phase: 'start',
260
- maxTokens: START_MAX_TOKENS,
261
- });
262
- const additionalContext = buildAdditionalContext({ result, sessionStart: true });
263
- await recordHookMetrics({
264
- action: 'SessionStart',
265
- sessionId: result.sessionId ?? null,
266
- additionalContext,
267
- continuityState: result.continuity?.state ?? null,
268
- });
269
- return buildHookContextResponse('SessionStart', additionalContext);
270
- };
271
-
272
- const handleUserPromptSubmit = async (input) => {
273
- const result = await smartTurn({
274
- phase: 'start',
275
- prompt: input.prompt,
276
- ensureSession: true,
277
- maxTokens: START_MAX_TOKENS,
278
- });
279
-
280
- const trackedState = await maybeTrackTurn({
281
- hookKey: buildHookKey({ sessionId: input.session_id }),
282
- claudeSessionId: input.session_id,
283
- projectSessionId: result.sessionId ?? null,
284
- prompt: input.prompt,
285
- continuityState: result.continuity?.state ?? '',
286
- });
287
- const additionalContext = buildAdditionalContext({ result });
288
- await recordHookMetrics({
289
- action: 'UserPromptSubmit',
290
- sessionId: trackedState?.projectSessionId ?? result.sessionId ?? null,
291
- additionalContext,
292
- continuityState: result.continuity?.state ?? null,
293
- });
294
- return buildHookContextResponse('UserPromptSubmit', additionalContext);
295
- };
296
-
297
- const handlePostToolUse = async (input) => {
298
- const hookKey = buildHookKey({ sessionId: input.session_id });
299
- const existing = await readHookTurnState(hookKey);
300
- if (!existing) {
301
- return null;
302
- }
303
-
304
- const checkpoint = isCheckpointToolUse({
305
- toolName: input.tool_name,
306
- toolInput: input.tool_input,
307
- });
308
- const touchedFiles = extractTouchedFiles({
309
- toolName: input.tool_name,
310
- toolInput: input.tool_input,
311
- toolResponse: input.tool_response,
312
- });
313
-
314
- const nextState = {
315
- ...existing,
316
- checkpointed: checkpoint.matched ? true : existing.checkpointed,
317
- checkpointEvent: checkpoint.matched ? checkpoint.event : existing.checkpointEvent,
318
- touchedFiles: uniq([...existing.touchedFiles, ...touchedFiles]),
319
- meaningfulWriteCount: existing.meaningfulWriteCount + touchedFiles.length,
320
- updatedAt: new Date().toISOString(),
321
- };
322
-
323
- await maybeSetTrackedTurnState({ hookKey, state: nextState });
324
- if (checkpoint.matched || touchedFiles.length > 0) {
325
- await recordHookMetrics({
326
- action: 'PostToolUse',
327
- sessionId: existing.projectSessionId,
328
- additionalContext: '',
329
- continuityState: existing.continuityState,
330
- });
331
- }
332
- return null;
333
- };
334
-
335
- const handleStop = async (input) => {
336
- const hookKey = buildHookKey({ sessionId: input.session_id });
337
- const state = await readHookTurnState(hookKey);
338
- if (!state) {
339
- return null;
340
- }
341
-
342
- if (getRepoMutationSafety().shouldBlock) {
343
- await recordHookMetrics({
344
- action: 'Stop',
345
- sessionId: state.projectSessionId,
346
- additionalContext: '',
347
- blocked: false,
348
- continuityState: state.continuityState,
349
- });
350
- return null;
351
- }
352
-
353
- const enforcement = computeStopEnforcement(state, input.last_assistant_message);
354
- const shouldEnforce = (state.requireCheckpoint || state.meaningfulWriteCount > 0) && enforcement.shouldBlock;
355
- if (!shouldEnforce || state.checkpointed) {
356
- await recordHookMetrics({
357
- action: 'Stop',
358
- sessionId: state.projectSessionId,
359
- additionalContext: '',
360
- blocked: false,
361
- continuityState: state.continuityState,
362
- });
363
- await maybeDeleteTrackedTurnState({ hookKey });
364
- return null;
365
- }
366
-
367
- if (input.stop_hook_active) {
368
- const update = buildCarryoverUpdate(state, input.last_assistant_message);
369
- if (state.projectSessionId && Object.keys(update).length > 0) {
370
- await smartSummary({
371
- action: 'auto_append',
372
- sessionId: state.projectSessionId,
373
- update,
374
- maxTokens: STOP_MAX_TOKENS,
375
- });
376
- }
377
-
378
- await recordHookMetrics({
379
- action: 'Stop',
380
- sessionId: state.projectSessionId,
381
- additionalContext: '',
382
- blocked: false,
383
- autoAppended: true,
384
- continuityState: state.continuityState,
385
- });
386
- await maybeDeleteTrackedTurnState({ hookKey });
387
- return null;
388
- }
389
-
390
- await recordHookMetrics({
391
- action: 'Stop',
392
- sessionId: state.projectSessionId,
393
- additionalContext: '',
394
- blocked: true,
395
- continuityState: state.continuityState,
396
- });
397
- return {
398
- decision: 'block',
399
- reason: `Persist this turn with mcp__devctx__smart_turn phase=end before stopping.${state.touchedFiles.length > 0 ? ' Include touchedFiles and the nextStep.' : ' Include the nextStep.'}`,
400
- };
401
- };
402
-
403
- export const handleClaudeHookEvent = async (input = {}) => {
404
- const eventName = input.hook_event_name;
405
-
406
- if (eventName === 'SessionStart') {
407
- return handleSessionStart(input);
408
- }
409
-
410
- if (eventName === 'UserPromptSubmit') {
411
- return handleUserPromptSubmit(input);
412
- }
413
-
414
- if (eventName === 'PostToolUse') {
415
- return handlePostToolUse(input);
416
- }
417
-
418
- if (eventName === 'Stop') {
419
- return handleStop(input);
420
- }
421
-
422
- return null;
423
- };
1
+ export { handleClaudeHookEvent } from '../orchestration/adapters/claude-adapter.js';
@@ -0,0 +1 @@
1
+ export { handleCursorHookEvent } from '../orchestration/adapters/cursor-adapter.js';