@claudetools/tools 0.8.4 → 0.8.6

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.
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Result of a single test execution
3
+ */
4
+ export interface TestResult {
5
+ /** Test name/description */
6
+ name: string;
7
+ /** Whether the test passed */
8
+ passed: boolean;
9
+ /** Duration in milliseconds */
10
+ duration_ms: number;
11
+ /** Error details if test failed */
12
+ error?: {
13
+ message: string;
14
+ stack?: string;
15
+ };
16
+ }
17
+ /**
18
+ * Summary of test suite execution
19
+ */
20
+ export interface TestSuiteSummary {
21
+ /** Total number of tests */
22
+ total: number;
23
+ /** Number of passed tests */
24
+ passed: number;
25
+ /** Number of failed tests */
26
+ failed: number;
27
+ /** Total duration in milliseconds */
28
+ total_duration_ms: number;
29
+ /** Average duration per test in milliseconds */
30
+ avg_duration_ms: number;
31
+ /** Pass rate as percentage (0-100) */
32
+ pass_rate: number;
33
+ }
34
+ /**
35
+ * Complete test suite results
36
+ */
37
+ export interface TestSuite {
38
+ /** Suite name */
39
+ name: string;
40
+ /** Individual test results */
41
+ tests: TestResult[];
42
+ /** Aggregated summary */
43
+ summary: TestSuiteSummary;
44
+ /** Timestamp when suite started */
45
+ started_at: string;
46
+ /** Timestamp when suite completed */
47
+ completed_at: string;
48
+ }
49
+ /**
50
+ * Test function signature
51
+ */
52
+ export type TestFunction = () => Promise<void> | void;
53
+ /**
54
+ * Test definition
55
+ */
56
+ export interface TestDefinition {
57
+ name: string;
58
+ fn: TestFunction;
59
+ }
60
+ /**
61
+ * Run a single test with timing and error handling
62
+ *
63
+ * @param name - Test name/description
64
+ * @param fn - Test function to execute
65
+ * @returns Test result with pass/fail, duration, and error details
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const result = await runTest('should store and retrieve fact', async () => {
70
+ * const fact = await storeFact('test', 'RELATES_TO', 'value');
71
+ * assert(fact !== null);
72
+ * });
73
+ *
74
+ * if (!result.passed) {
75
+ * console.error(`Test failed: ${result.error?.message}`);
76
+ * }
77
+ * ```
78
+ */
79
+ export declare function runTest(name: string, fn: TestFunction): Promise<TestResult>;
80
+ /**
81
+ * Run a suite of tests
82
+ *
83
+ * @param name - Suite name
84
+ * @param tests - Array of test definitions
85
+ * @returns Complete test suite results with summary
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * const suite = await runSuite('Memory Integration Tests', [
90
+ * { name: 'should store fact', fn: async () => { ... } },
91
+ * { name: 'should search facts', fn: async () => { ... } },
92
+ * { name: 'should inject context', fn: async () => { ... } },
93
+ * ]);
94
+ *
95
+ * console.log(formatResults(suite));
96
+ *
97
+ * if (suite.summary.failed > 0) {
98
+ * process.exit(1);
99
+ * }
100
+ * ```
101
+ */
102
+ export declare function runSuite(name: string, tests: TestDefinition[]): Promise<TestSuite>;
103
+ /**
104
+ * Format test suite results as human-readable text
105
+ *
106
+ * @param suite - Test suite results
107
+ * @returns Formatted string with test results and summary
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * const suite = await runSuite('My Tests', [...]);
112
+ * console.log(formatResults(suite));
113
+ * ```
114
+ *
115
+ * Output:
116
+ * ```
117
+ * ================================================================================
118
+ * Test Suite: My Tests
119
+ * ================================================================================
120
+ *
121
+ * ✓ should pass test 1 (45.32ms)
122
+ * ✗ should fail test 2 (12.18ms)
123
+ * Error: Expected value to be true
124
+ *
125
+ * --------------------------------------------------------------------------------
126
+ * Summary
127
+ * --------------------------------------------------------------------------------
128
+ * Total: 2
129
+ * Passed: 1
130
+ * Failed: 1
131
+ * Pass Rate: 50.00%
132
+ * Duration: 57.50ms (avg: 28.75ms)
133
+ * ```
134
+ */
135
+ export declare function formatResults(suite: TestSuite): string;
136
+ /**
137
+ * Format test suite results as JSON
138
+ *
139
+ * @param suite - Test suite results
140
+ * @param pretty - Whether to pretty-print the JSON (default: true)
141
+ * @returns JSON string
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * const suite = await runSuite('My Tests', [...]);
146
+ * const json = formatResultsJson(suite);
147
+ * await fs.writeFile('test-results.json', json);
148
+ * ```
149
+ *
150
+ * Output:
151
+ * ```json
152
+ * {
153
+ * "name": "My Tests",
154
+ * "started_at": "2025-01-01T12:00:00.000Z",
155
+ * "completed_at": "2025-01-01T12:00:01.234Z",
156
+ * "summary": {
157
+ * "total": 5,
158
+ * "passed": 4,
159
+ * "failed": 1,
160
+ * "pass_rate": 80.00,
161
+ * "total_duration_ms": 1234.56,
162
+ * "avg_duration_ms": 246.91
163
+ * },
164
+ * "tests": [...]
165
+ * }
166
+ * ```
167
+ */
168
+ export declare function formatResultsJson(suite: TestSuite, pretty?: boolean): string;
169
+ /**
170
+ * Create a simple assertion helper for tests
171
+ *
172
+ * @param condition - Condition to check
173
+ * @param message - Error message if assertion fails
174
+ * @throws Error if condition is false
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * await runTest('should work', async () => {
179
+ * const result = await someOperation();
180
+ * assert(result !== null, 'Result should not be null');
181
+ * assert(result.value === 'expected', 'Value should be expected');
182
+ * });
183
+ * ```
184
+ */
185
+ export declare function assert(condition: boolean, message?: string): asserts condition;
186
+ /**
187
+ * Deep equality assertion
188
+ *
189
+ * @param actual - Actual value
190
+ * @param expected - Expected value
191
+ * @param message - Error message if assertion fails
192
+ * @throws Error if values are not equal
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * await runTest('should match object', async () => {
197
+ * const result = await getUser();
198
+ * assertEqual(result, { id: '123', name: 'Test' });
199
+ * });
200
+ * ```
201
+ */
202
+ export declare function assertEqual<T>(actual: T, expected: T, message?: string): void;
203
+ /**
204
+ * Async error assertion helper
205
+ *
206
+ * @param fn - Function that should throw
207
+ * @param expectedMessage - Optional expected error message
208
+ * @throws Error if function doesn't throw
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * await runTest('should reject invalid input', async () => {
213
+ * await assertThrows(
214
+ * async () => await validateInput('invalid'),
215
+ * 'Invalid input format'
216
+ * );
217
+ * });
218
+ * ```
219
+ */
220
+ export declare function assertThrows(fn: () => Promise<void> | void, expectedMessage?: string): Promise<void>;
@@ -0,0 +1,302 @@
1
+ // =============================================================================
2
+ // Agent-Based Integration Test Runner
3
+ // =============================================================================
4
+ //
5
+ // Core test harness for running integration tests against the ClaudeTools
6
+ // memory system. Provides result collection, timing, and reporting.
7
+ //
8
+ /**
9
+ * Run a single test with timing and error handling
10
+ *
11
+ * @param name - Test name/description
12
+ * @param fn - Test function to execute
13
+ * @returns Test result with pass/fail, duration, and error details
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const result = await runTest('should store and retrieve fact', async () => {
18
+ * const fact = await storeFact('test', 'RELATES_TO', 'value');
19
+ * assert(fact !== null);
20
+ * });
21
+ *
22
+ * if (!result.passed) {
23
+ * console.error(`Test failed: ${result.error?.message}`);
24
+ * }
25
+ * ```
26
+ */
27
+ export async function runTest(name, fn) {
28
+ const startTime = performance.now();
29
+ try {
30
+ await fn();
31
+ const duration_ms = performance.now() - startTime;
32
+ return {
33
+ name,
34
+ passed: true,
35
+ duration_ms: Math.round(duration_ms * 100) / 100, // Round to 2 decimal places
36
+ };
37
+ }
38
+ catch (error) {
39
+ const duration_ms = performance.now() - startTime;
40
+ const err = error;
41
+ return {
42
+ name,
43
+ passed: false,
44
+ duration_ms: Math.round(duration_ms * 100) / 100,
45
+ error: {
46
+ message: err.message || String(error),
47
+ stack: err.stack,
48
+ },
49
+ };
50
+ }
51
+ }
52
+ /**
53
+ * Calculate summary statistics from test results
54
+ *
55
+ * @param tests - Array of test results
56
+ * @returns Aggregated summary with counts, duration, and pass rate
57
+ */
58
+ function calculateSummary(tests) {
59
+ const total = tests.length;
60
+ const passed = tests.filter(t => t.passed).length;
61
+ const failed = total - passed;
62
+ const total_duration_ms = tests.reduce((sum, t) => sum + t.duration_ms, 0);
63
+ const avg_duration_ms = total > 0
64
+ ? Math.round((total_duration_ms / total) * 100) / 100
65
+ : 0;
66
+ const pass_rate = total > 0
67
+ ? Math.round((passed / total) * 10000) / 100 // Round to 2 decimal places
68
+ : 0;
69
+ return {
70
+ total,
71
+ passed,
72
+ failed,
73
+ total_duration_ms: Math.round(total_duration_ms * 100) / 100,
74
+ avg_duration_ms,
75
+ pass_rate,
76
+ };
77
+ }
78
+ /**
79
+ * Run a suite of tests
80
+ *
81
+ * @param name - Suite name
82
+ * @param tests - Array of test definitions
83
+ * @returns Complete test suite results with summary
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const suite = await runSuite('Memory Integration Tests', [
88
+ * { name: 'should store fact', fn: async () => { ... } },
89
+ * { name: 'should search facts', fn: async () => { ... } },
90
+ * { name: 'should inject context', fn: async () => { ... } },
91
+ * ]);
92
+ *
93
+ * console.log(formatResults(suite));
94
+ *
95
+ * if (suite.summary.failed > 0) {
96
+ * process.exit(1);
97
+ * }
98
+ * ```
99
+ */
100
+ export async function runSuite(name, tests) {
101
+ const started_at = new Date().toISOString();
102
+ // Run all tests sequentially
103
+ const results = [];
104
+ for (const test of tests) {
105
+ const result = await runTest(test.name, test.fn);
106
+ results.push(result);
107
+ }
108
+ const completed_at = new Date().toISOString();
109
+ const summary = calculateSummary(results);
110
+ return {
111
+ name,
112
+ tests: results,
113
+ summary,
114
+ started_at,
115
+ completed_at,
116
+ };
117
+ }
118
+ /**
119
+ * Format test suite results as human-readable text
120
+ *
121
+ * @param suite - Test suite results
122
+ * @returns Formatted string with test results and summary
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * const suite = await runSuite('My Tests', [...]);
127
+ * console.log(formatResults(suite));
128
+ * ```
129
+ *
130
+ * Output:
131
+ * ```
132
+ * ================================================================================
133
+ * Test Suite: My Tests
134
+ * ================================================================================
135
+ *
136
+ * ✓ should pass test 1 (45.32ms)
137
+ * ✗ should fail test 2 (12.18ms)
138
+ * Error: Expected value to be true
139
+ *
140
+ * --------------------------------------------------------------------------------
141
+ * Summary
142
+ * --------------------------------------------------------------------------------
143
+ * Total: 2
144
+ * Passed: 1
145
+ * Failed: 1
146
+ * Pass Rate: 50.00%
147
+ * Duration: 57.50ms (avg: 28.75ms)
148
+ * ```
149
+ */
150
+ export function formatResults(suite) {
151
+ const lines = [];
152
+ // Header
153
+ lines.push('='.repeat(80));
154
+ lines.push(`Test Suite: ${suite.name}`);
155
+ lines.push('='.repeat(80));
156
+ lines.push('');
157
+ // Individual test results
158
+ for (const test of suite.tests) {
159
+ const icon = test.passed ? '✓' : '✗';
160
+ const status = test.passed ? 'PASS' : 'FAIL';
161
+ lines.push(`${icon} ${test.name} (${test.duration_ms}ms)`);
162
+ if (!test.passed && test.error) {
163
+ lines.push(` Error: ${test.error.message}`);
164
+ if (test.error.stack) {
165
+ const stackLines = test.error.stack.split('\n').slice(1, 4); // First 3 stack frames
166
+ stackLines.forEach(line => lines.push(` ${line.trim()}`));
167
+ }
168
+ }
169
+ }
170
+ // Summary
171
+ lines.push('');
172
+ lines.push('-'.repeat(80));
173
+ lines.push('Summary');
174
+ lines.push('-'.repeat(80));
175
+ lines.push(`Total: ${suite.summary.total}`);
176
+ lines.push(`Passed: ${suite.summary.passed}`);
177
+ lines.push(`Failed: ${suite.summary.failed}`);
178
+ lines.push(`Pass Rate: ${suite.summary.pass_rate}%`);
179
+ lines.push(`Duration: ${suite.summary.total_duration_ms}ms (avg: ${suite.summary.avg_duration_ms}ms)`);
180
+ lines.push('');
181
+ // Overall result
182
+ const overallStatus = suite.summary.failed === 0 ? 'PASSED ✓' : 'FAILED ✗';
183
+ lines.push(`Overall: ${overallStatus}`);
184
+ lines.push('='.repeat(80));
185
+ return lines.join('\n');
186
+ }
187
+ /**
188
+ * Format test suite results as JSON
189
+ *
190
+ * @param suite - Test suite results
191
+ * @param pretty - Whether to pretty-print the JSON (default: true)
192
+ * @returns JSON string
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * const suite = await runSuite('My Tests', [...]);
197
+ * const json = formatResultsJson(suite);
198
+ * await fs.writeFile('test-results.json', json);
199
+ * ```
200
+ *
201
+ * Output:
202
+ * ```json
203
+ * {
204
+ * "name": "My Tests",
205
+ * "started_at": "2025-01-01T12:00:00.000Z",
206
+ * "completed_at": "2025-01-01T12:00:01.234Z",
207
+ * "summary": {
208
+ * "total": 5,
209
+ * "passed": 4,
210
+ * "failed": 1,
211
+ * "pass_rate": 80.00,
212
+ * "total_duration_ms": 1234.56,
213
+ * "avg_duration_ms": 246.91
214
+ * },
215
+ * "tests": [...]
216
+ * }
217
+ * ```
218
+ */
219
+ export function formatResultsJson(suite, pretty = true) {
220
+ return pretty
221
+ ? JSON.stringify(suite, null, 2)
222
+ : JSON.stringify(suite);
223
+ }
224
+ /**
225
+ * Create a simple assertion helper for tests
226
+ *
227
+ * @param condition - Condition to check
228
+ * @param message - Error message if assertion fails
229
+ * @throws Error if condition is false
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * await runTest('should work', async () => {
234
+ * const result = await someOperation();
235
+ * assert(result !== null, 'Result should not be null');
236
+ * assert(result.value === 'expected', 'Value should be expected');
237
+ * });
238
+ * ```
239
+ */
240
+ export function assert(condition, message) {
241
+ if (!condition) {
242
+ throw new Error(message || 'Assertion failed');
243
+ }
244
+ }
245
+ /**
246
+ * Deep equality assertion
247
+ *
248
+ * @param actual - Actual value
249
+ * @param expected - Expected value
250
+ * @param message - Error message if assertion fails
251
+ * @throws Error if values are not equal
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * await runTest('should match object', async () => {
256
+ * const result = await getUser();
257
+ * assertEqual(result, { id: '123', name: 'Test' });
258
+ * });
259
+ * ```
260
+ */
261
+ export function assertEqual(actual, expected, message) {
262
+ const actualJson = JSON.stringify(actual);
263
+ const expectedJson = JSON.stringify(expected);
264
+ if (actualJson !== expectedJson) {
265
+ const msg = message || `Expected ${expectedJson} but got ${actualJson}`;
266
+ throw new Error(msg);
267
+ }
268
+ }
269
+ /**
270
+ * Async error assertion helper
271
+ *
272
+ * @param fn - Function that should throw
273
+ * @param expectedMessage - Optional expected error message
274
+ * @throws Error if function doesn't throw
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * await runTest('should reject invalid input', async () => {
279
+ * await assertThrows(
280
+ * async () => await validateInput('invalid'),
281
+ * 'Invalid input format'
282
+ * );
283
+ * });
284
+ * ```
285
+ */
286
+ export async function assertThrows(fn, expectedMessage) {
287
+ try {
288
+ await fn();
289
+ throw new Error('Expected function to throw an error');
290
+ }
291
+ catch (error) {
292
+ if (error instanceof Error && error.message === 'Expected function to throw an error') {
293
+ throw error;
294
+ }
295
+ if (expectedMessage) {
296
+ const actualMessage = error instanceof Error ? error.message : String(error);
297
+ if (!actualMessage.includes(expectedMessage)) {
298
+ throw new Error(`Expected error message to include "${expectedMessage}" but got "${actualMessage}"`);
299
+ }
300
+ }
301
+ }
302
+ }
package/dist/tools.js CHANGED
@@ -465,6 +465,54 @@ High hit rate memories (frequently fetched after being indexed) are good candida
465
465
  required: ['goal', 'epic_title', 'tasks'],
