@pcircle/memesh 2.9.0 → 2.9.2
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/mcp/ToolDefinitions.d.ts.map +1 -1
- package/dist/mcp/ToolDefinitions.js +0 -104
- package/dist/mcp/ToolDefinitions.js.map +1 -1
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/scripts/hooks/README.md +230 -0
- package/scripts/hooks/__tests__/hook-test-harness.js +218 -0
- package/scripts/hooks/__tests__/hooks.test.js +267 -0
- package/scripts/hooks/hook-utils.js +899 -0
- package/scripts/hooks/post-commit.js +307 -0
- package/scripts/hooks/post-tool-use.js +812 -0
- package/scripts/hooks/pre-tool-use.js +462 -0
- package/scripts/hooks/session-start.js +544 -0
- package/scripts/hooks/stop.js +673 -0
- package/scripts/hooks/subagent-stop.js +184 -0
- package/scripts/postinstall-lib.js +8 -4
- package/scripts/postinstall-new.js +15 -7
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PreToolUse Hook - Modular Handler Architecture
|
|
5
|
+
*
|
|
6
|
+
* Triggered before each tool execution in Claude Code.
|
|
7
|
+
*
|
|
8
|
+
* Handlers (each returns partial response or null):
|
|
9
|
+
* 1. codeReviewHandler — git commit → review reminder
|
|
10
|
+
* 2. routingHandler — Task → model/background selection
|
|
11
|
+
* 3. planningHandler — Task(Plan)/EnterPlanMode → SDD+BDD template
|
|
12
|
+
* 4. dryRunGateHandler — Task → untested code warning
|
|
13
|
+
*
|
|
14
|
+
* Response Merger combines all handler outputs into a single JSON response:
|
|
15
|
+
* - updatedInput: deep-merged
|
|
16
|
+
* - additionalContext: concatenated
|
|
17
|
+
* - permissionDecision: most-restrictive-wins
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
HOME_DIR,
|
|
22
|
+
STATE_DIR,
|
|
23
|
+
readJSONFile,
|
|
24
|
+
readStdin,
|
|
25
|
+
logError,
|
|
26
|
+
} from './hook-utils.js';
|
|
27
|
+
import fs from 'fs';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Constants
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const CURRENT_SESSION_FILE = path.join(STATE_DIR, 'current-session.json');
|
|
36
|
+
const ROUTING_CONFIG_FILE = path.join(HOME_DIR, '.memesh', 'routing-config.json');
|
|
37
|
+
const ROUTING_AUDIT_LOG = path.join(HOME_DIR, '.memesh', 'routing-audit.log');
|
|
38
|
+
const PLANNING_TEMPLATE_FILE = path.join(
|
|
39
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
40
|
+
'templates',
|
|
41
|
+
'planning-template.md'
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Response Merger
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Deep-merge two objects (shallow for top-level, recursive for nested).
|
|
50
|
+
* Later values override earlier ones.
|
|
51
|
+
*/
|
|
52
|
+
function deepMerge(target, source) {
|
|
53
|
+
if (!source) return target;
|
|
54
|
+
if (!target) return source;
|
|
55
|
+
|
|
56
|
+
const result = { ...target };
|
|
57
|
+
for (const key of Object.keys(source)) {
|
|
58
|
+
if (
|
|
59
|
+
typeof result[key] === 'object' && result[key] !== null &&
|
|
60
|
+
typeof source[key] === 'object' && source[key] !== null &&
|
|
61
|
+
!Array.isArray(result[key])
|
|
62
|
+
) {
|
|
63
|
+
result[key] = deepMerge(result[key], source[key]);
|
|
64
|
+
} else {
|
|
65
|
+
result[key] = source[key];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the most restrictive permission decision.
|
|
73
|
+
* Priority: deny > ask > allow > undefined
|
|
74
|
+
*/
|
|
75
|
+
function mostRestrictive(decisions) {
|
|
76
|
+
const priority = { deny: 3, ask: 2, allow: 1 };
|
|
77
|
+
let result = undefined;
|
|
78
|
+
let maxPriority = 0;
|
|
79
|
+
|
|
80
|
+
for (const d of decisions) {
|
|
81
|
+
if (d && priority[d] > maxPriority) {
|
|
82
|
+
maxPriority = priority[d];
|
|
83
|
+
result = d;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Merge multiple handler responses into a single hook output.
|
|
91
|
+
* @param {Array<Object|null>} responses - Handler responses
|
|
92
|
+
* @returns {Object|null} Merged response or null if all handlers returned null
|
|
93
|
+
*/
|
|
94
|
+
function mergeResponses(responses) {
|
|
95
|
+
const valid = responses.filter(Boolean);
|
|
96
|
+
if (valid.length === 0) return null;
|
|
97
|
+
|
|
98
|
+
let mergedInput = undefined;
|
|
99
|
+
const contextParts = [];
|
|
100
|
+
const decisions = [];
|
|
101
|
+
|
|
102
|
+
for (const r of valid) {
|
|
103
|
+
if (r.updatedInput) {
|
|
104
|
+
mergedInput = deepMerge(mergedInput, r.updatedInput);
|
|
105
|
+
}
|
|
106
|
+
if (r.additionalContext) {
|
|
107
|
+
contextParts.push(r.additionalContext);
|
|
108
|
+
}
|
|
109
|
+
if (r.permissionDecision) {
|
|
110
|
+
decisions.push(r.permissionDecision);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const merged = {};
|
|
115
|
+
if (mergedInput) merged.updatedInput = mergedInput;
|
|
116
|
+
if (contextParts.length > 0) merged.additionalContext = contextParts.join('\n\n');
|
|
117
|
+
|
|
118
|
+
const decision = mostRestrictive(decisions);
|
|
119
|
+
if (decision) merged.permissionDecision = decision;
|
|
120
|
+
|
|
121
|
+
return Object.keys(merged).length > 0 ? merged : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Routing Config
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Load routing config with fallback defaults.
|
|
130
|
+
* Creates default config if file doesn't exist.
|
|
131
|
+
*/
|
|
132
|
+
function loadRoutingConfig() {
|
|
133
|
+
const defaults = {
|
|
134
|
+
version: 1,
|
|
135
|
+
modelRouting: {
|
|
136
|
+
rules: [
|
|
137
|
+
{ subagentType: 'Explore', model: 'haiku', reason: 'Fast codebase search' },
|
|
138
|
+
],
|
|
139
|
+
default: null,
|
|
140
|
+
},
|
|
141
|
+
backgroundRules: [
|
|
142
|
+
{ subagentType: 'Explore', forceBackground: false },
|
|
143
|
+
],
|
|
144
|
+
planningEnforcement: {
|
|
145
|
+
enabled: true,
|
|
146
|
+
triggerSubagents: ['Plan'],
|
|
147
|
+
triggerEnterPlanMode: true,
|
|
148
|
+
},
|
|
149
|
+
dryRunGate: {
|
|
150
|
+
enabled: true,
|
|
151
|
+
skipSubagents: ['Explore', 'Plan', 'claude-code-guide'],
|
|
152
|
+
},
|
|
153
|
+
auditLog: true,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
if (fs.existsSync(ROUTING_CONFIG_FILE)) {
|
|
158
|
+
const config = JSON.parse(fs.readFileSync(ROUTING_CONFIG_FILE, 'utf-8'));
|
|
159
|
+
return { ...defaults, ...config };
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logError('loadRoutingConfig', error);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Create default config on first run
|
|
166
|
+
try {
|
|
167
|
+
const dir = path.dirname(ROUTING_CONFIG_FILE);
|
|
168
|
+
if (!fs.existsSync(dir)) {
|
|
169
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
fs.writeFileSync(ROUTING_CONFIG_FILE, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
172
|
+
} catch {
|
|
173
|
+
// Non-critical — works with in-memory defaults
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return defaults;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Audit Log
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Append an entry to the routing audit log.
|
|
185
|
+
* @param {string} entry - Log entry
|
|
186
|
+
* @param {Object} config - Routing config
|
|
187
|
+
*/
|
|
188
|
+
function auditLog(entry, config) {
|
|
189
|
+
if (!config.auditLog) return;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const timestamp = new Date().toISOString();
|
|
193
|
+
const line = `[${timestamp}] ${entry}\n`;
|
|
194
|
+
fs.appendFileSync(ROUTING_AUDIT_LOG, line);
|
|
195
|
+
} catch {
|
|
196
|
+
// Non-critical
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Handler 1: Code Review (existing behavior)
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
function codeReviewHandler(toolName, toolInput, _session) {
|
|
205
|
+
// Only applies to git commit commands
|
|
206
|
+
if (toolName !== 'Bash') return null;
|
|
207
|
+
|
|
208
|
+
const cmd = toolInput?.command || '';
|
|
209
|
+
if (!/git\s+commit\s/.test(cmd) || cmd.includes('--amend')) return null;
|
|
210
|
+
|
|
211
|
+
// Check if code review was done this session
|
|
212
|
+
const session = readJSONFile(CURRENT_SESSION_FILE, {});
|
|
213
|
+
if (session.codeReviewDone === true) return null;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
additionalContext: [
|
|
217
|
+
'<user-prompt-submit-hook>',
|
|
218
|
+
'PRE-COMMIT REVIEW REMINDER:',
|
|
219
|
+
'No comprehensive code review was detected in this session.',
|
|
220
|
+
'Before committing significant changes, run: @comprehensive-code-review',
|
|
221
|
+
'',
|
|
222
|
+
'The review checks for:',
|
|
223
|
+
'- Ripple Map: unsynchronized cross-file changes',
|
|
224
|
+
'- Reality Check: phantom imports, ghost methods, schema drift',
|
|
225
|
+
'- Cross-boundary Sync: type parity, client parity, route-SDK match',
|
|
226
|
+
'- Security, concurrency, error handling, and 7 other dimensions',
|
|
227
|
+
'',
|
|
228
|
+
'Skip only for trivial changes (typo fixes, formatting, comments).',
|
|
229
|
+
'</user-prompt-submit-hook>',
|
|
230
|
+
].join('\n'),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Handler 2: Model Routing
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
function routingHandler(toolName, toolInput, _session, config) {
|
|
239
|
+
if (toolName !== 'Task') return null;
|
|
240
|
+
|
|
241
|
+
const subagentType = toolInput?.subagent_type || '';
|
|
242
|
+
if (!subagentType) return null;
|
|
243
|
+
|
|
244
|
+
const result = { updatedInput: {} };
|
|
245
|
+
let applied = false;
|
|
246
|
+
|
|
247
|
+
// Model routing
|
|
248
|
+
const modelRules = config.modelRouting?.rules || [];
|
|
249
|
+
for (const rule of modelRules) {
|
|
250
|
+
if (!rule.subagentType) continue;
|
|
251
|
+
if (subagentType.toLowerCase() === rule.subagentType.toLowerCase()) {
|
|
252
|
+
// Never override user's explicit model choice
|
|
253
|
+
if (toolInput.model) {
|
|
254
|
+
auditLog(`Task(${subagentType}) → user override preserved (model: ${toolInput.model})`, config);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
result.updatedInput.model = rule.model;
|
|
258
|
+
auditLog(`Task(${subagentType}) → model: ${rule.model} (${rule.reason})`, config);
|
|
259
|
+
applied = true;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Background routing
|
|
265
|
+
const bgRules = config.backgroundRules || [];
|
|
266
|
+
for (const rule of bgRules) {
|
|
267
|
+
if (!rule.subagentType) continue;
|
|
268
|
+
if (subagentType.toLowerCase() === rule.subagentType.toLowerCase()) {
|
|
269
|
+
// Only force background if not explicitly set by user/Claude
|
|
270
|
+
if (rule.forceBackground && toolInput.run_in_background === undefined) {
|
|
271
|
+
result.updatedInput.run_in_background = true;
|
|
272
|
+
auditLog(`Task(${subagentType}) → background: true`, config);
|
|
273
|
+
applied = true;
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!applied && !toolInput.model) {
|
|
280
|
+
auditLog(`Task(${subagentType}) → no override (no matching rule)`, config);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return Object.keys(result.updatedInput).length > 0 ? result : null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Handler 3: Planning Enforcer
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
function planningHandler(toolName, toolInput, _session, config) {
|
|
291
|
+
const planConfig = config.planningEnforcement;
|
|
292
|
+
if (!planConfig?.enabled) return null;
|
|
293
|
+
|
|
294
|
+
// Case 1: Task tool dispatching a Plan subagent
|
|
295
|
+
if (toolName === 'Task') {
|
|
296
|
+
const subagentType = toolInput?.subagent_type || '';
|
|
297
|
+
const triggerSubagents = planConfig.triggerSubagents || ['Plan'];
|
|
298
|
+
|
|
299
|
+
if (triggerSubagents.some(t => subagentType.toLowerCase() === t.toLowerCase())) {
|
|
300
|
+
const template = loadPlanningTemplate();
|
|
301
|
+
if (!template) return null;
|
|
302
|
+
|
|
303
|
+
auditLog(`Task(${subagentType}) → planning template injected`, config);
|
|
304
|
+
|
|
305
|
+
// Append template to the subagent's prompt via updatedInput.prompt
|
|
306
|
+
const originalPrompt = toolInput?.prompt || '';
|
|
307
|
+
return {
|
|
308
|
+
updatedInput: {
|
|
309
|
+
prompt: originalPrompt + '\n\n---\n\n' + template,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Case 2: EnterPlanMode — inject into main Claude's context
|
|
316
|
+
if (toolName === 'EnterPlanMode' && planConfig.triggerEnterPlanMode) {
|
|
317
|
+
auditLog('EnterPlanMode → planning template context injected', config);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
additionalContext: [
|
|
321
|
+
'PLANNING MODE ACTIVATED — Use this template for your plan:',
|
|
322
|
+
'',
|
|
323
|
+
loadPlanningTemplate() || '(Planning template not found)',
|
|
324
|
+
'',
|
|
325
|
+
'IMPORTANT: Present the completed plan to the user and wait for',
|
|
326
|
+
'explicit approval before proceeding to implementation.',
|
|
327
|
+
].join('\n'),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Load the planning template from file.
|
|
336
|
+
* @returns {string|null}
|
|
337
|
+
*/
|
|
338
|
+
function loadPlanningTemplate() {
|
|
339
|
+
try {
|
|
340
|
+
if (fs.existsSync(PLANNING_TEMPLATE_FILE)) {
|
|
341
|
+
return fs.readFileSync(PLANNING_TEMPLATE_FILE, 'utf-8');
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
logError('loadPlanningTemplate', error);
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// Handler 4: Dry-Run Gate
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
function dryRunGateHandler(toolName, toolInput, _session, config) {
|
|
354
|
+
const gateConfig = config.dryRunGate;
|
|
355
|
+
if (!gateConfig?.enabled) return null;
|
|
356
|
+
|
|
357
|
+
// Only applies to Task dispatches (heavy operations)
|
|
358
|
+
if (toolName !== 'Task') return null;
|
|
359
|
+
|
|
360
|
+
const subagentType = toolInput?.subagent_type || '';
|
|
361
|
+
const skipTypes = gateConfig.skipSubagents || ['Explore', 'Plan', 'claude-code-guide'];
|
|
362
|
+
|
|
363
|
+
// Skip for research/planning agents that don't need tested code
|
|
364
|
+
if (skipTypes.some(t => subagentType.toLowerCase().includes(t.toLowerCase()))) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Read session state for file tracking
|
|
369
|
+
const session = readJSONFile(CURRENT_SESSION_FILE, {});
|
|
370
|
+
const modifiedFiles = session.modifiedFiles || [];
|
|
371
|
+
const testedFiles = session.testedFiles || [];
|
|
372
|
+
|
|
373
|
+
if (modifiedFiles.length === 0) return null;
|
|
374
|
+
|
|
375
|
+
// Find untested files
|
|
376
|
+
const untestedFiles = modifiedFiles.filter(f => !testedFiles.includes(f));
|
|
377
|
+
|
|
378
|
+
if (untestedFiles.length === 0) return null;
|
|
379
|
+
|
|
380
|
+
// Build warning (advisory only — never deny)
|
|
381
|
+
const fileList = untestedFiles.length <= 5
|
|
382
|
+
? untestedFiles.map(f => path.basename(f)).join(', ')
|
|
383
|
+
: `${untestedFiles.slice(0, 5).map(f => path.basename(f)).join(', ')} (+${untestedFiles.length - 5} more)`;
|
|
384
|
+
|
|
385
|
+
auditLog(`Task(${subagentType}) → dry-run warning: ${untestedFiles.length} untested files`, config);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
additionalContext: [
|
|
389
|
+
'UNTESTED CODE WARNING:',
|
|
390
|
+
`${untestedFiles.length} modified file(s) have not been tested yet: ${fileList}`,
|
|
391
|
+
'',
|
|
392
|
+
'Consider running tests before dispatching this task:',
|
|
393
|
+
'- node --check <file> (syntax verification)',
|
|
394
|
+
'- vitest run <test> (unit tests)',
|
|
395
|
+
'- tsc --noEmit (type checking)',
|
|
396
|
+
'',
|
|
397
|
+
'This is advisory — proceed if you are confident the code is correct.',
|
|
398
|
+
].join('\n'),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ============================================================================
|
|
403
|
+
// Hook Response Output
|
|
404
|
+
// ============================================================================
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Output hook response as JSON to stdout.
|
|
408
|
+
*/
|
|
409
|
+
function respond(hookOutput) {
|
|
410
|
+
process.stdout.write(JSON.stringify({
|
|
411
|
+
hookSpecificOutput: {
|
|
412
|
+
hookEventName: 'PreToolUse',
|
|
413
|
+
...hookOutput,
|
|
414
|
+
},
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// Main
|
|
420
|
+
// ============================================================================
|
|
421
|
+
|
|
422
|
+
async function preToolUse() {
|
|
423
|
+
try {
|
|
424
|
+
const input = await readStdin(3000);
|
|
425
|
+
if (!input || input.trim() === '') {
|
|
426
|
+
process.exit(0);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const data = JSON.parse(input);
|
|
430
|
+
const toolName = data.tool_name || data.toolName || '';
|
|
431
|
+
const toolInput = data.tool_input || data.arguments || {};
|
|
432
|
+
|
|
433
|
+
// Load config once for all handlers
|
|
434
|
+
const config = loadRoutingConfig();
|
|
435
|
+
|
|
436
|
+
// Load session state once for handlers that need it
|
|
437
|
+
const session = readJSONFile(CURRENT_SESSION_FILE, {});
|
|
438
|
+
|
|
439
|
+
// Run all handlers
|
|
440
|
+
const responses = [
|
|
441
|
+
codeReviewHandler(toolName, toolInput, session),
|
|
442
|
+
routingHandler(toolName, toolInput, session, config),
|
|
443
|
+
planningHandler(toolName, toolInput, session, config),
|
|
444
|
+
dryRunGateHandler(toolName, toolInput, session, config),
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
// Merge all responses
|
|
448
|
+
const merged = mergeResponses(responses);
|
|
449
|
+
|
|
450
|
+
// If any handler produced output, send the merged response
|
|
451
|
+
if (merged) {
|
|
452
|
+
respond(merged);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
process.exit(0);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
logError('PreToolUse', error);
|
|
458
|
+
process.exit(0); // Never block on hook errors
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
preToolUse();
|