@claudetools/tools 0.8.3 → 0.8.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/dist/cli.js +41 -0
- package/dist/context/deduplication.d.ts +72 -0
- package/dist/context/deduplication.js +77 -0
- package/dist/context/deduplication.test.d.ts +6 -0
- package/dist/context/deduplication.test.js +84 -0
- package/dist/context/emergency-eviction.d.ts +73 -0
- package/dist/context/emergency-eviction.example.d.ts +13 -0
- package/dist/context/emergency-eviction.example.js +94 -0
- package/dist/context/emergency-eviction.js +226 -0
- package/dist/context/eviction-engine.d.ts +76 -0
- package/dist/context/eviction-engine.example.d.ts +7 -0
- package/dist/context/eviction-engine.example.js +144 -0
- package/dist/context/eviction-engine.js +176 -0
- package/dist/context/example-usage.d.ts +1 -0
- package/dist/context/example-usage.js +128 -0
- package/dist/context/exchange-summariser.d.ts +80 -0
- package/dist/context/exchange-summariser.js +261 -0
- package/dist/context/health-monitor.d.ts +97 -0
- package/dist/context/health-monitor.example.d.ts +1 -0
- package/dist/context/health-monitor.example.js +164 -0
- package/dist/context/health-monitor.js +210 -0
- package/dist/context/importance-scorer.d.ts +94 -0
- package/dist/context/importance-scorer.example.d.ts +1 -0
- package/dist/context/importance-scorer.example.js +140 -0
- package/dist/context/importance-scorer.js +187 -0
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.js +16 -0
- package/dist/context/session-helper.d.ts +10 -0
- package/dist/context/session-helper.js +51 -0
- package/dist/context/session-store.d.ts +94 -0
- package/dist/context/session-store.js +286 -0
- package/dist/context/usage-estimator.d.ts +131 -0
- package/dist/context/usage-estimator.js +260 -0
- package/dist/context/usage-estimator.test.d.ts +1 -0
- package/dist/context/usage-estimator.test.js +208 -0
- package/dist/context-cli.d.ts +16 -0
- package/dist/context-cli.js +309 -0
- package/dist/handlers/codedna-handlers.d.ts +1 -1
- package/dist/handlers/tool-handlers.js +215 -13
- package/dist/helpers/api-client.d.ts +5 -1
- package/dist/helpers/api-client.js +3 -1
- package/dist/helpers/circuit-breaker.d.ts +28 -0
- package/dist/helpers/circuit-breaker.js +97 -0
- package/dist/helpers/compact-formatter.d.ts +2 -0
- package/dist/helpers/compact-formatter.js +6 -0
- package/dist/helpers/error-tracking.js +1 -1
- package/dist/helpers/tasks-retry.d.ts +9 -0
- package/dist/helpers/tasks-retry.js +30 -0
- package/dist/helpers/tasks.d.ts +91 -5
- package/dist/helpers/tasks.js +261 -16
- package/dist/helpers/usage-analytics.js +1 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/post-tool-use-hook-cli.d.ts +2 -0
- package/dist/hooks/post-tool-use-hook-cli.js +34 -0
- package/dist/hooks/post-tool-use.d.ts +67 -0
- package/dist/hooks/post-tool-use.js +234 -0
- package/dist/hooks/stop-hook-cli.d.ts +2 -0
- package/dist/hooks/stop-hook-cli.js +34 -0
- package/dist/hooks/stop.d.ts +64 -0
- package/dist/hooks/stop.js +192 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +4 -0
- package/dist/setup.js +206 -2
- package/dist/tools.js +107 -2
- package/package.json +19 -18
- package/scripts/verify-prompt-compliance.sh +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Context Management CLI Commands
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// CLI interface for context window management: status, evict, summarise, reset
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import prompts from 'prompts';
|
|
8
|
+
import { getSessionStore } from './context/session-store.js';
|
|
9
|
+
import { createEvictionEngine } from './context/eviction-engine.js';
|
|
10
|
+
import { createExchangeSummariser } from './context/exchange-summariser.js';
|
|
11
|
+
// -----------------------------------------------------------------------------
|
|
12
|
+
// Utility Functions
|
|
13
|
+
// -----------------------------------------------------------------------------
|
|
14
|
+
function success(msg) {
|
|
15
|
+
console.log(chalk.green('✓ ') + msg);
|
|
16
|
+
}
|
|
17
|
+
function error(msg) {
|
|
18
|
+
console.log(chalk.red('✗ ') + msg);
|
|
19
|
+
}
|
|
20
|
+
function info(msg) {
|
|
21
|
+
console.log(chalk.blue('ℹ ') + msg);
|
|
22
|
+
}
|
|
23
|
+
function warn(msg) {
|
|
24
|
+
console.log(chalk.yellow('⚠ ') + msg);
|
|
25
|
+
}
|
|
26
|
+
function header(title) {
|
|
27
|
+
console.log('\n' + chalk.cyan('━'.repeat(50)));
|
|
28
|
+
console.log(chalk.cyan.bold(title));
|
|
29
|
+
console.log(chalk.cyan('━'.repeat(50)) + '\n');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get session ID from args or prompt user to select
|
|
33
|
+
*/
|
|
34
|
+
async function getSessionId(args) {
|
|
35
|
+
// Check if session ID provided as argument
|
|
36
|
+
const sessionIdArg = args.find((arg) => arg.startsWith('--session='));
|
|
37
|
+
if (sessionIdArg) {
|
|
38
|
+
return sessionIdArg.split('=')[1];
|
|
39
|
+
}
|
|
40
|
+
// List available sessions
|
|
41
|
+
const store = getSessionStore();
|
|
42
|
+
const sessions = await store.listSessions();
|
|
43
|
+
if (sessions.length === 0) {
|
|
44
|
+
error('No active sessions found');
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
// If only one session, use it
|
|
48
|
+
if (sessions.length === 1) {
|
|
49
|
+
return sessions[0].session_id;
|
|
50
|
+
}
|
|
51
|
+
// Prompt user to select
|
|
52
|
+
const choices = sessions.map((s) => ({
|
|
53
|
+
title: `${s.session_id} (${s.model}, started ${s.started_at.toLocaleString()})`,
|
|
54
|
+
value: s.session_id,
|
|
55
|
+
}));
|
|
56
|
+
const response = await prompts({
|
|
57
|
+
type: 'select',
|
|
58
|
+
name: 'sessionId',
|
|
59
|
+
message: 'Select a session:',
|
|
60
|
+
choices,
|
|
61
|
+
});
|
|
62
|
+
return response.sessionId || null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Format fill percentage with colour coding
|
|
66
|
+
*/
|
|
67
|
+
function formatFill(fill) {
|
|
68
|
+
const percentage = (fill * 100).toFixed(1);
|
|
69
|
+
if (fill < 0.5) {
|
|
70
|
+
return chalk.green(`${percentage}%`);
|
|
71
|
+
}
|
|
72
|
+
else if (fill < 0.7) {
|
|
73
|
+
return chalk.yellow(`${percentage}%`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
return chalk.red(`${percentage}%`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Format session summary
|
|
81
|
+
*/
|
|
82
|
+
function formatSessionSummary(session) {
|
|
83
|
+
console.log(chalk.bold('Session ID:'), session.session_id);
|
|
84
|
+
console.log(chalk.bold('Model:'), session.model);
|
|
85
|
+
console.log(chalk.bold('Started:'), session.started_at.toLocaleString());
|
|
86
|
+
console.log(chalk.bold('Context Limit:'), session.context_limit.toLocaleString(), 'tokens');
|
|
87
|
+
console.log(chalk.bold('Estimated Fill:'), formatFill(session.estimated_fill));
|
|
88
|
+
console.log(chalk.bold('Used Tokens:'), Math.round(session.estimated_fill * session.context_limit).toLocaleString());
|
|
89
|
+
console.log(chalk.bold('Injected Facts:'), session.injected_facts.length);
|
|
90
|
+
console.log(chalk.bold('Exchanges:'), session.exchanges.length);
|
|
91
|
+
// Show exchange summary
|
|
92
|
+
const summarisedCount = session.exchanges.filter((ex) => ex.summarised_at).length;
|
|
93
|
+
if (summarisedCount > 0) {
|
|
94
|
+
console.log(chalk.bold('Summarised Exchanges:'), summarisedCount);
|
|
95
|
+
}
|
|
96
|
+
console.log(chalk.bold('Last Updated:'), session.last_updated.toLocaleString());
|
|
97
|
+
}
|
|
98
|
+
// -----------------------------------------------------------------------------
|
|
99
|
+
// Commands
|
|
100
|
+
// -----------------------------------------------------------------------------
|
|
101
|
+
/**
|
|
102
|
+
* claudetools context status - Show current session context usage
|
|
103
|
+
*/
|
|
104
|
+
export async function contextStatus(args) {
|
|
105
|
+
header('Context Status');
|
|
106
|
+
const sessionId = await getSessionId(args);
|
|
107
|
+
if (!sessionId) {
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const spinner = ora('Loading session state...').start();
|
|
111
|
+
try {
|
|
112
|
+
const store = getSessionStore();
|
|
113
|
+
const session = await store.getSession(sessionId);
|
|
114
|
+
if (!session) {
|
|
115
|
+
spinner.fail('Session not found');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
spinner.succeed('Session loaded');
|
|
119
|
+
console.log();
|
|
120
|
+
formatSessionSummary(session);
|
|
121
|
+
// Show warnings
|
|
122
|
+
console.log();
|
|
123
|
+
if (session.estimated_fill > 0.85) {
|
|
124
|
+
warn('Critical: Context window is near full (>85%). Emergency eviction may be triggered.');
|
|
125
|
+
}
|
|
126
|
+
else if (session.estimated_fill > 0.6) {
|
|
127
|
+
warn('Warning: Context window is filling up (>60%). Consider running eviction.');
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
success('Context window is healthy.');
|
|
131
|
+
}
|
|
132
|
+
// Show eviction eligibility
|
|
133
|
+
const engine = createEvictionEngine();
|
|
134
|
+
if (engine.shouldEvict(session)) {
|
|
135
|
+
console.log();
|
|
136
|
+
info('Automatic eviction will be triggered at next opportunity.');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
spinner.fail('Failed to load session');
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* claudetools context evict - Manually trigger eviction cycle
|
|
146
|
+
*/
|
|
147
|
+
export async function contextEvict(args) {
|
|
148
|
+
header('Context Eviction');
|
|
149
|
+
const sessionId = await getSessionId(args);
|
|
150
|
+
if (!sessionId) {
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
const spinner = ora('Loading session state...').start();
|
|
154
|
+
try {
|
|
155
|
+
const store = getSessionStore();
|
|
156
|
+
const session = await store.getSession(sessionId);
|
|
157
|
+
if (!session) {
|
|
158
|
+
spinner.fail('Session not found');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
spinner.text = 'Analysing session...';
|
|
162
|
+
// Check if eviction is needed
|
|
163
|
+
const engine = createEvictionEngine();
|
|
164
|
+
if (!engine.shouldEvict(session)) {
|
|
165
|
+
spinner.info(`No eviction needed (fill: ${formatFill(session.estimated_fill)})`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Get eviction plan
|
|
169
|
+
const plan = engine.getEvictionPlan(session);
|
|
170
|
+
spinner.stop();
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(chalk.bold('Eviction Plan:'));
|
|
173
|
+
console.log(chalk.bold('Facts to evict:'), plan.factsToEvict.length);
|
|
174
|
+
console.log(chalk.bold('Current fill:'), formatFill(session.estimated_fill));
|
|
175
|
+
console.log(chalk.bold('Expected fill after:'), formatFill(plan.expectedFillAfter));
|
|
176
|
+
if (plan.includesCritical) {
|
|
177
|
+
warn('Plan includes evicting CRITICAL facts (fill >85%)');
|
|
178
|
+
}
|
|
179
|
+
// Confirm before proceeding
|
|
180
|
+
const confirm = await prompts({
|
|
181
|
+
type: 'confirm',
|
|
182
|
+
name: 'proceed',
|
|
183
|
+
message: 'Proceed with eviction?',
|
|
184
|
+
initial: true,
|
|
185
|
+
});
|
|
186
|
+
if (!confirm.proceed) {
|
|
187
|
+
info('Eviction cancelled');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
spinner.start('Evicting facts...');
|
|
191
|
+
// Execute eviction
|
|
192
|
+
const result = await engine.runEviction(session);
|
|
193
|
+
spinner.succeed('Eviction complete');
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(chalk.bold('Evicted:'), result.evictedCount, 'facts');
|
|
196
|
+
console.log(chalk.bold('New fill:'), formatFill(result.newEstimatedFill));
|
|
197
|
+
// Update session in store
|
|
198
|
+
await store.updateSession(sessionId, { estimated_fill: result.newEstimatedFill });
|
|
199
|
+
success(`Context window reduced from ${formatFill(session.estimated_fill)} to ${formatFill(result.newEstimatedFill)}`);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
spinner.fail('Eviction failed');
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* claudetools context summarise - Summarise and compress exchanges
|
|
208
|
+
*/
|
|
209
|
+
export async function contextSummarise(args) {
|
|
210
|
+
header('Exchange Summarisation');
|
|
211
|
+
const sessionId = await getSessionId(args);
|
|
212
|
+
if (!sessionId) {
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
const spinner = ora('Loading session state...').start();
|
|
216
|
+
try {
|
|
217
|
+
const store = getSessionStore();
|
|
218
|
+
const session = await store.getSession(sessionId);
|
|
219
|
+
if (!session) {
|
|
220
|
+
spinner.fail('Session not found');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
spinner.text = 'Analysing exchanges...';
|
|
224
|
+
// Check if summarisation is needed
|
|
225
|
+
const summariser = createExchangeSummariser();
|
|
226
|
+
if (!summariser.shouldSummarise(session)) {
|
|
227
|
+
spinner.info('No exchanges need summarisation (< 10 unsummarised)');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
spinner.stop();
|
|
231
|
+
console.log();
|
|
232
|
+
const unsummarised = session.exchanges.filter((ex) => !ex.summarised_at);
|
|
233
|
+
console.log(chalk.bold('Unsummarised exchanges:'), unsummarised.length);
|
|
234
|
+
console.log(chalk.bold('Total exchanges:'), session.exchanges.length);
|
|
235
|
+
// Confirm before proceeding
|
|
236
|
+
const confirm = await prompts({
|
|
237
|
+
type: 'confirm',
|
|
238
|
+
name: 'proceed',
|
|
239
|
+
message: 'Proceed with summarisation?',
|
|
240
|
+
initial: true,
|
|
241
|
+
});
|
|
242
|
+
if (!confirm.proceed) {
|
|
243
|
+
info('Summarisation cancelled');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
spinner.start('Summarising exchanges...');
|
|
247
|
+
// Execute summarisation
|
|
248
|
+
const result = await summariser.summariseOldExchanges(session);
|
|
249
|
+
spinner.succeed('Summarisation complete');
|
|
250
|
+
console.log();
|
|
251
|
+
console.log(chalk.bold('Summarised:'), result.summarisedCount, 'exchanges');
|
|
252
|
+
console.log(chalk.bold('Tokens saved:'), result.tokensSaved.toLocaleString());
|
|
253
|
+
// Mark exchanges as summarised in store
|
|
254
|
+
for (let i = 0; i < result.summarisedCount; i++) {
|
|
255
|
+
await store.markExchangeSummarised(sessionId, i);
|
|
256
|
+
}
|
|
257
|
+
success(`Compressed ${result.summarisedCount} exchanges, saved ~${result.tokensSaved} tokens`);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
spinner.fail('Summarisation failed');
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* claudetools context reset - Clear session state
|
|
266
|
+
*/
|
|
267
|
+
export async function contextReset(args) {
|
|
268
|
+
header('Context Reset');
|
|
269
|
+
const sessionId = await getSessionId(args);
|
|
270
|
+
if (!sessionId) {
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
const spinner = ora('Loading session state...').start();
|
|
274
|
+
try {
|
|
275
|
+
const store = getSessionStore();
|
|
276
|
+
const session = await store.getSession(sessionId);
|
|
277
|
+
if (!session) {
|
|
278
|
+
spinner.fail('Session not found');
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
spinner.stop();
|
|
282
|
+
console.log();
|
|
283
|
+
formatSessionSummary(session);
|
|
284
|
+
console.log();
|
|
285
|
+
warn('This will permanently delete all session data including:');
|
|
286
|
+
console.log(' - Injected facts');
|
|
287
|
+
console.log(' - Exchange history');
|
|
288
|
+
console.log(' - Token usage estimates');
|
|
289
|
+
// Confirm before proceeding
|
|
290
|
+
const confirm = await prompts({
|
|
291
|
+
type: 'confirm',
|
|
292
|
+
name: 'proceed',
|
|
293
|
+
message: chalk.red('Are you sure you want to delete this session?'),
|
|
294
|
+
initial: false,
|
|
295
|
+
});
|
|
296
|
+
if (!confirm.proceed) {
|
|
297
|
+
info('Reset cancelled');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
spinner.start('Deleting session...');
|
|
301
|
+
await store.deleteSession(sessionId);
|
|
302
|
+
spinner.succeed('Session deleted');
|
|
303
|
+
success('Context state cleared');
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
spinner.fail('Reset failed');
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -369,7 +369,7 @@ export declare function handleInitProject(args: {
|
|
|
369
369
|
patterns: {
|
|
370
370
|
pattern_id: string;
|
|
371
371
|
name: string;
|
|
372
|
-
category: "hooks" | "
|
|
372
|
+
category: "hooks" | "state" | "components" | "forms" | "validation" | "styling" | "anti-patterns";
|
|
373
373
|
description: string;
|
|
374
374
|
}[];
|
|
375
375
|
summary: {
|
|
@@ -11,8 +11,8 @@ import { recordToolCall, getToolCallWarnings } from '../helpers/session-validati
|
|
|
11
11
|
import { queryDependencies, analyzeImpact } from '../helpers/dependencies.js';
|
|
12
12
|
import { checkPatterns } from '../helpers/patterns.js';
|
|
13
13
|
import { formatContextForClaude } from '../helpers/formatter.js';
|
|
14
|
-
import { compactTaskList, compactTaskCreated, compactTaskStart, compactTaskComplete, compactTaskClaim, compactTaskRelease, compactStatusUpdate, compactContextAdded, compactHeartbeat, compactSummary, } from '../helpers/compact-formatter.js';
|
|
15
|
-
import { createTask, listTasks, getTask, claimTask, releaseTask, updateTaskStatus, addTaskContext, getTaskSummary, heartbeatTask, parseJsonArray, getDispatchableTasks, getExecutionContext, resolveTaskDependencies, getEpicStatus, getActiveTaskCount, } from '../helpers/tasks.js';
|
|
14
|
+
import { shortId, compactTaskList, compactTaskCreated, compactTaskStart, compactTaskComplete, compactTaskClaim, compactTaskRelease, compactStatusUpdate, compactContextAdded, compactHeartbeat, compactTaskHandoff, compactSummary, } from '../helpers/compact-formatter.js';
|
|
15
|
+
import { createTask, listTasks, getTask, claimTask, releaseTask, updateTaskStatus, addTaskContext, getTaskSummary, heartbeatTask, handoffTask, parseJsonArray, getDispatchableTasks, getExecutionContext, resolveTaskDependencies, getEpicStatus, getEpicAggregate, getActiveTaskCount, reviseEpic, } from '../helpers/tasks.js';
|
|
16
16
|
import { detectTimedOutTasks, retryTask, failTask, autoRetryTimedOutTasks, } from '../helpers/tasks-retry.js';
|
|
17
17
|
import { detectLibrariesFromPlan } from '../helpers/library-detection.js';
|
|
18
18
|
import { handleGenerateApi, handleGenerateFrontend, handleGenerateComponent, handleListGenerators, handleValidateSpec, handleListPatterns, handleGetPattern, handleDetectPatterns, handleInitProject, } from './codedna-handlers.js';
|
|
@@ -132,17 +132,89 @@ export function registerToolHandlers(server) {
|
|
|
132
132
|
const relationship = args?.relationship;
|
|
133
133
|
const entity2 = args?.entity2;
|
|
134
134
|
const context = args?.context;
|
|
135
|
-
const
|
|
135
|
+
const is_critical = args?.is_critical;
|
|
136
|
+
// Commercial-grade storage with blocking verification
|
|
137
|
+
const MAX_RETRIES = 3;
|
|
138
|
+
const VERIFY_DELAY_MS = 200;
|
|
139
|
+
let lastError = null;
|
|
140
|
+
let storedFactId = null;
|
|
141
|
+
let storedIsCritical = false;
|
|
142
|
+
let verified = false;
|
|
143
|
+
let attempts = 0;
|
|
144
|
+
for (let attempt = 1; attempt <= MAX_RETRIES && !verified; attempt++) {
|
|
145
|
+
attempts = attempt;
|
|
146
|
+
try {
|
|
147
|
+
// Step 1: Store the fact
|
|
148
|
+
mcpLogger.info('STORE', `Attempt ${attempt}/${MAX_RETRIES}: Storing "${entity1} ${relationship} ${entity2}"${is_critical ? ' [CRITICAL]' : ''}`);
|
|
149
|
+
const result = await storeFact(projectId, entity1, relationship, entity2, context, { is_critical });
|
|
150
|
+
storedFactId = result.fact_id;
|
|
151
|
+
storedIsCritical = result.is_critical;
|
|
152
|
+
mcpLogger.info('STORE', `Storage response: ${JSON.stringify(result)}`);
|
|
153
|
+
if (!result.success || !result.fact_id) {
|
|
154
|
+
lastError = new Error(`Storage returned unsuccessful: ${JSON.stringify(result)}`);
|
|
155
|
+
mcpLogger.warn('STORE', `Attempt ${attempt} failed: ${lastError.message}`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// Step 2: Wait briefly for eventual consistency
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, VERIFY_DELAY_MS));
|
|
160
|
+
// Step 3: Verify the fact is retrievable by searching for it
|
|
161
|
+
mcpLogger.info('STORE', `Verifying fact ${storedFactId} is retrievable...`);
|
|
162
|
+
const searchQuery = `${entity1} ${relationship} ${entity2}`;
|
|
163
|
+
const searchResult = await searchMemory(projectId, searchQuery, 5);
|
|
164
|
+
// Check if our fact appears in results
|
|
165
|
+
const factFound = searchResult.relevant_facts?.some(f => f.fact?.includes(entity1) && f.fact?.includes(entity2)) || false;
|
|
166
|
+
if (factFound) {
|
|
167
|
+
verified = true;
|
|
168
|
+
mcpLogger.info('STORE', `✓ Fact verified as retrievable`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
lastError = new Error(`Fact stored but not found in search results`);
|
|
172
|
+
mcpLogger.warn('STORE', `Attempt ${attempt}: Stored but not retrievable. Search returned ${searchResult.relevant_facts?.length || 0} facts.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
177
|
+
mcpLogger.error('STORE', `Attempt ${attempt} error: ${lastError.message}`, err);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
136
180
|
mcpLogger.memoryStore(entity1, relationship, entity2);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
const criticalTag = storedIsCritical ? ' 🔴 CRITICAL' : '';
|
|
182
|
+
if (verified && storedFactId) {
|
|
183
|
+
mcpLogger.toolResult(name, true, timer(), `ID: ${storedFactId} (verified)${criticalTag}`);
|
|
184
|
+
return {
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: 'text',
|
|
188
|
+
text: `✓ Stored and verified: "${entity1} ${relationship} ${entity2}" (ID: ${storedFactId})${criticalTag}`,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
else if (storedFactId) {
|
|
194
|
+
// Stored but couldn't verify - warn but don't fail
|
|
195
|
+
mcpLogger.toolResult(name, true, timer(), `ID: ${storedFactId} (unverified)${criticalTag}`);
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: 'text',
|
|
200
|
+
text: `⚠️ Stored but verification pending: "${entity1} ${relationship} ${entity2}" (ID: ${storedFactId})${criticalTag}\nNote: Fact may take a moment to become searchable.`,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Complete failure after all retries
|
|
207
|
+
mcpLogger.toolResult(name, false, timer(), `Failed after ${attempts} attempts`);
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: 'text',
|
|
212
|
+
text: `❌ Failed to store fact after ${attempts} attempts: "${entity1} ${relationship} ${entity2}"\nError: ${lastError?.message || 'Unknown error'}`,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
isError: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
146
218
|
}
|
|
147
219
|
case 'memory_get_context': {
|
|
148
220
|
const query = args?.query;
|
|
@@ -755,6 +827,73 @@ export function registerToolHandlers(server) {
|
|
|
755
827
|
content: [{ type: 'text', text: output }],
|
|
756
828
|
};
|
|
757
829
|
}
|
|
830
|
+
case 'task_plan_revise': {
|
|
831
|
+
const epicId = args?.epic_id;
|
|
832
|
+
const addTasks = args?.add_tasks;
|
|
833
|
+
const removeTaskIds = args?.remove_task_ids;
|
|
834
|
+
const updateTasks = args?.update_tasks;
|
|
835
|
+
try {
|
|
836
|
+
const result = await reviseEpic(DEFAULT_USER_ID, projectId, epicId, {
|
|
837
|
+
add_tasks: addTasks,
|
|
838
|
+
remove_task_ids: removeTaskIds,
|
|
839
|
+
update_tasks: updateTasks,
|
|
840
|
+
});
|
|
841
|
+
mcpLogger.toolResult(name, true, timer(), `Epic ${shortId(epicId)} revised: +${result.added.length} tasks, -${result.removed.length} tasks, ~${result.updated.length} tasks`);
|
|
842
|
+
let output = `# Epic Revised: ${result.epic.title}\n\n`;
|
|
843
|
+
output += `**Epic ID:** \`${result.epic.id}\`\n`;
|
|
844
|
+
output += `**Status:** ${result.epic.status}\n\n`;
|
|
845
|
+
if (result.added.length > 0) {
|
|
846
|
+
output += `## Added Tasks (${result.added.length})\n\n`;
|
|
847
|
+
result.added.forEach((task, i) => {
|
|
848
|
+
output += `${i + 1}. **${task.title}** (\`${shortId(task.id)}\`)`;
|
|
849
|
+
if (task.estimated_effort)
|
|
850
|
+
output += ` - ${task.estimated_effort}`;
|
|
851
|
+
output += `\n`;
|
|
852
|
+
if (task.description)
|
|
853
|
+
output += ` ${task.description}\n`;
|
|
854
|
+
});
|
|
855
|
+
output += '\n';
|
|
856
|
+
}
|
|
857
|
+
if (result.removed.length > 0) {
|
|
858
|
+
output += `## Cancelled Tasks (${result.removed.length})\n\n`;
|
|
859
|
+
result.removed.forEach((taskId, i) => {
|
|
860
|
+
output += `${i + 1}. \`${shortId(taskId)}\` (cancelled)\n`;
|
|
861
|
+
});
|
|
862
|
+
output += '\n';
|
|
863
|
+
}
|
|
864
|
+
if (result.updated.length > 0) {
|
|
865
|
+
output += `## Updated Tasks (${result.updated.length})\n\n`;
|
|
866
|
+
result.updated.forEach((task, i) => {
|
|
867
|
+
output += `${i + 1}. **${task.title}** (\`${shortId(task.id)}\`) - Context added with update details\n`;
|
|
868
|
+
});
|
|
869
|
+
output += '\n';
|
|
870
|
+
}
|
|
871
|
+
// Get updated epic status
|
|
872
|
+
const epicStatus = await getEpicStatus(DEFAULT_USER_ID, projectId, epicId);
|
|
873
|
+
output += `## Current Status\n\n`;
|
|
874
|
+
output += `**Total Tasks:** ${epicStatus.totalTasks}\n`;
|
|
875
|
+
output += `**Progress:** ${epicStatus.percentComplete}% complete\n`;
|
|
876
|
+
output += `**By Status:**\n`;
|
|
877
|
+
Object.entries(epicStatus.byStatus).forEach(([status, count]) => {
|
|
878
|
+
if (count > 0) {
|
|
879
|
+
output += `- ${status}: ${count}\n`;
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
return {
|
|
883
|
+
content: [{ type: 'text', text: output }],
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
mcpLogger.toolResult(name, false, timer(), error instanceof Error ? error.message : 'Unknown error');
|
|
888
|
+
return {
|
|
889
|
+
content: [{
|
|
890
|
+
type: 'text',
|
|
891
|
+
text: `Error revising epic: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
892
|
+
}],
|
|
893
|
+
isError: true,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
758
897
|
case 'task_start': {
|
|
759
898
|
const taskId = args?.task_id;
|
|
760
899
|
const agentId = args?.agent_id || 'claude-code';
|
|
@@ -976,6 +1115,19 @@ export function registerToolHandlers(server) {
|
|
|
976
1115
|
content: [{ type: 'text', text: output }],
|
|
977
1116
|
};
|
|
978
1117
|
}
|
|
1118
|
+
case 'task_handoff': {
|
|
1119
|
+
const taskId = args?.task_id;
|
|
1120
|
+
const newWorkerType = args?.new_worker_type;
|
|
1121
|
+
const reason = args?.reason;
|
|
1122
|
+
const agentId = args?.agent_id || 'claude-code';
|
|
1123
|
+
const result = await handoffTask(DEFAULT_USER_ID, projectId, taskId, agentId, newWorkerType, reason);
|
|
1124
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1125
|
+
// Compact output
|
|
1126
|
+
const output = compactTaskHandoff(result.data.task, result.data.handed_off, result.data.new_worker_type);
|
|
1127
|
+
return {
|
|
1128
|
+
content: [{ type: 'text', text: output }],
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
979
1131
|
// =========================================================================
|
|
980
1132
|
// ORCHESTRATION HANDLERS
|
|
981
1133
|
// =========================================================================
|
|
@@ -1028,7 +1180,12 @@ export function registerToolHandlers(server) {
|
|
|
1028
1180
|
output += `**Task:** ${context.task.title}\n`;
|
|
1029
1181
|
output += `**Task ID:** \`${context.task.id}\`\n`;
|
|
1030
1182
|
output += `**Worker Type:** ${context.worker.name} (\`${context.worker.id}\`)\n`;
|
|
1031
|
-
output += `**Status:** ${context.task.status}\n
|
|
1183
|
+
output += `**Status:** ${context.task.status}\n`;
|
|
1184
|
+
// Display lock expiry warning if present
|
|
1185
|
+
if (context.lockWarning?.warning && context.lockWarning.message) {
|
|
1186
|
+
output += `\n${context.lockWarning.message}\n`;
|
|
1187
|
+
}
|
|
1188
|
+
output += `\n`;
|
|
1032
1189
|
output += `## System Prompt for Worker\n\n`;
|
|
1033
1190
|
output += `\`\`\`\n${context.systemPrompt}\`\`\`\n\n`;
|
|
1034
1191
|
if (context.parentTask) {
|
|
@@ -1152,6 +1309,47 @@ export function registerToolHandlers(server) {
|
|
|
1152
1309
|
content: [{ type: 'text', text: output }],
|
|
1153
1310
|
};
|
|
1154
1311
|
}
|
|
1312
|
+
case 'task_aggregate': {
|
|
1313
|
+
const epicId = args?.epic_id;
|
|
1314
|
+
const includePending = args?.include_pending || false;
|
|
1315
|
+
const aggregate = await getEpicAggregate(DEFAULT_USER_ID, projectId, epicId, includePending);
|
|
1316
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1317
|
+
let output = `# Epic Aggregate: ${aggregate.epic.title}\n\n`;
|
|
1318
|
+
output += `**Epic ID:** \`${epicId}\`\n`;
|
|
1319
|
+
output += `**Epic Status:** ${aggregate.epic.status}\n`;
|
|
1320
|
+
output += `**Description:** ${aggregate.epic.description}\n\n`;
|
|
1321
|
+
output += `## Summary Statistics\n\n`;
|
|
1322
|
+
output += `- **Total Tasks:** ${aggregate.summary_stats.total}\n`;
|
|
1323
|
+
output += `- **Completed:** ${aggregate.summary_stats.completed}\n`;
|
|
1324
|
+
output += `- **In Progress:** ${aggregate.summary_stats.in_progress}\n`;
|
|
1325
|
+
output += `- **Pending:** ${aggregate.summary_stats.pending}\n\n`;
|
|
1326
|
+
output += `## Task Work Logs\n\n`;
|
|
1327
|
+
if (aggregate.tasks.length === 0) {
|
|
1328
|
+
output += `No tasks ${includePending ? '' : 'with work logs '}found for this epic.\n`;
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
for (const task of aggregate.tasks) {
|
|
1332
|
+
const statusEmoji = task.status === 'done' ? '✅' : task.status === 'in_progress' ? '🔄' : '📋';
|
|
1333
|
+
output += `### ${statusEmoji} ${task.title}\n`;
|
|
1334
|
+
output += `- **Task ID:** \`${task.id}\`\n`;
|
|
1335
|
+
output += `- **Status:** ${task.status}\n`;
|
|
1336
|
+
if (task.completed_at) {
|
|
1337
|
+
const completedDate = new Date(task.completed_at).toLocaleString();
|
|
1338
|
+
output += `- **Completed:** ${completedDate}\n`;
|
|
1339
|
+
}
|
|
1340
|
+
if (task.work_log) {
|
|
1341
|
+
output += `- **Work Log:**\n\n`;
|
|
1342
|
+
output += ` ${task.work_log}\n\n`;
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
output += `- **Work Log:** None\n\n`;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return {
|
|
1350
|
+
content: [{ type: 'text', text: output }],
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1155
1353
|
case 'task_detect_timeouts': {
|
|
1156
1354
|
const timedOut = await detectTimedOutTasks(DEFAULT_USER_ID, projectId);
|
|
1157
1355
|
mcpLogger.toolResult(name, true, timer());
|
|
@@ -1206,6 +1404,10 @@ export function registerToolHandlers(server) {
|
|
|
1206
1404
|
if (result.error?.includes('Retry limit exceeded')) {
|
|
1207
1405
|
output += `The task has been marked as **failed** permanently.\n`;
|
|
1208
1406
|
}
|
|
1407
|
+
else if (result.retryAfter) {
|
|
1408
|
+
output += `**Retry After:** ${result.retryAfter}\n`;
|
|
1409
|
+
output += `The task is in exponential backoff. Please wait before retrying.\n`;
|
|
1410
|
+
}
|
|
1209
1411
|
}
|
|
1210
1412
|
return {
|
|
1211
1413
|
content: [{ type: 'text', text: output }],
|
|
@@ -26,9 +26,13 @@ export declare function addMemory(projectId: string, sessionId: string, messages
|
|
|
26
26
|
episode_ids: string[];
|
|
27
27
|
}>;
|
|
28
28
|
export declare function searchMemory(projectId: string, query: string, limit?: number, userId?: string): Promise<MemoryContext>;
|
|
29
|
-
export declare function storeFact(projectId: string, entity1: string, relationship: string, entity2: string, context: string,
|
|
29
|
+
export declare function storeFact(projectId: string, entity1: string, relationship: string, entity2: string, context: string, options?: {
|
|
30
|
+
userId?: string;
|
|
31
|
+
is_critical?: boolean;
|
|
32
|
+
}): Promise<{
|
|
30
33
|
success: boolean;
|
|
31
34
|
fact_id: string;
|
|
35
|
+
is_critical: boolean;
|
|
32
36
|
}>;
|
|
33
37
|
export declare function getContext(projectId: string, query?: string, userId?: string): Promise<MemoryContext>;
|
|
34
38
|
export declare function getSummary(projectId: string, userId?: string): Promise<string>;
|
|
@@ -39,12 +39,14 @@ export async function searchMemory(projectId, query, limit = 10, userId = DEFAUL
|
|
|
39
39
|
limit,
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
export async function storeFact(projectId, entity1, relationship, entity2, context,
|
|
42
|
+
export async function storeFact(projectId, entity1, relationship, entity2, context, options = {}) {
|
|
43
|
+
const userId = options.userId ?? DEFAULT_USER_ID;
|
|
43
44
|
return apiRequest(`/api/v1/memory/${userId}/${projectId}/fact`, 'POST', {
|
|
44
45
|
entity1,
|
|
45
46
|
relationship,
|
|
46
47
|
entity2,
|
|
47
48
|
context,
|
|
49
|
+
...(options.is_critical !== undefined && { is_critical: options.is_critical }),
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
52
|
export async function getContext(projectId, query, userId = DEFAULT_USER_ID) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface CircuitState {
|
|
2
|
+
failures: number;
|
|
3
|
+
lastFailure: number;
|
|
4
|
+
openedAt: number | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Record a worker failure
|
|
8
|
+
* Increments failure count and potentially opens the circuit
|
|
9
|
+
*/
|
|
10
|
+
export declare function recordWorkerFailure(workerType: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Check if circuit is open for a worker type
|
|
13
|
+
* Auto-closes circuit if cooldown period has elapsed
|
|
14
|
+
*/
|
|
15
|
+
export declare function isCircuitOpen(workerType: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Get circuit status for all worker types
|
|
18
|
+
* Useful for monitoring and debugging
|
|
19
|
+
*/
|
|
20
|
+
export declare function getCircuitStatus(): Map<string, CircuitState>;
|
|
21
|
+
/**
|
|
22
|
+
* Manually reset a circuit (for testing or admin override)
|
|
23
|
+
*/
|
|
24
|
+
export declare function resetCircuit(workerType: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Reset all circuits (for testing)
|
|
27
|
+
*/
|
|
28
|
+
export declare function resetAllCircuits(): void;
|