466
466
  },
467
467
  },
468
+ {
469
+ name: 'task_plan_revise',
470
+ description: 'Modify an existing epic by adding new tasks, removing tasks, or updating task details. Enables iterative planning without recreating the entire plan.',
471
+ inputSchema: {
472
+ type: 'object',
473
+ properties: {
474
+ epic_id: {
475
+ type: 'string',
476
+ description: 'The ID of the epic to revise',
477
+ },
478
+ add_tasks: {
479
+ type: 'array',
480
+ items: {
481
+ type: 'object',
482
+ properties: {
483
+ title: { type: 'string' },
484
+ description: { type: 'string' },
485
+ effort: { type: 'string', enum: ['xs', 's', 'm', 'l', 'xl'] },
486
+ domain: { type: 'string' },
487
+ blocked_by: { type: 'array', items: { type: 'string' } },
488
+ },
489
+ required: ['title'],
490
+ },
491
+ description: 'New tasks to add to the epic',
492
+ },
493
+ remove_task_ids: {
494
+ type: 'array',
495
+ items: { type: 'string' },
496
+ description: 'Task IDs to cancel (sets status to cancelled)',
497
+ },
498
+ update_tasks: {
499
+ type: 'array',
500
+ items: {
501
+ type: 'object',
502
+ properties: {
503
+ task_id: { type: 'string' },
504
+ title: { type: 'string' },
505
+ description: { type: 'string' },
506
+ effort: { type: 'string', enum: ['xs', 's', 'm', 'l', 'xl'] },
507
+ },
508
+ required: ['task_id'],
509
+ },
510
+ description: 'Tasks to update (adds context about changes)',
511
+ },
512
+ },
513
+ required: ['epic_id'],
514
+ },
515
+ },
468
516
  {
469
517
  name: 'task_start',
470
518
  description: 'PROACTIVE: When starting work on a task, claim it and get all context. Use this before beginning any task work.',
@@ -608,7 +656,16 @@ High hit rate memories (frequently fetched after being indexed) are good candida
608
656
  },
