@claudetools/tools 0.1.2 → 0.2.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/dist/cli.js +12 -4
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +510 -162
- package/package.json +6 -2
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { parseArgs } from 'node:util';
|
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
|
-
import { runSetup } from './setup.js';
|
|
10
|
+
import { runSetup, runUninstall } from './setup.js';
|
|
11
11
|
import { startServer } from './index.js';
|
|
12
12
|
// Get version from package.json
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -19,6 +19,7 @@ const version = packageJson.version;
|
|
|
19
19
|
const { values } = parseArgs({
|
|
20
20
|
options: {
|
|
21
21
|
setup: { type: 'boolean', short: 's' },
|
|
22
|
+
uninstall: { type: 'boolean', short: 'u' },
|
|
22
23
|
version: { type: 'boolean', short: 'v' },
|
|
23
24
|
help: { type: 'boolean', short: 'h' },
|
|
24
25
|
},
|
|
@@ -38,9 +39,10 @@ Usage:
|
|
|
38
39
|
claudetools [options]
|
|
39
40
|
|
|
40
41
|
Options:
|
|
41
|
-
-s, --setup
|
|
42
|
-
-
|
|
43
|
-
-
|
|
42
|
+
-s, --setup Interactive setup wizard
|
|
43
|
+
-u, --uninstall Remove from Claude Code
|
|
44
|
+
-v, --version Show version
|
|
45
|
+
-h, --help Show this help
|
|
44
46
|
|
|
45
47
|
Running without options starts the server.
|
|
46
48
|
|
|
@@ -55,6 +57,12 @@ if (values.setup) {
|
|
|
55
57
|
process.exit(1);
|
|
56
58
|
});
|
|
57
59
|
}
|
|
60
|
+
else if (values.uninstall) {
|
|
61
|
+
runUninstall().catch((error) => {
|
|
62
|
+
console.error('Uninstall failed:', error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
58
66
|
else {
|
|
59
67
|
// Start MCP server
|
|
60
68
|
startServer();
|
package/dist/setup.d.ts
CHANGED
package/dist/setup.js
CHANGED
|
@@ -1,206 +1,554 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
// ClaudeTools
|
|
2
|
+
// ClaudeTools Interactive Setup Wizard
|
|
3
3
|
// =============================================================================
|
|
4
|
-
// Guides users through
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
// Guides users through authentication, service configuration, and Claude Code integration
|
|
5
|
+
import prompts from 'prompts';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
|
|
7
11
|
import { loadConfigFromFile, saveConfig, ensureConfigDir, getConfigPath, DEFAULT_CONFIG, } from './helpers/config-manager.js';
|
|
8
12
|
// -----------------------------------------------------------------------------
|
|
9
|
-
//
|
|
10
|
-
// -----------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
rl.close();
|
|
22
|
-
resolve(answer.trim() || defaultValue || '');
|
|
23
|
-
});
|
|
24
|
-
});
|
|
13
|
+
// Constants
|
|
14
|
+
// -----------------------------------------------------------------------------
|
|
15
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
16
|
+
const MCP_CONFIG_PATH = join(CLAUDE_DIR, 'mcp.json');
|
|
17
|
+
const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
|
|
18
|
+
// -----------------------------------------------------------------------------
|
|
19
|
+
// Utility Functions
|
|
20
|
+
// -----------------------------------------------------------------------------
|
|
21
|
+
function header(title) {
|
|
22
|
+
console.log('\n' + chalk.cyan('━'.repeat(50)));
|
|
23
|
+
console.log(chalk.cyan.bold(title));
|
|
24
|
+
console.log(chalk.cyan('━'.repeat(50)) + '\n');
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
return answer.toLowerCase().startsWith('y');
|
|
26
|
+
function success(msg) {
|
|
27
|
+
console.log(chalk.green('✓ ') + msg);
|
|
28
|
+
}
|
|
29
|
+
function error(msg) {
|
|
30
|
+
console.log(chalk.red('✗ ') + msg);
|
|
31
|
+
}
|
|
32
|
+
function info(msg) {
|
|
33
|
+
console.log(chalk.blue('ℹ ') + msg);
|
|
36
34
|
}
|
|
37
35
|
// -----------------------------------------------------------------------------
|
|
38
|
-
//
|
|
36
|
+
// Authentication
|
|
39
37
|
// -----------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
async function authenticateWithEmailPassword(apiUrl) {
|
|
39
|
+
const { email, password } = await prompts([
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
name: 'email',
|
|
43
|
+
message: 'Email:',
|
|
44
|
+
validate: (v) => v.includes('@') || 'Enter a valid email',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'password',
|
|
48
|
+
name: 'password',
|
|
49
|
+
message: 'Password:',
|
|
50
|
+
validate: (v) => v.length >= 8 || 'Password must be at least 8 characters',
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
if (!email || !password)
|
|
54
|
+
return null;
|
|
55
|
+
const spinner = ora('Authenticating...').start();
|
|
44
56
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
const response = await fetch(`${apiUrl}/api/v1/auth/login`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ email, password }),
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const data = await response.json().catch(() => ({}));
|
|
64
|
+
spinner.fail('Authentication failed');
|
|
65
|
+
error(data.message || `HTTP ${response.status}`);
|
|
66
|
+
return null;
|
|
50
67
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
spinner.succeed('Authenticated');
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
spinner.fail('Connection failed');
|
|
74
|
+
error(err instanceof Error ? err.message : String(err));
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function authenticateWithDeviceCode(apiUrl) {
|
|
79
|
+
const spinner = ora('Requesting device code...').start();
|
|
80
|
+
try {
|
|
81
|
+
// Request device code
|
|
82
|
+
const codeResponse = await fetch(`${apiUrl}/api/v1/auth/device/code`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
85
|
});
|
|
55
|
-
|
|
86
|
+
if (!codeResponse.ok) {
|
|
87
|
+
spinner.fail('Failed to get device code');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const { device_code, user_code, verification_uri, expires_in, interval } = await codeResponse.json();
|
|
91
|
+
spinner.stop();
|
|
92
|
+
console.log('\n' + chalk.yellow.bold(' ' + user_code) + '\n');
|
|
93
|
+
console.log(` Open: ${chalk.underline(verification_uri)}`);
|
|
94
|
+
console.log(` Enter the code above to authenticate.\n`);
|
|
95
|
+
const pollSpinner = ora('Waiting for authentication...').start();
|
|
96
|
+
// Poll for token
|
|
97
|
+
const pollInterval = (interval || 5) * 1000;
|
|
98
|
+
const expiresAt = Date.now() + (expires_in || 900) * 1000;
|
|
99
|
+
while (Date.now() < expiresAt) {
|
|
100
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
101
|
+
const tokenResponse = await fetch(`${apiUrl}/api/v1/auth/device/token`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({ device_code }),
|
|
105
|
+
});
|
|
106
|
+
if (tokenResponse.ok) {
|
|
107
|
+
const data = await tokenResponse.json();
|
|
108
|
+
pollSpinner.succeed('Authenticated');
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
const errorData = await tokenResponse.json().catch(() => ({ error: 'unknown' }));
|
|
112
|
+
if (errorData.error === 'authorization_pending') {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
else if (errorData.error === 'slow_down') {
|
|
116
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
pollSpinner.fail('Authentication failed');
|
|
121
|
+
error(errorData.error || 'Unknown error');
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
pollSpinner.fail('Authentication timed out');
|
|
126
|
+
return null;
|
|
56
127
|
}
|
|
57
|
-
catch (
|
|
58
|
-
|
|
128
|
+
catch (err) {
|
|
129
|
+
spinner.fail('Connection failed');
|
|
130
|
+
error(err instanceof Error ? err.message : String(err));
|
|
131
|
+
return null;
|
|
59
132
|
}
|
|
60
133
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
134
|
+
async function signUp(apiUrl) {
|
|
135
|
+
const { email, password, confirmPassword } = await prompts([
|
|
136
|
+
{
|
|
137
|
+
type: 'text',
|
|
138
|
+
name: 'email',
|
|
139
|
+
message: 'Email:',
|
|
140
|
+
validate: (v) => v.includes('@') || 'Enter a valid email',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'password',
|
|
144
|
+
name: 'password',
|
|
145
|
+
message: 'Password:',
|
|
146
|
+
validate: (v) => v.length >= 8 || 'Password must be at least 8 characters',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: 'password',
|
|
150
|
+
name: 'confirmPassword',
|
|
151
|
+
message: 'Confirm password:',
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
if (!email || !password)
|
|
155
|
+
return null;
|
|
156
|
+
if (password !== confirmPassword) {
|
|
157
|
+
error('Passwords do not match');
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const spinner = ora('Creating account...').start();
|
|
65
161
|
try {
|
|
66
|
-
const
|
|
67
|
-
const platform = process.platform;
|
|
68
|
-
const cwd = process.cwd();
|
|
69
|
-
const response = await fetch(`${config.apiUrl}/api/v1/systems/register`, {
|
|
162
|
+
const response = await fetch(`${apiUrl}/api/v1/auth/signup`, {
|
|
70
163
|
method: 'POST',
|
|
71
|
-
headers: {
|
|
72
|
-
|
|
73
|
-
'Authorization': config.apiKey ? `Bearer ${config.apiKey}` : '',
|
|
74
|
-
},
|
|
75
|
-
body: JSON.stringify({
|
|
76
|
-
name: hostname,
|
|
77
|
-
platform,
|
|
78
|
-
working_directory: cwd,
|
|
79
|
-
}),
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify({ email, password }),
|
|
80
166
|
});
|
|
81
167
|
if (!response.ok) {
|
|
82
|
-
|
|
168
|
+
const data = await response.json().catch(() => ({}));
|
|
169
|
+
spinner.fail('Sign up failed');
|
|
170
|
+
error(data.message || `HTTP ${response.status}`);
|
|
171
|
+
return null;
|
|
83
172
|
}
|
|
84
173
|
const data = await response.json();
|
|
85
|
-
|
|
174
|
+
spinner.succeed('Account created');
|
|
175
|
+
return data;
|
|
86
176
|
}
|
|
87
|
-
catch (
|
|
88
|
-
|
|
177
|
+
catch (err) {
|
|
178
|
+
spinner.fail('Connection failed');
|
|
179
|
+
error(err instanceof Error ? err.message : String(err));
|
|
180
|
+
return null;
|
|
89
181
|
}
|
|
90
182
|
}
|
|
183
|
+
async function runAuthFlow(apiUrl) {
|
|
184
|
+
const { authMethod } = await prompts({
|
|
185
|
+
type: 'select',
|
|
186
|
+
name: 'authMethod',
|
|
187
|
+
message: 'How would you like to authenticate?',
|
|
188
|
+
choices: [
|
|
189
|
+
{ title: 'Sign up (new account)', value: 'signup' },
|
|
190
|
+
{ title: 'Login with email/password', value: 'email' },
|
|
191
|
+
{ title: 'Login with device code (browser)', value: 'device' },
|
|
192
|
+
{ title: 'Skip (use existing API key)', value: 'skip' },
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
switch (authMethod) {
|
|
196
|
+
case 'signup':
|
|
197
|
+
return signUp(apiUrl);
|
|
198
|
+
case 'email':
|
|
199
|
+
return authenticateWithEmailPassword(apiUrl);
|
|
200
|
+
case 'device':
|
|
201
|
+
return authenticateWithDeviceCode(apiUrl);
|
|
202
|
+
case 'skip':
|
|
203
|
+
return null;
|
|
204
|
+
default:
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// -----------------------------------------------------------------------------
|
|
209
|
+
// Service Configuration
|
|
210
|
+
// -----------------------------------------------------------------------------
|
|
211
|
+
async function configureServices() {
|
|
212
|
+
header('Service Configuration');
|
|
213
|
+
info('Configure optional integrations for enhanced functionality.\n');
|
|
214
|
+
// Context7
|
|
215
|
+
console.log(chalk.bold('Context7') + ' - Library documentation fetching');
|
|
216
|
+
console.log(chalk.dim('Provides up-to-date docs for npm packages, frameworks, etc.\n'));
|
|
217
|
+
const { context7ApiKey } = await prompts({
|
|
218
|
+
type: 'text',
|
|
219
|
+
name: 'context7ApiKey',
|
|
220
|
+
message: 'Context7 API key (leave blank to skip):',
|
|
221
|
+
});
|
|
222
|
+
// Sequential Thinking
|
|
223
|
+
console.log('\n' + chalk.bold('Sequential Thinking') + ' - Planning and reasoning MCP');
|
|
224
|
+
console.log(chalk.dim('Enables structured problem-solving and task planning.\n'));
|
|
225
|
+
const { sequentialThinking } = await prompts({
|
|
226
|
+
type: 'confirm',
|
|
227
|
+
name: 'sequentialThinking',
|
|
228
|
+
message: 'Enable Sequential Thinking?',
|
|
229
|
+
initial: true,
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
context7ApiKey: context7ApiKey || undefined,
|
|
233
|
+
sequentialThinkingEnabled: sequentialThinking,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
91
236
|
// -----------------------------------------------------------------------------
|
|
92
|
-
//
|
|
237
|
+
// Claude Code Integration
|
|
238
|
+
// -----------------------------------------------------------------------------
|
|
239
|
+
function ensureClaudeDir() {
|
|
240
|
+
if (!existsSync(CLAUDE_DIR)) {
|
|
241
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function backupFile(path) {
|
|
245
|
+
if (existsSync(path)) {
|
|
246
|
+
const backupPath = `${path}.backup.${Date.now()}`;
|
|
247
|
+
copyFileSync(path, backupPath);
|
|
248
|
+
return backupPath;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
async function configureMcpSettings(services) {
|
|
253
|
+
header('Claude Code MCP Configuration');
|
|
254
|
+
ensureClaudeDir();
|
|
255
|
+
// Read existing config
|
|
256
|
+
let mcpConfig = { mcpServers: {} };
|
|
257
|
+
if (existsSync(MCP_CONFIG_PATH)) {
|
|
258
|
+
try {
|
|
259
|
+
mcpConfig = JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf-8'));
|
|
260
|
+
const backup = backupFile(MCP_CONFIG_PATH);
|
|
261
|
+
if (backup) {
|
|
262
|
+
info(`Backed up existing config to ${backup}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
error('Could not parse existing mcp.json, creating new one');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Ensure mcpServers object exists
|
|
270
|
+
if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== 'object') {
|
|
271
|
+
mcpConfig.mcpServers = {};
|
|
272
|
+
}
|
|
273
|
+
const servers = mcpConfig.mcpServers;
|
|
274
|
+
// Add claudetools
|
|
275
|
+
servers['claudetools_memory'] = {
|
|
276
|
+
command: 'claudetools',
|
|
277
|
+
};
|
|
278
|
+
success('Added claudetools_memory server');
|
|
279
|
+
// Add context7 if configured
|
|
280
|
+
if (services.context7ApiKey) {
|
|
281
|
+
servers['context7'] = {
|
|
282
|
+
command: 'npx',
|
|
283
|
+
args: ['-y', '@context7/mcp-server'],
|
|
284
|
+
env: {
|
|
285
|
+
CONTEXT7_API_KEY: services.context7ApiKey,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
success('Added context7 server');
|
|
289
|
+
}
|
|
290
|
+
// Add sequential-thinking if enabled
|
|
291
|
+
if (services.sequentialThinkingEnabled) {
|
|
292
|
+
servers['sequential-thinking'] = {
|
|
293
|
+
command: 'npx',
|
|
294
|
+
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
|
295
|
+
};
|
|
296
|
+
success('Added sequential-thinking server');
|
|
297
|
+
}
|
|
298
|
+
// Write config
|
|
299
|
+
writeFileSync(MCP_CONFIG_PATH, JSON.stringify(mcpConfig, null, 2));
|
|
300
|
+
success(`Saved MCP config to ${MCP_CONFIG_PATH}`);
|
|
301
|
+
}
|
|
302
|
+
async function installHooks() {
|
|
303
|
+
header('Claude Code Hooks Installation');
|
|
304
|
+
ensureClaudeDir();
|
|
305
|
+
if (!existsSync(HOOKS_DIR)) {
|
|
306
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
// User prompt submit hook - injects context before each message
|
|
309
|
+
const userPromptHook = `#!/bin/bash
|
|
310
|
+
# ClaudeTools Context Injection Hook
|
|
311
|
+
# Automatically injects relevant memory context before each prompt
|
|
312
|
+
|
|
313
|
+
# Prevent recursion
|
|
314
|
+
LOCK_FILE="/tmp/claude-prompt-hook.lock"
|
|
315
|
+
if [ -f "$LOCK_FILE" ]; then exit 0; fi
|
|
316
|
+
touch "$LOCK_FILE"
|
|
317
|
+
trap "rm -f $LOCK_FILE" EXIT
|
|
318
|
+
|
|
319
|
+
# Skip if disabled
|
|
320
|
+
if [ "$CLAUDE_DISABLE_HOOKS" = "1" ]; then exit 0; fi
|
|
321
|
+
|
|
322
|
+
# Read config
|
|
323
|
+
CONFIG_FILE="$HOME/.claudetools/config.json"
|
|
324
|
+
if [ ! -f "$CONFIG_FILE" ]; then exit 0; fi
|
|
325
|
+
|
|
326
|
+
API_URL=$(jq -r '.apiUrl // "https://api.claudetools.dev"' "$CONFIG_FILE")
|
|
327
|
+
API_KEY=$(jq -r '.apiKey // empty' "$CONFIG_FILE")
|
|
328
|
+
|
|
329
|
+
if [ -z "$API_KEY" ]; then exit 0; fi
|
|
330
|
+
|
|
331
|
+
# Get current project
|
|
332
|
+
PROJECT_FILE="$HOME/.claudetools/projects.json"
|
|
333
|
+
CWD=$(pwd)
|
|
334
|
+
PROJECT_ID=""
|
|
335
|
+
|
|
336
|
+
if [ -f "$PROJECT_FILE" ]; then
|
|
337
|
+
PROJECT_ID=$(jq -r --arg cwd "$CWD" '.[$cwd] // empty' "$PROJECT_FILE")
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
# Inject context (silent fail)
|
|
341
|
+
curl -s -X POST "$API_URL/api/v1/context/inject" \\
|
|
342
|
+
-H "Authorization: Bearer $API_KEY" \\
|
|
343
|
+
-H "Content-Type: application/json" \\
|
|
344
|
+
-d "{\\"project_id\\": \\"$PROJECT_ID\\", \\"cwd\\": \\"$CWD\\"}" \\
|
|
345
|
+
2>/dev/null || true
|
|
346
|
+
`;
|
|
347
|
+
const userPromptPath = join(HOOKS_DIR, 'user-prompt-submit.sh');
|
|
348
|
+
if (existsSync(userPromptPath)) {
|
|
349
|
+
const backup = backupFile(userPromptPath);
|
|
350
|
+
if (backup)
|
|
351
|
+
info(`Backed up existing hook to ${backup}`);
|
|
352
|
+
}
|
|
353
|
+
writeFileSync(userPromptPath, userPromptHook, { mode: 0o755 });
|
|
354
|
+
success('Installed user-prompt-submit.sh hook');
|
|
355
|
+
// Post tool use hook - logs tool usage for learning
|
|
356
|
+
const postToolHook = `#!/bin/bash
|
|
357
|
+
# ClaudeTools Tool Usage Logger
|
|
358
|
+
# Logs tool executions for pattern learning
|
|
359
|
+
|
|
360
|
+
# Prevent recursion
|
|
361
|
+
LOCK_FILE="/tmp/claude-tool-hook.lock"
|
|
362
|
+
if [ -f "$LOCK_FILE" ]; then exit 0; fi
|
|
363
|
+
touch "$LOCK_FILE"
|
|
364
|
+
trap "rm -f $LOCK_FILE" EXIT
|
|
365
|
+
|
|
366
|
+
# Skip if disabled
|
|
367
|
+
if [ "$CLAUDE_DISABLE_HOOKS" = "1" ]; then exit 0; fi
|
|
368
|
+
|
|
369
|
+
# Read input from stdin
|
|
370
|
+
INPUT=$(cat)
|
|
371
|
+
|
|
372
|
+
# Read config
|
|
373
|
+
CONFIG_FILE="$HOME/.claudetools/config.json"
|
|
374
|
+
if [ ! -f "$CONFIG_FILE" ]; then exit 0; fi
|
|
375
|
+
|
|
376
|
+
API_URL=$(jq -r '.apiUrl // "https://api.claudetools.dev"' "$CONFIG_FILE")
|
|
377
|
+
API_KEY=$(jq -r '.apiKey // empty' "$CONFIG_FILE")
|
|
378
|
+
|
|
379
|
+
if [ -z "$API_KEY" ]; then exit 0; fi
|
|
380
|
+
|
|
381
|
+
# Log tool usage (silent fail)
|
|
382
|
+
curl -s -X POST "$API_URL/api/v1/tools/log" \\
|
|
383
|
+
-H "Authorization: Bearer $API_KEY" \\
|
|
384
|
+
-H "Content-Type: application/json" \\
|
|
385
|
+
-d "$INPUT" \\
|
|
386
|
+
2>/dev/null || true
|
|
387
|
+
`;
|
|
388
|
+
const postToolPath = join(HOOKS_DIR, 'post-tool-use.sh');
|
|
389
|
+
if (existsSync(postToolPath)) {
|
|
390
|
+
const backup = backupFile(postToolPath);
|
|
391
|
+
if (backup)
|
|
392
|
+
info(`Backed up existing hook to ${backup}`);
|
|
393
|
+
}
|
|
394
|
+
writeFileSync(postToolPath, postToolHook, { mode: 0o755 });
|
|
395
|
+
success('Installed post-tool-use.sh hook');
|
|
396
|
+
}
|
|
397
|
+
// -----------------------------------------------------------------------------
|
|
398
|
+
// Verification
|
|
399
|
+
// -----------------------------------------------------------------------------
|
|
400
|
+
async function verifySetup(config) {
|
|
401
|
+
header('Verification');
|
|
402
|
+
const spinner = ora('Checking API connection...').start();
|
|
403
|
+
try {
|
|
404
|
+
const response = await fetch(`${config.apiUrl}/health`, {
|
|
405
|
+
headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
|
|
406
|
+
signal: AbortSignal.timeout(10000),
|
|
407
|
+
});
|
|
408
|
+
if (response.ok) {
|
|
409
|
+
spinner.succeed('API connection verified');
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
spinner.warn('API returned non-OK status');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
spinner.fail('Could not connect to API');
|
|
417
|
+
}
|
|
418
|
+
// Check MCP config exists
|
|
419
|
+
if (existsSync(MCP_CONFIG_PATH)) {
|
|
420
|
+
success('MCP config installed');
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
error('MCP config not found');
|
|
424
|
+
}
|
|
425
|
+
// Check hooks installed
|
|
426
|
+
if (existsSync(join(HOOKS_DIR, 'user-prompt-submit.sh'))) {
|
|
427
|
+
success('Hooks installed');
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
error('Hooks not found');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// -----------------------------------------------------------------------------
|
|
434
|
+
// Main Setup Flow
|
|
93
435
|
// -----------------------------------------------------------------------------
|
|
94
436
|
export async function runSetup() {
|
|
95
|
-
console.log('\n
|
|
96
|
-
console.log('
|
|
437
|
+
console.log('\n' + chalk.bold.cyan(' ClaudeTools Setup Wizard') + '\n');
|
|
438
|
+
console.log(' ' + chalk.dim('Persistent AI memory for Claude Code') + '\n');
|
|
97
439
|
try {
|
|
98
|
-
//
|
|
440
|
+
// Ensure config directory exists
|
|
99
441
|
await ensureConfigDir();
|
|
100
|
-
//
|
|
442
|
+
// Load existing config
|
|
101
443
|
const loadedConfig = await loadConfigFromFile();
|
|
102
|
-
// Merge with defaults to ensure all fields exist
|
|
103
444
|
let config = { ...DEFAULT_CONFIG, ...loadedConfig };
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
console.log('API Configuration');
|
|
107
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
108
|
-
const useDefaultApi = await confirm(`Use default API URL (${DEFAULT_CONFIG.apiUrl})?`, true);
|
|
109
|
-
if (!useDefaultApi) {
|
|
110
|
-
const customApiUrl = await prompt('Enter custom API URL');
|
|
111
|
-
if (customApiUrl) {
|
|
112
|
-
config.apiUrl = customApiUrl;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// 4. API Key configuration
|
|
116
|
-
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
117
|
-
console.log('Authentication');
|
|
118
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
445
|
+
// Step 1: Authentication
|
|
446
|
+
header('Authentication');
|
|
119
447
|
if (config.apiKey) {
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
448
|
+
info(`Existing API key found: ${config.apiKey.substring(0, 10)}...`);
|
|
449
|
+
const { replace } = await prompts({
|
|
450
|
+
type: 'confirm',
|
|
451
|
+
name: 'replace',
|
|
452
|
+
message: 'Replace existing authentication?',
|
|
453
|
+
initial: false,
|
|
454
|
+
});
|
|
455
|
+
if (replace) {
|
|
456
|
+
const auth = await runAuthFlow(config.apiUrl || DEFAULT_CONFIG.apiUrl);
|
|
457
|
+
if (auth) {
|
|
458
|
+
config.apiKey = auth.token;
|
|
459
|
+
success(`Logged in as ${auth.email}`);
|
|
126
460
|
}
|
|
127
461
|
}
|
|
128
462
|
}
|
|
129
463
|
else {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
464
|
+
const auth = await runAuthFlow(config.apiUrl || DEFAULT_CONFIG.apiUrl);
|
|
465
|
+
if (auth) {
|
|
466
|
+
config.apiKey = auth.token;
|
|
467
|
+
success(`Logged in as ${auth.email}`);
|
|
134
468
|
}
|
|
469
|
+
else {
|
|
470
|
+
// Manual API key entry
|
|
471
|
+
const { apiKey } = await prompts({
|
|
472
|
+
type: 'text',
|
|
473
|
+
name: 'apiKey',
|
|
474
|
+
message: 'Enter API key manually (from claudetools.dev/dashboard):',
|
|
475
|
+
});
|
|
476
|
+
if (apiKey) {
|
|
477
|
+
config.apiKey = apiKey;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// Step 2: Service Configuration
|
|
482
|
+
const services = await configureServices();
|
|
483
|
+
// Store service configs in extended config
|
|
484
|
+
const extendedConfig = config;
|
|
485
|
+
if (services.context7ApiKey) {
|
|
486
|
+
extendedConfig.context7ApiKey = services.context7ApiKey;
|
|
135
487
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
488
|
+
extendedConfig.sequentialThinkingEnabled = services.sequentialThinkingEnabled;
|
|
489
|
+
// Step 3: Save ClaudeTools config
|
|
490
|
+
header('Saving Configuration');
|
|
491
|
+
await saveConfig(extendedConfig);
|
|
492
|
+
success(`Configuration saved to ${getConfigPath()}`);
|
|
493
|
+
// Step 4: Configure Claude Code MCP
|
|
494
|
+
await configureMcpSettings(services);
|
|
495
|
+
// Step 5: Install Hooks
|
|
496
|
+
await installHooks();
|
|
497
|
+
// Step 6: Verify
|
|
498
|
+
await verifySetup(extendedConfig);
|
|
499
|
+
// Done
|
|
500
|
+
header('Setup Complete');
|
|
501
|
+
console.log(chalk.green(' ClaudeTools is now configured!\n'));
|
|
502
|
+
console.log(' ' + chalk.bold('Next step:') + ' Restart Claude Code\n');
|
|
503
|
+
console.log(' The memory system will activate automatically.\n');
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
console.error('\n' + chalk.red('Setup failed:'), err instanceof Error ? err.message : String(err));
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// -----------------------------------------------------------------------------
|
|
511
|
+
// Uninstall
|
|
512
|
+
// -----------------------------------------------------------------------------
|
|
513
|
+
export async function runUninstall() {
|
|
514
|
+
console.log('\n' + chalk.bold.red(' ClaudeTools Uninstall') + '\n');
|
|
515
|
+
const { confirm } = await prompts({
|
|
516
|
+
type: 'confirm',
|
|
517
|
+
name: 'confirm',
|
|
518
|
+
message: 'Remove ClaudeTools from Claude Code?',
|
|
519
|
+
initial: false,
|
|
520
|
+
});
|
|
521
|
+
if (!confirm) {
|
|
522
|
+
console.log('Cancelled.');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
// Remove from MCP config
|
|
526
|
+
if (existsSync(MCP_CONFIG_PATH)) {
|
|
527
|
+
try {
|
|
528
|
+
const mcpConfig = JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf-8'));
|
|
529
|
+
if (mcpConfig.mcpServers) {
|
|
530
|
+
delete mcpConfig.mcpServers['claudetools_memory'];
|
|
531
|
+
writeFileSync(MCP_CONFIG_PATH, JSON.stringify(mcpConfig, null, 2));
|
|
532
|
+
success('Removed from MCP config');
|
|
149
533
|
}
|
|
150
534
|
}
|
|
151
|
-
|
|
152
|
-
|
|
535
|
+
catch {
|
|
536
|
+
error('Could not update MCP config');
|
|
153
537
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
console.log('System Registration');
|
|
166
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
167
|
-
const shouldRegister = await confirm('Register this system?', true);
|
|
168
|
-
if (shouldRegister) {
|
|
169
|
-
try {
|
|
170
|
-
console.log('Registering system...');
|
|
171
|
-
const systemId = await registerSystem(config);
|
|
172
|
-
console.log(`✅ System registered: ${systemId}`);
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
console.error(`❌ Registration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
176
|
-
console.error(' You can continue without registration.');
|
|
177
|
-
}
|
|
538
|
+
}
|
|
539
|
+
// Remove hooks
|
|
540
|
+
const hooks = ['user-prompt-submit.sh', 'post-tool-use.sh'];
|
|
541
|
+
for (const hook of hooks) {
|
|
542
|
+
const hookPath = join(HOOKS_DIR, hook);
|
|
543
|
+
if (existsSync(hookPath)) {
|
|
544
|
+
const content = readFileSync(hookPath, 'utf-8');
|
|
545
|
+
if (content.includes('ClaudeTools')) {
|
|
546
|
+
const { unlinkSync } = await import('fs');
|
|
547
|
+
unlinkSync(hookPath);
|
|
548
|
+
success(`Removed ${hook}`);
|
|
178
549
|
}
|
|
179
550
|
}
|
|
180
|
-
// 8. Save configuration
|
|
181
|
-
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
182
|
-
console.log('Saving Configuration');
|
|
183
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
184
|
-
await saveConfig(config);
|
|
185
|
-
console.log(`✅ Configuration saved to ${getConfigPath()}`);
|
|
186
|
-
// 9. Show next steps
|
|
187
|
-
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
188
|
-
console.log('Next Steps');
|
|
189
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
190
|
-
console.log('1. Add to Claude Code MCP settings:');
|
|
191
|
-
console.log(' Location: ~/.claude/mcp.json\n');
|
|
192
|
-
console.log(' {');
|
|
193
|
-
console.log(' "mcpServers": {');
|
|
194
|
-
console.log(' "claudetools-memory": {');
|
|
195
|
-
console.log(' "command": "claudetools-memory"');
|
|
196
|
-
console.log(' }');
|
|
197
|
-
console.log(' }');
|
|
198
|
-
console.log(' }\n');
|
|
199
|
-
console.log('2. Restart Claude Code\n');
|
|
200
|
-
console.log('Done! 🎉\n');
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
console.error('\n❌ Setup failed:', error instanceof Error ? error.message : String(error));
|
|
204
|
-
process.exit(1);
|
|
205
551
|
}
|
|
552
|
+
console.log('\n' + chalk.green('ClaudeTools removed from Claude Code.'));
|
|
553
|
+
console.log(chalk.dim('Your ~/.claudetools/ config and data are preserved.\n'));
|
|
206
554
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudetools/tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Persistent AI memory, task management, and codebase intelligence for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -46,10 +46,14 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
50
|
+
"chalk": "^5.6.2",
|
|
51
|
+
"ora": "^9.0.0",
|
|
52
|
+
"prompts": "^2.4.2"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"@types/node": "^20.10.0",
|
|
56
|
+
"@types/prompts": "^2.4.9",
|
|
53
57
|
"@vitest/ui": "^4.0.15",
|
|
54
58
|
"tsx": "^4.7.0",
|
|
55
59
|
"typescript": "^5.3.0",
|