agentrace 0.0.10 → 0.0.11
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/README.md +51 -4
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +34 -7
- package/dist/commands/mcp-server.js +3 -1
- package/dist/commands/off.d.ts +4 -1
- package/dist/commands/off.js +21 -8
- package/dist/commands/on.d.ts +1 -0
- package/dist/commands/on.js +22 -6
- package/dist/commands/send.js +3 -3
- package/dist/commands/uninstall.d.ts +4 -1
- package/dist/commands/uninstall.js +40 -10
- package/dist/config/manager.d.ts +9 -0
- package/dist/config/manager.js +52 -0
- package/dist/config/manager.test.d.ts +1 -0
- package/dist/config/manager.test.js +86 -0
- package/dist/hooks/installer.d.ts +40 -7
- package/dist/hooks/installer.js +114 -57
- package/dist/hooks/installer.test.d.ts +1 -0
- package/dist/hooks/installer.test.js +166 -0
- package/dist/index.js +17 -6
- package/dist/mcp/plan-document-client.d.ts +1 -1
- package/dist/mcp/plan-document-client.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,11 +35,15 @@ That's it! When you use Claude Code, sessions will be automatically sent to Agen
|
|
|
35
35
|
| -------------------------------------------- | -------------------------------------- |
|
|
36
36
|
| `agentrace init --url <url>` | Initial setup + hooks installation |
|
|
37
37
|
| `agentrace init --url <url> --proxy <proxy>` | Setup with proxy |
|
|
38
|
+
| `agentrace init --url <url> --local` | Setup for current project only |
|
|
38
39
|
| `agentrace login` | Open the web dashboard |
|
|
39
40
|
| `agentrace send` | Send transcript diff (used by hooks) |
|
|
40
41
|
| `agentrace on` | Enable hooks |
|
|
42
|
+
| `agentrace on --local` | Enable hooks for current project |
|
|
41
43
|
| `agentrace off` | Disable hooks |
|
|
44
|
+
| `agentrace off --local` | Disable hooks for current project |
|
|
42
45
|
| `agentrace uninstall` | Remove hooks and configuration |
|
|
46
|
+
| `agentrace uninstall --local` | Remove project-local settings only |
|
|
43
47
|
|
|
44
48
|
## Command Details
|
|
45
49
|
|
|
@@ -48,7 +52,14 @@ That's it! When you use Claude Code, sessions will be automatically sent to Agen
|
|
|
48
52
|
Sets up the server connection and installs Claude Code hooks.
|
|
49
53
|
|
|
50
54
|
```bash
|
|
55
|
+
# Global setup (all projects)
|
|
51
56
|
npx agentrace init --url http://localhost:9080
|
|
57
|
+
|
|
58
|
+
# Project-local setup (current project only)
|
|
59
|
+
npx agentrace init --url http://localhost:9080 --local
|
|
60
|
+
|
|
61
|
+
# Project-local with separate config file
|
|
62
|
+
npx agentrace init --url http://localhost:9080 --local --separate-local-config
|
|
52
63
|
```
|
|
53
64
|
|
|
54
65
|
**Process flow:**
|
|
@@ -57,6 +68,15 @@ npx agentrace init --url http://localhost:9080
|
|
|
57
68
|
2. After registration, API key is automatically retrieved
|
|
58
69
|
3. Claude Code hooks are configured
|
|
59
70
|
|
|
71
|
+
**Options:**
|
|
72
|
+
|
|
73
|
+
| Option | Description |
|
|
74
|
+
| ------ | ----------- |
|
|
75
|
+
| `--url <url>` | Server URL (required) |
|
|
76
|
+
| `--proxy <url>` | HTTP/HTTPS proxy URL |
|
|
77
|
+
| `--local` | Install hooks/MCP for current project only |
|
|
78
|
+
| `--separate-local-config` | Store config in project directory (requires --local) |
|
|
79
|
+
|
|
60
80
|
### login
|
|
61
81
|
|
|
62
82
|
Issues a login URL for the web dashboard and opens it in browser.
|
|
@@ -70,11 +90,15 @@ npx agentrace login
|
|
|
70
90
|
Toggle hooks enabled/disabled. Configuration is preserved.
|
|
71
91
|
|
|
72
92
|
```bash
|
|
73
|
-
# Temporarily stop sending
|
|
93
|
+
# Temporarily stop sending (global)
|
|
74
94
|
npx agentrace off
|
|
75
95
|
|
|
76
|
-
# Resume sending
|
|
96
|
+
# Resume sending (global)
|
|
77
97
|
npx agentrace on
|
|
98
|
+
|
|
99
|
+
# For project-local settings
|
|
100
|
+
npx agentrace off --local
|
|
101
|
+
npx agentrace on --local
|
|
78
102
|
```
|
|
79
103
|
|
|
80
104
|
### uninstall
|
|
@@ -82,7 +106,11 @@ npx agentrace on
|
|
|
82
106
|
Completely removes hooks and configuration files.
|
|
83
107
|
|
|
84
108
|
```bash
|
|
109
|
+
# Remove global settings
|
|
85
110
|
npx agentrace uninstall
|
|
111
|
+
|
|
112
|
+
# Remove project-local settings only
|
|
113
|
+
npx agentrace uninstall --local
|
|
86
114
|
```
|
|
87
115
|
|
|
88
116
|
### send
|
|
@@ -95,9 +123,28 @@ Configuration is stored in the following locations:
|
|
|
95
123
|
|
|
96
124
|
| File | Location |
|
|
97
125
|
| -------------------- | --------------------------------- |
|
|
98
|
-
| AgenTrace config | `~/.
|
|
99
|
-
| Cursor data | `~/.
|
|
126
|
+
| AgenTrace config | `~/.agentrace/config.json` |
|
|
127
|
+
| Cursor data | `~/.agentrace/cursors/` |
|
|
100
128
|
| Claude Code hooks | `~/.claude/settings.json` |
|
|
129
|
+
| MCP servers | `~/.claude.json` |
|
|
130
|
+
|
|
131
|
+
### Project-Local Configuration
|
|
132
|
+
|
|
133
|
+
With the `--local` option, hooks and MCP are configured per-project:
|
|
134
|
+
|
|
135
|
+
| File | Location |
|
|
136
|
+
| -------------------- | ---------------------------------------------- |
|
|
137
|
+
| Claude Code hooks | `{project}/.claude/settings.json` |
|
|
138
|
+
| MCP servers | `~/.claude.json` (projects.{path}.mcpServers) |
|
|
139
|
+
| Config (optional) | `{project}/.agentrace/config.json` |
|
|
140
|
+
|
|
141
|
+
To also store the config locally, use `--separate-local-config`:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npx agentrace init --url http://localhost:9080 --local --separate-local-config
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Note:** Add `.agentrace/` to your `.gitignore` when using `--separate-local-config` to avoid committing API keys.
|
|
101
148
|
|
|
102
149
|
## How It Works
|
|
103
150
|
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { ProxyAgent } from "undici";
|
|
4
|
-
import { saveConfig, getConfigPath } from "../config/manager.js";
|
|
4
|
+
import { saveConfig, getConfigPath, saveLocalConfig, getLocalConfigPath } from "../config/manager.js";
|
|
5
5
|
import { installHooks, installMcpServer, installPreToolUseHook } from "../hooks/installer.js";
|
|
6
6
|
import { startCallbackServer, getRandomPort, generateToken, } from "../utils/callback-server.js";
|
|
7
7
|
import { openBrowser, buildSetupUrl } from "../utils/browser.js";
|
|
@@ -44,6 +44,11 @@ export async function initCommand(options = {}) {
|
|
|
44
44
|
if (options.dev) {
|
|
45
45
|
console.log("[Dev Mode] Using local CLI for hooks\n");
|
|
46
46
|
}
|
|
47
|
+
if (options.local) {
|
|
48
|
+
console.log("[Local Mode] Installing hooks/MCP for this project only\n");
|
|
49
|
+
}
|
|
50
|
+
// Project directory for local scope
|
|
51
|
+
const projectDir = options.local ? process.cwd() : undefined;
|
|
47
52
|
// Generate token and start callback server
|
|
48
53
|
const token = generateToken();
|
|
49
54
|
const port = getRandomPort();
|
|
@@ -73,12 +78,22 @@ export async function initCommand(options = {}) {
|
|
|
73
78
|
const result = await callbackPromise;
|
|
74
79
|
// Save config (remove trailing slash from URL)
|
|
75
80
|
const serverUrlStr = serverUrl.toString().replace(/\/+$/, '');
|
|
76
|
-
|
|
81
|
+
const configData = {
|
|
77
82
|
server_url: serverUrlStr,
|
|
78
83
|
api_key: result.apiKey,
|
|
79
84
|
...(options.proxy && { proxy_url: options.proxy }),
|
|
80
|
-
}
|
|
81
|
-
|
|
85
|
+
};
|
|
86
|
+
if (options.local && options.separateLocalConfig && projectDir) {
|
|
87
|
+
// Save config locally in project directory
|
|
88
|
+
saveLocalConfig(projectDir, configData);
|
|
89
|
+
console.log(`✓ Config saved to ${getLocalConfigPath(projectDir)}`);
|
|
90
|
+
console.log(`⚠ Remember to add '.agentrace/' to your .gitignore`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Save config globally
|
|
94
|
+
saveConfig(configData);
|
|
95
|
+
console.log(`✓ Config saved to ${getConfigPath()}`);
|
|
96
|
+
}
|
|
82
97
|
if (options.proxy) {
|
|
83
98
|
console.log(` Proxy: ${options.proxy}`);
|
|
84
99
|
}
|
|
@@ -92,7 +107,11 @@ export async function initCommand(options = {}) {
|
|
|
92
107
|
console.log(` Hook command: ${hookCommand}`);
|
|
93
108
|
}
|
|
94
109
|
// Install hooks
|
|
95
|
-
const hookResult = installHooks({
|
|
110
|
+
const hookResult = installHooks({
|
|
111
|
+
command: hookCommand,
|
|
112
|
+
local: options.local,
|
|
113
|
+
projectDir,
|
|
114
|
+
});
|
|
96
115
|
if (hookResult.success) {
|
|
97
116
|
console.log(`✓ ${hookResult.message}`);
|
|
98
117
|
}
|
|
@@ -108,7 +127,12 @@ export async function initCommand(options = {}) {
|
|
|
108
127
|
mcpCommand = "npx";
|
|
109
128
|
mcpArgs = ["tsx", indexPath, "mcp-server"];
|
|
110
129
|
}
|
|
111
|
-
const mcpResult = installMcpServer({
|
|
130
|
+
const mcpResult = installMcpServer({
|
|
131
|
+
command: mcpCommand,
|
|
132
|
+
args: mcpArgs,
|
|
133
|
+
local: options.local,
|
|
134
|
+
projectDir,
|
|
135
|
+
});
|
|
112
136
|
if (mcpResult.success) {
|
|
113
137
|
console.log(`✓ ${mcpResult.message}`);
|
|
114
138
|
}
|
|
@@ -116,7 +140,10 @@ export async function initCommand(options = {}) {
|
|
|
116
140
|
console.error(`✗ ${mcpResult.message}`);
|
|
117
141
|
}
|
|
118
142
|
// Install PreToolUse hook for session_id injection
|
|
119
|
-
const preToolUseResult = installPreToolUseHook(
|
|
143
|
+
const preToolUseResult = installPreToolUseHook({
|
|
144
|
+
local: options.local,
|
|
145
|
+
projectDir,
|
|
146
|
+
});
|
|
120
147
|
if (preToolUseResult.success) {
|
|
121
148
|
console.log(`✓ ${preToolUseResult.message}`);
|
|
122
149
|
}
|
|
@@ -126,7 +126,9 @@ IMPORTANT GUIDELINES:
|
|
|
126
126
|
let client = null;
|
|
127
127
|
function getClient() {
|
|
128
128
|
if (!client) {
|
|
129
|
-
|
|
129
|
+
// Use CLAUDE_PROJECT_DIR to find local config if available
|
|
130
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR;
|
|
131
|
+
client = new PlanDocumentClient(projectDir);
|
|
130
132
|
}
|
|
131
133
|
return client;
|
|
132
134
|
}
|
package/dist/commands/off.d.ts
CHANGED
package/dist/commands/off.js
CHANGED
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
import { uninstallHooks, uninstallMcpServer, uninstallPreToolUseHook } from "../hooks/installer.js";
|
|
2
|
-
import { loadConfig } from "../config/manager.js";
|
|
3
|
-
export async function offCommand() {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { loadConfig, loadConfigWithFallback } from "../config/manager.js";
|
|
3
|
+
export async function offCommand(options = {}) {
|
|
4
|
+
const projectDir = options.local ? process.cwd() : undefined;
|
|
5
|
+
// Check if config exists (local config takes precedence if --local is specified)
|
|
6
|
+
const config = options.local ? loadConfigWithFallback(projectDir) : loadConfig();
|
|
6
7
|
if (!config) {
|
|
7
8
|
console.log("AgenTrace is not configured. Run 'npx agentrace init' first.");
|
|
8
9
|
return;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
if (options.local) {
|
|
12
|
+
console.log("[Local Mode] Disabling hooks/MCP for this project only\n");
|
|
13
|
+
}
|
|
14
|
+
const result = uninstallHooks({
|
|
15
|
+
local: options.local,
|
|
16
|
+
projectDir,
|
|
17
|
+
});
|
|
11
18
|
if (result.success) {
|
|
12
19
|
console.log(`✓ Hooks disabled. Your credentials are still saved.`);
|
|
13
|
-
console.log(` Run 'npx agentrace on' to re-enable.`);
|
|
20
|
+
console.log(` Run 'npx agentrace on${options.local ? " --local" : ""}' to re-enable.`);
|
|
14
21
|
}
|
|
15
22
|
else {
|
|
16
23
|
console.error(`✗ ${result.message}`);
|
|
17
24
|
}
|
|
18
25
|
// Remove PreToolUse hook
|
|
19
|
-
const preToolUseResult = uninstallPreToolUseHook(
|
|
26
|
+
const preToolUseResult = uninstallPreToolUseHook({
|
|
27
|
+
local: options.local,
|
|
28
|
+
projectDir,
|
|
29
|
+
});
|
|
20
30
|
if (preToolUseResult.success) {
|
|
21
31
|
console.log(`✓ ${preToolUseResult.message}`);
|
|
22
32
|
}
|
|
23
33
|
else {
|
|
24
34
|
console.error(`✗ ${preToolUseResult.message}`);
|
|
25
35
|
}
|
|
26
|
-
const mcpResult = uninstallMcpServer(
|
|
36
|
+
const mcpResult = uninstallMcpServer({
|
|
37
|
+
local: options.local,
|
|
38
|
+
projectDir,
|
|
39
|
+
});
|
|
27
40
|
if (mcpResult.success) {
|
|
28
41
|
console.log(`✓ ${mcpResult.message}`);
|
|
29
42
|
}
|
package/dist/commands/on.d.ts
CHANGED
package/dist/commands/on.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { installHooks, installMcpServer, installPreToolUseHook } from "../hooks/installer.js";
|
|
4
|
-
import { loadConfig } from "../config/manager.js";
|
|
4
|
+
import { loadConfig, loadConfigWithFallback } from "../config/manager.js";
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
export async function onCommand(options = {}) {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const projectDir = options.local ? process.cwd() : undefined;
|
|
9
|
+
// Check if config exists (local config takes precedence if --local is specified)
|
|
10
|
+
const config = options.local ? loadConfigWithFallback(projectDir) : loadConfig();
|
|
10
11
|
if (!config) {
|
|
11
12
|
console.log("AgenTrace is not configured. Run 'npx agentrace init' first.");
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
15
|
+
if (options.local) {
|
|
16
|
+
console.log("[Local Mode] Enabling hooks/MCP for this project only\n");
|
|
17
|
+
}
|
|
14
18
|
// Determine hook command
|
|
15
19
|
let hookCommand;
|
|
16
20
|
if (options.dev) {
|
|
@@ -19,7 +23,11 @@ export async function onCommand(options = {}) {
|
|
|
19
23
|
const indexPath = path.join(cliRoot, "src/index.ts");
|
|
20
24
|
hookCommand = `npx tsx ${indexPath} send`;
|
|
21
25
|
}
|
|
22
|
-
const result = installHooks({
|
|
26
|
+
const result = installHooks({
|
|
27
|
+
command: hookCommand,
|
|
28
|
+
local: options.local,
|
|
29
|
+
projectDir,
|
|
30
|
+
});
|
|
23
31
|
if (result.success) {
|
|
24
32
|
console.log(`✓ Hooks enabled. Session data will be sent to ${config.server_url}`);
|
|
25
33
|
}
|
|
@@ -35,7 +43,12 @@ export async function onCommand(options = {}) {
|
|
|
35
43
|
mcpCommand = "npx";
|
|
36
44
|
mcpArgs = ["tsx", indexPath, "mcp-server"];
|
|
37
45
|
}
|
|
38
|
-
const mcpResult = installMcpServer({
|
|
46
|
+
const mcpResult = installMcpServer({
|
|
47
|
+
command: mcpCommand,
|
|
48
|
+
args: mcpArgs,
|
|
49
|
+
local: options.local,
|
|
50
|
+
projectDir,
|
|
51
|
+
});
|
|
39
52
|
if (mcpResult.success) {
|
|
40
53
|
console.log(`✓ ${mcpResult.message}`);
|
|
41
54
|
}
|
|
@@ -43,7 +56,10 @@ export async function onCommand(options = {}) {
|
|
|
43
56
|
console.error(`✗ ${mcpResult.message}`);
|
|
44
57
|
}
|
|
45
58
|
// Install PreToolUse hook for session_id injection
|
|
46
|
-
const preToolUseResult = installPreToolUseHook(
|
|
59
|
+
const preToolUseResult = installPreToolUseHook({
|
|
60
|
+
local: options.local,
|
|
61
|
+
projectDir,
|
|
62
|
+
});
|
|
47
63
|
if (preToolUseResult.success) {
|
|
48
64
|
console.log(`✓ ${preToolUseResult.message}`);
|
|
49
65
|
}
|
package/dist/commands/send.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import { loadConfig } from "../config/manager.js";
|
|
2
|
+
import { loadConfig, loadConfigWithFallback } from "../config/manager.js";
|
|
3
3
|
import { getNewLines, saveCursor, hasCursor } from "../config/cursor.js";
|
|
4
4
|
import { sendIngest } from "../utils/http.js";
|
|
5
5
|
import { findSessionFile, extractCwdFromTranscript, } from "../utils/session-finder.js";
|
|
@@ -41,8 +41,8 @@ async function sendTranscript(params) {
|
|
|
41
41
|
console.error(message);
|
|
42
42
|
process.exit(isHook ? 0 : 1);
|
|
43
43
|
};
|
|
44
|
-
// Check if config exists
|
|
45
|
-
const config =
|
|
44
|
+
// Check if config exists (local config takes precedence over global)
|
|
45
|
+
const config = loadConfigWithFallback(cwd);
|
|
46
46
|
if (!config) {
|
|
47
47
|
exitWithError("[agentrace] Warning: Config not found. Run 'npx agentrace init' first.");
|
|
48
48
|
return;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { deleteConfig } from "../config/manager.js";
|
|
1
|
+
import { deleteConfig, deleteLocalConfig } from "../config/manager.js";
|
|
2
2
|
import { uninstallHooks, uninstallMcpServer, uninstallPreToolUseHook } from "../hooks/installer.js";
|
|
3
|
-
export async function uninstallCommand() {
|
|
4
|
-
|
|
3
|
+
export async function uninstallCommand(options = {}) {
|
|
4
|
+
const projectDir = options.local ? process.cwd() : undefined;
|
|
5
|
+
if (options.local) {
|
|
6
|
+
console.log("Uninstalling AgenTrace (local settings only)...\n");
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
console.log("Uninstalling AgenTrace...\n");
|
|
10
|
+
}
|
|
5
11
|
// Remove hooks
|
|
6
|
-
const hookResult = uninstallHooks(
|
|
12
|
+
const hookResult = uninstallHooks({
|
|
13
|
+
local: options.local,
|
|
14
|
+
projectDir,
|
|
15
|
+
});
|
|
7
16
|
if (hookResult.success) {
|
|
8
17
|
console.log(`✓ ${hookResult.message}`);
|
|
9
18
|
}
|
|
@@ -11,7 +20,12 @@ export async function uninstallCommand() {
|
|
|
11
20
|
console.error(`✗ ${hookResult.message}`);
|
|
12
21
|
}
|
|
13
22
|
// Remove PreToolUse hook
|
|
14
|
-
const preToolUseResult = uninstallPreToolUseHook(
|
|
23
|
+
const preToolUseResult = uninstallPreToolUseHook({
|
|
24
|
+
local: options.local,
|
|
25
|
+
projectDir,
|
|
26
|
+
// Don't remove the hook script when uninstalling local settings
|
|
27
|
+
removeScript: !options.local,
|
|
28
|
+
});
|
|
15
29
|
if (preToolUseResult.success) {
|
|
16
30
|
console.log(`✓ ${preToolUseResult.message}`);
|
|
17
31
|
}
|
|
@@ -19,7 +33,10 @@ export async function uninstallCommand() {
|
|
|
19
33
|
console.error(`✗ ${preToolUseResult.message}`);
|
|
20
34
|
}
|
|
21
35
|
// Remove MCP server
|
|
22
|
-
const mcpResult = uninstallMcpServer(
|
|
36
|
+
const mcpResult = uninstallMcpServer({
|
|
37
|
+
local: options.local,
|
|
38
|
+
projectDir,
|
|
39
|
+
});
|
|
23
40
|
if (mcpResult.success) {
|
|
24
41
|
console.log(`✓ ${mcpResult.message}`);
|
|
25
42
|
}
|
|
@@ -27,12 +44,25 @@ export async function uninstallCommand() {
|
|
|
27
44
|
console.error(`✗ ${mcpResult.message}`);
|
|
28
45
|
}
|
|
29
46
|
// Remove config
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
if (options.local && projectDir) {
|
|
48
|
+
// Remove local config directory
|
|
49
|
+
const configRemoved = deleteLocalConfig(projectDir);
|
|
50
|
+
if (configRemoved) {
|
|
51
|
+
console.log("✓ Local config removed (.agentrace/)");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log("✓ No local config to remove");
|
|
55
|
+
}
|
|
33
56
|
}
|
|
34
57
|
else {
|
|
35
|
-
|
|
58
|
+
// Remove global config
|
|
59
|
+
const configRemoved = deleteConfig();
|
|
60
|
+
if (configRemoved) {
|
|
61
|
+
console.log("✓ Config removed");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log("✓ No config to remove");
|
|
65
|
+
}
|
|
36
66
|
}
|
|
37
67
|
console.log("\nUninstall complete!");
|
|
38
68
|
}
|
package/dist/config/manager.d.ts
CHANGED
|
@@ -7,3 +7,12 @@ export declare function getConfigPath(): string;
|
|
|
7
7
|
export declare function loadConfig(): AgentraceConfig | null;
|
|
8
8
|
export declare function saveConfig(config: AgentraceConfig): void;
|
|
9
9
|
export declare function deleteConfig(): boolean;
|
|
10
|
+
export declare function getLocalConfigDir(projectDir: string): string;
|
|
11
|
+
export declare function getLocalConfigPath(projectDir: string): string;
|
|
12
|
+
export declare function loadLocalConfig(projectDir: string): AgentraceConfig | null;
|
|
13
|
+
export declare function saveLocalConfig(projectDir: string, config: AgentraceConfig): void;
|
|
14
|
+
export declare function deleteLocalConfig(projectDir: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Load config with fallback: local config > global config
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadConfigWithFallback(projectDir?: string): AgentraceConfig | null;
|
package/dist/config/manager.js
CHANGED
|
@@ -36,3 +36,55 @@ export function deleteConfig() {
|
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
// --- Local (project-level) config functions ---
|
|
40
|
+
export function getLocalConfigDir(projectDir) {
|
|
41
|
+
return path.join(projectDir, ".agentrace");
|
|
42
|
+
}
|
|
43
|
+
export function getLocalConfigPath(projectDir) {
|
|
44
|
+
return path.join(getLocalConfigDir(projectDir), "config.json");
|
|
45
|
+
}
|
|
46
|
+
export function loadLocalConfig(projectDir) {
|
|
47
|
+
const configFile = getLocalConfigPath(projectDir);
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(configFile)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const content = fs.readFileSync(configFile, "utf-8");
|
|
53
|
+
return JSON.parse(content);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function saveLocalConfig(projectDir, config) {
|
|
60
|
+
const localConfigDir = getLocalConfigDir(projectDir);
|
|
61
|
+
const localConfigFile = getLocalConfigPath(projectDir);
|
|
62
|
+
if (!fs.existsSync(localConfigDir)) {
|
|
63
|
+
fs.mkdirSync(localConfigDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
fs.writeFileSync(localConfigFile, JSON.stringify(config, null, 2));
|
|
66
|
+
}
|
|
67
|
+
export function deleteLocalConfig(projectDir) {
|
|
68
|
+
const localConfigDir = getLocalConfigDir(projectDir);
|
|
69
|
+
try {
|
|
70
|
+
if (fs.existsSync(localConfigDir)) {
|
|
71
|
+
fs.rmSync(localConfigDir, { recursive: true });
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Load config with fallback: local config > global config
|
|
82
|
+
*/
|
|
83
|
+
export function loadConfigWithFallback(projectDir) {
|
|
84
|
+
if (projectDir) {
|
|
85
|
+
const localConfig = loadLocalConfig(projectDir);
|
|
86
|
+
if (localConfig)
|
|
87
|
+
return localConfig;
|
|
88
|
+
}
|
|
89
|
+
return loadConfig();
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { getConfigPath, loadLocalConfig, saveLocalConfig, deleteLocalConfig, getLocalConfigPath, loadConfigWithFallback, } from "./manager.js";
|
|
6
|
+
describe("config/manager", () => {
|
|
7
|
+
const testConfig = {
|
|
8
|
+
server_url: "http://localhost:8080",
|
|
9
|
+
api_key: "agtr_test_key",
|
|
10
|
+
};
|
|
11
|
+
describe("global config", () => {
|
|
12
|
+
it("getConfigPath returns expected path", () => {
|
|
13
|
+
expect(getConfigPath()).toBe(path.join(os.homedir(), ".agentrace", "config.json"));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe("local config", () => {
|
|
17
|
+
let tempProjectDir;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tempProjectDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentrace-project-"));
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
if (fs.existsSync(tempProjectDir)) {
|
|
23
|
+
fs.rmSync(tempProjectDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
it("getLocalConfigPath returns correct path", () => {
|
|
27
|
+
const configPath = getLocalConfigPath(tempProjectDir);
|
|
28
|
+
expect(configPath).toBe(path.join(tempProjectDir, ".agentrace", "config.json"));
|
|
29
|
+
});
|
|
30
|
+
it("saveLocalConfig creates config file", () => {
|
|
31
|
+
saveLocalConfig(tempProjectDir, testConfig);
|
|
32
|
+
const configPath = getLocalConfigPath(tempProjectDir);
|
|
33
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
34
|
+
const content = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
35
|
+
expect(content).toEqual(testConfig);
|
|
36
|
+
});
|
|
37
|
+
it("loadLocalConfig returns config when exists", () => {
|
|
38
|
+
saveLocalConfig(tempProjectDir, testConfig);
|
|
39
|
+
const loaded = loadLocalConfig(tempProjectDir);
|
|
40
|
+
expect(loaded).toEqual(testConfig);
|
|
41
|
+
});
|
|
42
|
+
it("loadLocalConfig returns null when config does not exist", () => {
|
|
43
|
+
const loaded = loadLocalConfig(tempProjectDir);
|
|
44
|
+
expect(loaded).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
it("deleteLocalConfig removes config directory", () => {
|
|
47
|
+
saveLocalConfig(tempProjectDir, testConfig);
|
|
48
|
+
const configDir = path.join(tempProjectDir, ".agentrace");
|
|
49
|
+
expect(fs.existsSync(configDir)).toBe(true);
|
|
50
|
+
const result = deleteLocalConfig(tempProjectDir);
|
|
51
|
+
expect(result).toBe(true);
|
|
52
|
+
expect(fs.existsSync(configDir)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it("deleteLocalConfig returns false when no config exists", () => {
|
|
55
|
+
const result = deleteLocalConfig(tempProjectDir);
|
|
56
|
+
expect(result).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("loadConfigWithFallback", () => {
|
|
60
|
+
let tempProjectDir;
|
|
61
|
+
const localConfig = {
|
|
62
|
+
server_url: "http://local:8080",
|
|
63
|
+
api_key: "test_local_key",
|
|
64
|
+
};
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
tempProjectDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentrace-project-"));
|
|
67
|
+
});
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
if (fs.existsSync(tempProjectDir)) {
|
|
70
|
+
fs.rmSync(tempProjectDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
it("returns local config when it exists", () => {
|
|
74
|
+
saveLocalConfig(tempProjectDir, localConfig);
|
|
75
|
+
const loaded = loadConfigWithFallback(tempProjectDir);
|
|
76
|
+
expect(loaded).toEqual(localConfig);
|
|
77
|
+
});
|
|
78
|
+
it("prefers local config over global config", () => {
|
|
79
|
+
// Save local config
|
|
80
|
+
saveLocalConfig(tempProjectDir, localConfig);
|
|
81
|
+
// loadConfigWithFallback should return local config
|
|
82
|
+
const loaded = loadConfigWithFallback(tempProjectDir);
|
|
83
|
+
expect(loaded).toEqual(localConfig);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,34 +1,67 @@
|
|
|
1
1
|
export interface InstallHooksOptions {
|
|
2
2
|
command?: string;
|
|
3
|
+
local?: boolean;
|
|
4
|
+
projectDir?: string;
|
|
3
5
|
}
|
|
4
6
|
export declare function installHooks(options?: InstallHooksOptions): {
|
|
5
7
|
success: boolean;
|
|
6
8
|
message: string;
|
|
7
9
|
};
|
|
8
|
-
export
|
|
10
|
+
export interface UninstallHooksOptions {
|
|
11
|
+
local?: boolean;
|
|
12
|
+
projectDir?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function uninstallHooks(options?: UninstallHooksOptions): {
|
|
9
15
|
success: boolean;
|
|
10
16
|
message: string;
|
|
11
17
|
};
|
|
12
|
-
export
|
|
18
|
+
export interface CheckHooksOptions {
|
|
19
|
+
local?: boolean;
|
|
20
|
+
projectDir?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function checkHooksInstalled(options?: CheckHooksOptions): boolean;
|
|
13
23
|
export interface InstallMcpServerOptions {
|
|
14
24
|
command?: string;
|
|
15
25
|
args?: string[];
|
|
26
|
+
local?: boolean;
|
|
27
|
+
projectDir?: string;
|
|
16
28
|
}
|
|
17
29
|
export declare function installMcpServer(options?: InstallMcpServerOptions): {
|
|
18
30
|
success: boolean;
|
|
19
31
|
message: string;
|
|
20
32
|
};
|
|
21
|
-
export
|
|
33
|
+
export interface UninstallMcpServerOptions {
|
|
34
|
+
local?: boolean;
|
|
35
|
+
projectDir?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function uninstallMcpServer(options?: UninstallMcpServerOptions): {
|
|
22
38
|
success: boolean;
|
|
23
39
|
message: string;
|
|
24
40
|
};
|
|
25
|
-
export
|
|
26
|
-
|
|
41
|
+
export interface CheckMcpServerOptions {
|
|
42
|
+
local?: boolean;
|
|
43
|
+
projectDir?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function checkMcpServerInstalled(options?: CheckMcpServerOptions): boolean;
|
|
46
|
+
export interface InstallPreToolUseHookOptions {
|
|
47
|
+
local?: boolean;
|
|
48
|
+
projectDir?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function installPreToolUseHook(options?: InstallPreToolUseHookOptions): {
|
|
27
51
|
success: boolean;
|
|
28
52
|
message: string;
|
|
29
53
|
};
|
|
30
|
-
export
|
|
54
|
+
export interface UninstallPreToolUseHookOptions {
|
|
55
|
+
local?: boolean;
|
|
56
|
+
projectDir?: string;
|
|
57
|
+
removeScript?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare function uninstallPreToolUseHook(options?: UninstallPreToolUseHookOptions): {
|
|
31
60
|
success: boolean;
|
|
32
61
|
message: string;
|
|
33
62
|
};
|
|
34
|
-
export
|
|
63
|
+
export interface CheckPreToolUseHookOptions {
|
|
64
|
+
local?: boolean;
|
|
65
|
+
projectDir?: string;
|
|
66
|
+
}
|
|
67
|
+
export declare function checkPreToolUseHookInstalled(options?: CheckPreToolUseHookOptions): boolean;
|
package/dist/hooks/installer.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as os from "node:os";
|
|
4
|
-
const
|
|
4
|
+
const GLOBAL_CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
5
5
|
// MCP servers are configured in ~/.claude.json, NOT in settings.json
|
|
6
6
|
const CLAUDE_CONFIG_PATH = path.join(os.homedir(), ".claude.json");
|
|
7
7
|
// Agentrace hooks directory
|
|
8
8
|
const AGENTRACE_HOOKS_DIR = path.join(os.homedir(), ".agentrace", "hooks");
|
|
9
9
|
const SESSION_ID_HOOK_PATH = path.join(AGENTRACE_HOOKS_DIR, "inject-session-id.js");
|
|
10
|
+
// Helper to get settings path based on local/global scope
|
|
11
|
+
function getSettingsPath(options) {
|
|
12
|
+
if (options.local && options.projectDir) {
|
|
13
|
+
return path.join(options.projectDir, ".claude", "settings.json");
|
|
14
|
+
}
|
|
15
|
+
return GLOBAL_CLAUDE_SETTINGS_PATH;
|
|
16
|
+
}
|
|
10
17
|
const DEFAULT_COMMAND = "npx agentrace send";
|
|
11
18
|
function createAgentraceHook(command) {
|
|
12
19
|
return {
|
|
@@ -21,11 +28,12 @@ function isAgentraceHook(hook) {
|
|
|
21
28
|
export function installHooks(options = {}) {
|
|
22
29
|
const command = options.command || DEFAULT_COMMAND;
|
|
23
30
|
const agentraceHook = createAgentraceHook(command);
|
|
31
|
+
const settingsPath = getSettingsPath(options);
|
|
24
32
|
try {
|
|
25
33
|
let settings = {};
|
|
26
34
|
// Load existing settings if file exists
|
|
27
|
-
if (fs.existsSync(
|
|
28
|
-
const content = fs.readFileSync(
|
|
35
|
+
if (fs.existsSync(settingsPath)) {
|
|
36
|
+
const content = fs.readFileSync(settingsPath, "utf-8");
|
|
29
37
|
settings = JSON.parse(content);
|
|
30
38
|
}
|
|
31
39
|
// Initialize hooks structure if not present
|
|
@@ -76,25 +84,26 @@ export function installHooks(options = {}) {
|
|
|
76
84
|
});
|
|
77
85
|
}
|
|
78
86
|
// Ensure directory exists
|
|
79
|
-
const dir = path.dirname(
|
|
87
|
+
const dir = path.dirname(settingsPath);
|
|
80
88
|
if (!fs.existsSync(dir)) {
|
|
81
89
|
fs.mkdirSync(dir, { recursive: true });
|
|
82
90
|
}
|
|
83
91
|
// Write settings
|
|
84
|
-
fs.writeFileSync(
|
|
85
|
-
return { success: true, message: `Hooks added to ${
|
|
92
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
93
|
+
return { success: true, message: `Hooks added to ${settingsPath}` };
|
|
86
94
|
}
|
|
87
95
|
catch (error) {
|
|
88
96
|
const message = error instanceof Error ? error.message : String(error);
|
|
89
97
|
return { success: false, message: `Failed to install hooks: ${message}` };
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
|
-
export function uninstallHooks() {
|
|
100
|
+
export function uninstallHooks(options = {}) {
|
|
101
|
+
const settingsPath = getSettingsPath(options);
|
|
93
102
|
try {
|
|
94
|
-
if (!fs.existsSync(
|
|
103
|
+
if (!fs.existsSync(settingsPath)) {
|
|
95
104
|
return { success: true, message: "No settings file found" };
|
|
96
105
|
}
|
|
97
|
-
const content = fs.readFileSync(
|
|
106
|
+
const content = fs.readFileSync(settingsPath, "utf-8");
|
|
98
107
|
const settings = JSON.parse(content);
|
|
99
108
|
if (!settings.hooks) {
|
|
100
109
|
return { success: true, message: "No hooks configured" };
|
|
@@ -131,10 +140,10 @@ export function uninstallHooks() {
|
|
|
131
140
|
if (Object.keys(settings.hooks).length === 0) {
|
|
132
141
|
delete settings.hooks;
|
|
133
142
|
}
|
|
134
|
-
fs.writeFileSync(
|
|
143
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
135
144
|
return {
|
|
136
145
|
success: true,
|
|
137
|
-
message: `Removed hooks from ${
|
|
146
|
+
message: `Removed hooks from ${settingsPath}`,
|
|
138
147
|
};
|
|
139
148
|
}
|
|
140
149
|
catch (error) {
|
|
@@ -142,12 +151,13 @@ export function uninstallHooks() {
|
|
|
142
151
|
return { success: false, message: `Failed to uninstall hooks: ${message}` };
|
|
143
152
|
}
|
|
144
153
|
}
|
|
145
|
-
export function checkHooksInstalled() {
|
|
154
|
+
export function checkHooksInstalled(options = {}) {
|
|
155
|
+
const settingsPath = getSettingsPath(options);
|
|
146
156
|
try {
|
|
147
|
-
if (!fs.existsSync(
|
|
157
|
+
if (!fs.existsSync(settingsPath)) {
|
|
148
158
|
return false;
|
|
149
159
|
}
|
|
150
|
-
const content = fs.readFileSync(
|
|
160
|
+
const content = fs.readFileSync(settingsPath, "utf-8");
|
|
151
161
|
const settings = JSON.parse(content);
|
|
152
162
|
const hasStopHook = settings.hooks?.Stop?.some((matcher) => matcher.hooks?.some(isAgentraceHook));
|
|
153
163
|
const hasUserPromptSubmitHook = settings.hooks?.UserPromptSubmit?.some((matcher) => matcher.hooks?.some(isAgentraceHook));
|
|
@@ -172,63 +182,107 @@ export function installMcpServer(options = {}) {
|
|
|
172
182
|
const content = fs.readFileSync(CLAUDE_CONFIG_PATH, "utf-8");
|
|
173
183
|
config = JSON.parse(content);
|
|
174
184
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
config.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
if (options.local && options.projectDir) {
|
|
186
|
+
// Local scope: add to projects.{projectDir}.mcpServers
|
|
187
|
+
if (!config.projects) {
|
|
188
|
+
config.projects = {};
|
|
189
|
+
}
|
|
190
|
+
if (!config.projects[options.projectDir]) {
|
|
191
|
+
config.projects[options.projectDir] = {};
|
|
192
|
+
}
|
|
193
|
+
if (!config.projects[options.projectDir].mcpServers) {
|
|
194
|
+
config.projects[options.projectDir].mcpServers = {};
|
|
195
|
+
}
|
|
196
|
+
const projectMcpServers = config.projects[options.projectDir].mcpServers;
|
|
197
|
+
const alreadyExists = !!projectMcpServers[MCP_SERVER_NAME];
|
|
198
|
+
projectMcpServers[MCP_SERVER_NAME] = { command, args };
|
|
199
|
+
fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
message: alreadyExists
|
|
203
|
+
? "MCP server config updated (local)"
|
|
204
|
+
: `MCP server added to ${CLAUDE_CONFIG_PATH} (local scope for ${options.projectDir})`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// User scope: add to mcpServers (global)
|
|
209
|
+
if (!config.mcpServers) {
|
|
210
|
+
config.mcpServers = {};
|
|
211
|
+
}
|
|
212
|
+
const alreadyExists = !!config.mcpServers[MCP_SERVER_NAME];
|
|
182
213
|
config.mcpServers[MCP_SERVER_NAME] = { command, args };
|
|
183
214
|
fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
184
|
-
return {
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
message: alreadyExists
|
|
218
|
+
? "MCP server config updated"
|
|
219
|
+
: `MCP server added to ${CLAUDE_CONFIG_PATH}`,
|
|
220
|
+
};
|
|
185
221
|
}
|
|
186
|
-
// Add MCP server config
|
|
187
|
-
config.mcpServers[MCP_SERVER_NAME] = { command, args };
|
|
188
|
-
// Write config
|
|
189
|
-
fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
190
|
-
return { success: true, message: `MCP server added to ${CLAUDE_CONFIG_PATH}` };
|
|
191
222
|
}
|
|
192
223
|
catch (error) {
|
|
193
224
|
const message = error instanceof Error ? error.message : String(error);
|
|
194
225
|
return { success: false, message: `Failed to install MCP server: ${message}` };
|
|
195
226
|
}
|
|
196
227
|
}
|
|
197
|
-
export function uninstallMcpServer() {
|
|
228
|
+
export function uninstallMcpServer(options = {}) {
|
|
198
229
|
try {
|
|
199
230
|
if (!fs.existsSync(CLAUDE_CONFIG_PATH)) {
|
|
200
231
|
return { success: true, message: "No config file found" };
|
|
201
232
|
}
|
|
202
233
|
const content = fs.readFileSync(CLAUDE_CONFIG_PATH, "utf-8");
|
|
203
234
|
const config = JSON.parse(content);
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
235
|
+
if (options.local && options.projectDir) {
|
|
236
|
+
// Local scope: remove from projects.{projectDir}.mcpServers
|
|
237
|
+
if (!config.projects?.[options.projectDir]?.mcpServers?.[MCP_SERVER_NAME]) {
|
|
238
|
+
return { success: true, message: "MCP server not configured (local)" };
|
|
239
|
+
}
|
|
240
|
+
delete config.projects[options.projectDir].mcpServers[MCP_SERVER_NAME];
|
|
241
|
+
// Clean up empty mcpServers object
|
|
242
|
+
if (Object.keys(config.projects[options.projectDir].mcpServers).length === 0) {
|
|
243
|
+
delete config.projects[options.projectDir].mcpServers;
|
|
244
|
+
}
|
|
245
|
+
fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
246
|
+
return {
|
|
247
|
+
success: true,
|
|
248
|
+
message: `Removed MCP server from ${CLAUDE_CONFIG_PATH} (local scope)`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// User scope: remove from mcpServers (global)
|
|
253
|
+
if (!config.mcpServers || !config.mcpServers[MCP_SERVER_NAME]) {
|
|
254
|
+
return { success: true, message: "MCP server not configured" };
|
|
255
|
+
}
|
|
256
|
+
delete config.mcpServers[MCP_SERVER_NAME];
|
|
257
|
+
// Clean up empty mcpServers object
|
|
258
|
+
if (Object.keys(config.mcpServers).length === 0) {
|
|
259
|
+
delete config.mcpServers;
|
|
260
|
+
}
|
|
261
|
+
fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
262
|
+
return {
|
|
263
|
+
success: true,
|
|
264
|
+
message: `Removed MCP server from ${CLAUDE_CONFIG_PATH}`,
|
|
265
|
+
};
|
|
212
266
|
}
|
|
213
|
-
fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
214
|
-
return {
|
|
215
|
-
success: true,
|
|
216
|
-
message: `Removed MCP server from ${CLAUDE_CONFIG_PATH}`,
|
|
217
|
-
};
|
|
218
267
|
}
|
|
219
268
|
catch (error) {
|
|
220
269
|
const message = error instanceof Error ? error.message : String(error);
|
|
221
270
|
return { success: false, message: `Failed to uninstall MCP server: ${message}` };
|
|
222
271
|
}
|
|
223
272
|
}
|
|
224
|
-
export function checkMcpServerInstalled() {
|
|
273
|
+
export function checkMcpServerInstalled(options = {}) {
|
|
225
274
|
try {
|
|
226
275
|
if (!fs.existsSync(CLAUDE_CONFIG_PATH)) {
|
|
227
276
|
return false;
|
|
228
277
|
}
|
|
229
278
|
const content = fs.readFileSync(CLAUDE_CONFIG_PATH, "utf-8");
|
|
230
279
|
const config = JSON.parse(content);
|
|
231
|
-
|
|
280
|
+
if (options.local && options.projectDir) {
|
|
281
|
+
return !!config.projects?.[options.projectDir]?.mcpServers?.[MCP_SERVER_NAME];
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
return !!config.mcpServers?.[MCP_SERVER_NAME];
|
|
285
|
+
}
|
|
232
286
|
}
|
|
233
287
|
catch {
|
|
234
288
|
return false;
|
|
@@ -276,7 +330,8 @@ function isAgentracePreToolUseHook(matcher) {
|
|
|
276
330
|
return matcher.matcher === AGENTRACE_MCP_TOOLS_MATCHER &&
|
|
277
331
|
matcher.hooks?.some(h => h.command?.includes("inject-session-id"));
|
|
278
332
|
}
|
|
279
|
-
export function installPreToolUseHook() {
|
|
333
|
+
export function installPreToolUseHook(options = {}) {
|
|
334
|
+
const settingsPath = getSettingsPath(options);
|
|
280
335
|
try {
|
|
281
336
|
// Create hooks directory if not exists
|
|
282
337
|
if (!fs.existsSync(AGENTRACE_HOOKS_DIR)) {
|
|
@@ -286,8 +341,8 @@ export function installPreToolUseHook() {
|
|
|
286
341
|
fs.writeFileSync(SESSION_ID_HOOK_PATH, SESSION_ID_HOOK_SCRIPT, { mode: 0o755 });
|
|
287
342
|
// Load existing settings
|
|
288
343
|
let settings = {};
|
|
289
|
-
if (fs.existsSync(
|
|
290
|
-
const content = fs.readFileSync(
|
|
344
|
+
if (fs.existsSync(settingsPath)) {
|
|
345
|
+
const content = fs.readFileSync(settingsPath, "utf-8");
|
|
291
346
|
settings = JSON.parse(content);
|
|
292
347
|
}
|
|
293
348
|
// Initialize hooks structure if not present
|
|
@@ -313,12 +368,12 @@ export function installPreToolUseHook() {
|
|
|
313
368
|
],
|
|
314
369
|
});
|
|
315
370
|
// Ensure directory exists
|
|
316
|
-
const dir = path.dirname(
|
|
371
|
+
const dir = path.dirname(settingsPath);
|
|
317
372
|
if (!fs.existsSync(dir)) {
|
|
318
373
|
fs.mkdirSync(dir, { recursive: true });
|
|
319
374
|
}
|
|
320
375
|
// Write settings
|
|
321
|
-
fs.writeFileSync(
|
|
376
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
322
377
|
return { success: true, message: `PreToolUse hook installed to ${SESSION_ID_HOOK_PATH}` };
|
|
323
378
|
}
|
|
324
379
|
catch (error) {
|
|
@@ -326,17 +381,18 @@ export function installPreToolUseHook() {
|
|
|
326
381
|
return { success: false, message: `Failed to install PreToolUse hook: ${message}` };
|
|
327
382
|
}
|
|
328
383
|
}
|
|
329
|
-
export function uninstallPreToolUseHook() {
|
|
384
|
+
export function uninstallPreToolUseHook(options = {}) {
|
|
385
|
+
const settingsPath = getSettingsPath(options);
|
|
330
386
|
try {
|
|
331
|
-
// Remove hook script
|
|
332
|
-
if (fs.existsSync(SESSION_ID_HOOK_PATH)) {
|
|
387
|
+
// Remove hook script only when uninstalling global hooks (not local)
|
|
388
|
+
if (!options.local && options.removeScript !== false && fs.existsSync(SESSION_ID_HOOK_PATH)) {
|
|
333
389
|
fs.unlinkSync(SESSION_ID_HOOK_PATH);
|
|
334
390
|
}
|
|
335
391
|
// Remove from settings
|
|
336
|
-
if (!fs.existsSync(
|
|
392
|
+
if (!fs.existsSync(settingsPath)) {
|
|
337
393
|
return { success: true, message: "No settings file found" };
|
|
338
394
|
}
|
|
339
|
-
const content = fs.readFileSync(
|
|
395
|
+
const content = fs.readFileSync(settingsPath, "utf-8");
|
|
340
396
|
const settings = JSON.parse(content);
|
|
341
397
|
if (!settings.hooks?.PreToolUse) {
|
|
342
398
|
return { success: true, message: "No PreToolUse hooks configured" };
|
|
@@ -350,7 +406,7 @@ export function uninstallPreToolUseHook() {
|
|
|
350
406
|
if (Object.keys(settings.hooks).length === 0) {
|
|
351
407
|
delete settings.hooks;
|
|
352
408
|
}
|
|
353
|
-
fs.writeFileSync(
|
|
409
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
354
410
|
return { success: true, message: "PreToolUse hook removed" };
|
|
355
411
|
}
|
|
356
412
|
catch (error) {
|
|
@@ -358,12 +414,13 @@ export function uninstallPreToolUseHook() {
|
|
|
358
414
|
return { success: false, message: `Failed to uninstall PreToolUse hook: ${message}` };
|
|
359
415
|
}
|
|
360
416
|
}
|
|
361
|
-
export function checkPreToolUseHookInstalled() {
|
|
417
|
+
export function checkPreToolUseHookInstalled(options = {}) {
|
|
418
|
+
const settingsPath = getSettingsPath(options);
|
|
362
419
|
try {
|
|
363
|
-
if (!fs.existsSync(
|
|
420
|
+
if (!fs.existsSync(settingsPath) || !fs.existsSync(SESSION_ID_HOOK_PATH)) {
|
|
364
421
|
return false;
|
|
365
422
|
}
|
|
366
|
-
const content = fs.readFileSync(
|
|
423
|
+
const content = fs.readFileSync(settingsPath, "utf-8");
|
|
367
424
|
const settings = JSON.parse(content);
|
|
368
425
|
return settings.hooks?.PreToolUse?.some(isAgentracePreToolUseHook) ?? false;
|
|
369
426
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { installHooks, uninstallHooks, checkHooksInstalled, installMcpServer, uninstallMcpServer, checkMcpServerInstalled, } from "./installer.js";
|
|
6
|
+
describe("hooks/installer", () => {
|
|
7
|
+
describe("local hooks", () => {
|
|
8
|
+
let tempProjectDir;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tempProjectDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentrace-project-"));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
if (fs.existsSync(tempProjectDir)) {
|
|
14
|
+
fs.rmSync(tempProjectDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
it("installHooks creates local settings.json", () => {
|
|
18
|
+
const result = installHooks({
|
|
19
|
+
local: true,
|
|
20
|
+
projectDir: tempProjectDir,
|
|
21
|
+
});
|
|
22
|
+
expect(result.success).toBe(true);
|
|
23
|
+
const settingsPath = path.join(tempProjectDir, ".claude", "settings.json");
|
|
24
|
+
expect(fs.existsSync(settingsPath)).toBe(true);
|
|
25
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
26
|
+
expect(settings.hooks).toBeDefined();
|
|
27
|
+
expect(settings.hooks.Stop).toBeDefined();
|
|
28
|
+
expect(settings.hooks.UserPromptSubmit).toBeDefined();
|
|
29
|
+
expect(settings.hooks.SubagentStop).toBeDefined();
|
|
30
|
+
expect(settings.hooks.PostToolUse).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
it("installHooks with custom command", () => {
|
|
33
|
+
const customCommand = "npx custom-cli send";
|
|
34
|
+
const result = installHooks({
|
|
35
|
+
command: customCommand,
|
|
36
|
+
local: true,
|
|
37
|
+
projectDir: tempProjectDir,
|
|
38
|
+
});
|
|
39
|
+
expect(result.success).toBe(true);
|
|
40
|
+
const settingsPath = path.join(tempProjectDir, ".claude", "settings.json");
|
|
41
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
42
|
+
expect(settings.hooks.Stop[0].hooks[0].command).toBe(customCommand);
|
|
43
|
+
});
|
|
44
|
+
it("checkHooksInstalled returns true for local hooks", () => {
|
|
45
|
+
installHooks({
|
|
46
|
+
local: true,
|
|
47
|
+
projectDir: tempProjectDir,
|
|
48
|
+
});
|
|
49
|
+
const installed = checkHooksInstalled({
|
|
50
|
+
local: true,
|
|
51
|
+
projectDir: tempProjectDir,
|
|
52
|
+
});
|
|
53
|
+
expect(installed).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it("checkHooksInstalled returns false when no local hooks", () => {
|
|
56
|
+
const installed = checkHooksInstalled({
|
|
57
|
+
local: true,
|
|
58
|
+
projectDir: tempProjectDir,
|
|
59
|
+
});
|
|
60
|
+
expect(installed).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it("uninstallHooks removes local hooks", () => {
|
|
63
|
+
installHooks({
|
|
64
|
+
local: true,
|
|
65
|
+
projectDir: tempProjectDir,
|
|
66
|
+
});
|
|
67
|
+
const result = uninstallHooks({
|
|
68
|
+
local: true,
|
|
69
|
+
projectDir: tempProjectDir,
|
|
70
|
+
});
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
const installed = checkHooksInstalled({
|
|
73
|
+
local: true,
|
|
74
|
+
projectDir: tempProjectDir,
|
|
75
|
+
});
|
|
76
|
+
expect(installed).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("local MCP server", () => {
|
|
80
|
+
let tempProjectDir;
|
|
81
|
+
let claudeJsonPath;
|
|
82
|
+
let originalClaudeJson = null;
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
tempProjectDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentrace-project-"));
|
|
85
|
+
claudeJsonPath = path.join(os.homedir(), ".claude.json");
|
|
86
|
+
// Backup original ~/.claude.json if exists
|
|
87
|
+
if (fs.existsSync(claudeJsonPath)) {
|
|
88
|
+
originalClaudeJson = fs.readFileSync(claudeJsonPath, "utf-8");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
if (fs.existsSync(tempProjectDir)) {
|
|
93
|
+
fs.rmSync(tempProjectDir, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
// Restore original ~/.claude.json
|
|
96
|
+
if (originalClaudeJson !== null) {
|
|
97
|
+
fs.writeFileSync(claudeJsonPath, originalClaudeJson);
|
|
98
|
+
}
|
|
99
|
+
else if (fs.existsSync(claudeJsonPath)) {
|
|
100
|
+
// If original didn't exist but now exists, we need to clean up
|
|
101
|
+
// But we should be careful not to delete user's actual config
|
|
102
|
+
// So we just leave it as is
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
it("installMcpServer adds to projects for local scope", () => {
|
|
106
|
+
const result = installMcpServer({
|
|
107
|
+
local: true,
|
|
108
|
+
projectDir: tempProjectDir,
|
|
109
|
+
});
|
|
110
|
+
expect(result.success).toBe(true);
|
|
111
|
+
expect(result.message).toContain("local scope");
|
|
112
|
+
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, "utf-8"));
|
|
113
|
+
expect(claudeJson.projects).toBeDefined();
|
|
114
|
+
expect(claudeJson.projects[tempProjectDir]).toBeDefined();
|
|
115
|
+
expect(claudeJson.projects[tempProjectDir].mcpServers).toBeDefined();
|
|
116
|
+
expect(claudeJson.projects[tempProjectDir].mcpServers.agentrace).toBeDefined();
|
|
117
|
+
});
|
|
118
|
+
it("checkMcpServerInstalled returns true for local MCP", () => {
|
|
119
|
+
installMcpServer({
|
|
120
|
+
local: true,
|
|
121
|
+
projectDir: tempProjectDir,
|
|
122
|
+
});
|
|
123
|
+
const installed = checkMcpServerInstalled({
|
|
124
|
+
local: true,
|
|
125
|
+
projectDir: tempProjectDir,
|
|
126
|
+
});
|
|
127
|
+
expect(installed).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it("checkMcpServerInstalled returns false when no local MCP", () => {
|
|
130
|
+
const installed = checkMcpServerInstalled({
|
|
131
|
+
local: true,
|
|
132
|
+
projectDir: tempProjectDir,
|
|
133
|
+
});
|
|
134
|
+
expect(installed).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
it("uninstallMcpServer removes local MCP", () => {
|
|
137
|
+
installMcpServer({
|
|
138
|
+
local: true,
|
|
139
|
+
projectDir: tempProjectDir,
|
|
140
|
+
});
|
|
141
|
+
const result = uninstallMcpServer({
|
|
142
|
+
local: true,
|
|
143
|
+
projectDir: tempProjectDir,
|
|
144
|
+
});
|
|
145
|
+
expect(result.success).toBe(true);
|
|
146
|
+
const installed = checkMcpServerInstalled({
|
|
147
|
+
local: true,
|
|
148
|
+
projectDir: tempProjectDir,
|
|
149
|
+
});
|
|
150
|
+
expect(installed).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
it("installMcpServer with custom command and args", () => {
|
|
153
|
+
const result = installMcpServer({
|
|
154
|
+
command: "node",
|
|
155
|
+
args: ["custom-server.js"],
|
|
156
|
+
local: true,
|
|
157
|
+
projectDir: tempProjectDir,
|
|
158
|
+
});
|
|
159
|
+
expect(result.success).toBe(true);
|
|
160
|
+
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, "utf-8"));
|
|
161
|
+
const mcpConfig = claudeJson.projects[tempProjectDir].mcpServers.agentrace;
|
|
162
|
+
expect(mcpConfig.command).toBe("node");
|
|
163
|
+
expect(mcpConfig.args).toEqual(["custom-server.js"]);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,16 @@ program
|
|
|
15
15
|
.requiredOption("--url <url>", "Server URL (required)")
|
|
16
16
|
.option("--proxy <url>", "HTTP/HTTPS proxy URL")
|
|
17
17
|
.option("--dev", "Use local CLI path for development")
|
|
18
|
+
.option("--local", "Install hooks/MCP for current project only (project-local scope)")
|
|
19
|
+
.option("--separate-local-config", "Store config in project directory (requires --local)")
|
|
18
20
|
.action(async (options) => {
|
|
19
|
-
await initCommand({
|
|
21
|
+
await initCommand({
|
|
22
|
+
url: options.url,
|
|
23
|
+
proxy: options.proxy,
|
|
24
|
+
dev: options.dev,
|
|
25
|
+
local: options.local,
|
|
26
|
+
separateLocalConfig: options.separateLocalConfig,
|
|
27
|
+
});
|
|
20
28
|
});
|
|
21
29
|
program
|
|
22
30
|
.command("login")
|
|
@@ -39,21 +47,24 @@ program
|
|
|
39
47
|
program
|
|
40
48
|
.command("uninstall")
|
|
41
49
|
.description("Remove agentrace hooks and config")
|
|
42
|
-
.
|
|
43
|
-
|
|
50
|
+
.option("--local", "Remove only project-local hooks/MCP/config")
|
|
51
|
+
.action(async (options) => {
|
|
52
|
+
await uninstallCommand({ local: options.local });
|
|
44
53
|
});
|
|
45
54
|
program
|
|
46
55
|
.command("on")
|
|
47
56
|
.description("Enable agentrace hooks (credentials preserved)")
|
|
48
57
|
.option("--dev", "Use local CLI path for development")
|
|
58
|
+
.option("--local", "Enable hooks/MCP for current project only")
|
|
49
59
|
.action(async (options) => {
|
|
50
|
-
await onCommand({ dev: options.dev });
|
|
60
|
+
await onCommand({ dev: options.dev, local: options.local });
|
|
51
61
|
});
|
|
52
62
|
program
|
|
53
63
|
.command("off")
|
|
54
64
|
.description("Disable agentrace hooks (credentials preserved)")
|
|
55
|
-
.
|
|
56
|
-
|
|
65
|
+
.option("--local", "Disable hooks/MCP for current project only")
|
|
66
|
+
.action(async (options) => {
|
|
67
|
+
await offCommand({ local: options.local });
|
|
57
68
|
});
|
|
58
69
|
program
|
|
59
70
|
.command("mcp-server")
|
|
@@ -56,7 +56,7 @@ export interface UpdatePlanRequest {
|
|
|
56
56
|
export declare class PlanDocumentClient {
|
|
57
57
|
private serverUrl;
|
|
58
58
|
private apiKey;
|
|
59
|
-
constructor();
|
|
59
|
+
constructor(projectDir?: string);
|
|
60
60
|
private request;
|
|
61
61
|
searchPlans(params?: SearchPlansParams): Promise<PlanDocument[]>;
|
|
62
62
|
getPlan(id: string): Promise<PlanDocument>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { fetch } from "undici";
|
|
2
|
-
import {
|
|
2
|
+
import { loadConfigWithFallback } from "../config/manager.js";
|
|
3
3
|
import { createDispatcher } from "../utils/proxy.js";
|
|
4
4
|
export class PlanDocumentClient {
|
|
5
5
|
serverUrl;
|
|
6
6
|
apiKey;
|
|
7
|
-
constructor() {
|
|
8
|
-
const config =
|
|
7
|
+
constructor(projectDir) {
|
|
8
|
+
const config = loadConfigWithFallback(projectDir);
|
|
9
9
|
if (!config) {
|
|
10
10
|
throw new Error("AgenTrace is not configured. Run 'npx agentrace init' first.");
|
|
11
11
|
}
|