609
657
  {
610
658
  name: 'task_claim',
611
- description: 'Claim a task for working on it. Creates a lock preventing other agents from working on it.',
659
+ description: `Claim a task for working on it. Creates a lock preventing other agents from working on it.
660
+
661
+ Lock duration is automatically scaled based on task effort:
662
+ - xs: 15 minutes
663
+ - s: 30 minutes (default if no effort specified)
664
+ - m: 60 minutes (1 hour)
665
+ - l: 120 minutes (2 hours)
666
+ - xl: 240 minutes (4 hours)
667
+
668
+ Explicit lock_duration_minutes parameter overrides the effort-based default.`,
612
669
  inputSchema: {
613
670
  type: 'object',
614
671
  properties: {
@@ -622,7 +679,7 @@ High hit rate memories (frequently fetched after being indexed) are good candida
622
679
  },
623
680
  lock_duration_minutes: {
624
681
  type: 'number',
625
- description: 'Lock duration in minutes (default: 30)',
682
+ description: 'Optional: Override effort-based lock duration (in minutes)',
626
683
  },
627
684
  },
628
685
  required: ['task_id', 'agent_id'],
@@ -739,6 +796,32 @@ High hit rate memories (frequently fetched after being indexed) are good candida
739
796
  required: ['task_id', 'agent_id'],
740
797
  },
741
798
  },
799
+ {
800
+ name: 'task_handoff',
801
+ description: 'Hand off a task to a different expert worker type during execution. Releases current lock, updates agent_type, and sets task back to ready status for re-dispatch.',
802
+ inputSchema: {
803
+ type: 'object',
804
+ properties: {
805
+ task_id: {
806
+ type: 'string',
807
+ description: 'The task ID to hand off',
808
+ },
809
+ new_worker_type: {
810
+ type: 'string',
811
+ description: 'Target worker type (e.g., "api-expert", "frontend-expert", "database-expert")',
812
+ },
813
+ reason: {
814
+ type: 'string',
815
+ description: 'Why the handoff is needed (e.g., "requires database expertise", "needs API integration")',
816
+ },
817
+ agent_id: {
818
+ type: 'string',
819
+ description: 'Your agent identifier (default: claude-code)',
820
+ },
821
+ },
822
+ required: ['task_id', 'new_worker_type', 'reason'],
823
+ },
824
+ },
742
825
  // =========================================================================
743
826
  // ORCHESTRATION TOOLS
744
827
  // =========================================================================
@@ -921,6 +1004,24 @@ High hit rate memories (frequently fetched after being indexed) are good candida
921
1004
  required: ['epic_id'],
922
1005
  },
923
1006
  },
1007
+ {
1008
+ name: 'task_aggregate',
1009
+ description: 'Aggregate work_log context from all sibling tasks under an epic for orchestrator synthesis. Returns structured summary of completed work across all tasks in the epic.',
1010
+ inputSchema: {
1011
+ type: 'object',
1012
+ properties: {
1013
+ epic_id: {
1014
+ type: 'string',
1015
+ description: 'Epic ID to aggregate from',
1016
+ },
1017
+ include_pending: {
1018
+ type: 'boolean',
1019
+ description: 'Include non-completed tasks (default: false)',
1020
+ },
1021
+ },
1022
+ required: ['epic_id'],
1023
+ },
1024
+ },
924
1025
  // =========================================================================
925
1026
  // CODEBASE MAPPING TOOLS
926
1027
  // =========================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudetools/tools",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Persistent AI memory, task management, and codebase intelligence for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",