agentrace 0.0.1 → 0.0.3

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 ADDED
@@ -0,0 +1,132 @@
1
+ # AgenTrace CLI
2
+
3
+ A CLI tool to send Claude Code sessions to the AgenTrace server.
4
+
5
+ ## Overview
6
+
7
+ AgenTrace is a self-hosted service that enables teams to review Claude Code conversations. Since Claude Code logs contain source code and environment information, AgenTrace is designed to run on your local machine or internal network rather than on the public internet.
8
+
9
+ This CLI uses Claude Code's hooks feature to automatically send session data to your AgenTrace server.
10
+
11
+ ## Setup
12
+
13
+ ### 1. Start the Server
14
+
15
+ ```bash
16
+ # Using Docker (recommended)
17
+ docker run -d --name agentrace -p 9080:9080 -v $(pwd)/data:/data satetsu888/agentrace:latest
18
+ ```
19
+
20
+ Docker Hub: <https://hub.docker.com/r/satetsu888/agentrace>
21
+
22
+ ### 2. Initialize the CLI
23
+
24
+ ```bash
25
+ npx agentrace init --url http://localhost:9080
26
+ ```
27
+
28
+ A browser window will open displaying the registration/login page. After registration, hooks are automatically configured.
29
+
30
+ That's it! When you use Claude Code, sessions will be automatically sent to AgenTrace.
31
+
32
+ ## Commands
33
+
34
+ | Command | Description |
35
+ | ---------------------------- | -------------------------------------- |
36
+ | `agentrace init --url <url>` | Initial setup + hooks installation |
37
+ | `agentrace login` | Open the web dashboard |
38
+ | `agentrace send` | Send transcript diff (used by hooks) |
39
+ | `agentrace on` | Enable hooks |
40
+ | `agentrace off` | Disable hooks |
41
+ | `agentrace uninstall` | Remove hooks and configuration |
42
+
43
+ ## Command Details
44
+
45
+ ### init
46
+
47
+ Sets up the server connection and installs Claude Code hooks.
48
+
49
+ ```bash
50
+ npx agentrace init --url http://localhost:9080
51
+ ```
52
+
53
+ **Process flow:**
54
+
55
+ 1. Opens the server's registration/login page in browser
56
+ 2. After registration, API key is automatically retrieved
57
+ 3. Claude Code hooks are configured
58
+
59
+ ### login
60
+
61
+ Issues a login URL for the web dashboard and opens it in browser.
62
+
63
+ ```bash
64
+ npx agentrace login
65
+ ```
66
+
67
+ ### on / off
68
+
69
+ Toggle hooks enabled/disabled. Configuration is preserved.
70
+
71
+ ```bash
72
+ # Temporarily stop sending
73
+ npx agentrace off
74
+
75
+ # Resume sending
76
+ npx agentrace on
77
+ ```
78
+
79
+ ### uninstall
80
+
81
+ Completely removes hooks and configuration files.
82
+
83
+ ```bash
84
+ npx agentrace uninstall
85
+ ```
86
+
87
+ ### send
88
+
89
+ This command is automatically called by Claude Code's Stop hook. You normally don't need to run it manually.
90
+
91
+ ## Configuration Files
92
+
93
+ Configuration is stored in the following locations:
94
+
95
+ | File | Location |
96
+ | -------------------- | --------------------------------- |
97
+ | AgenTrace config | `~/.config/agentrace/config.json` |
98
+ | Cursor data | `~/.config/agentrace/cursors/` |
99
+ | Claude Code hooks | `~/.claude/settings.json` |
100
+
101
+ ## How It Works
102
+
103
+ ```text
104
+ ┌─────────────────┐
105
+ │ Claude Code │
106
+ │ (Stop hook) │
107
+ └────────┬────────┘
108
+ │ npx agentrace send
109
+
110
+ ┌─────────────────┐
111
+ │ AgenTrace CLI │
112
+ │ Extract & Send │
113
+ └────────┬────────┘
114
+ │ POST /api/ingest
115
+
116
+ ┌─────────────────┐
117
+ │ AgenTrace Server│
118
+ │ Save to DB │
119
+ └─────────────────┘
120
+ ```
121
+
122
+ - Only the transcript diff is sent to the server when a Claude Code conversation ends
123
+ - Errors do not block Claude Code's operation by design
124
+
125
+ ## Requirements
126
+
127
+ - Node.js 18 or later
128
+ - Claude Code installed
129
+
130
+ ## License
131
+
132
+ MIT
@@ -1,7 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { saveConfig, getConfigPath } from "../config/manager.js";
4
- import { installHooks } from "../hooks/installer.js";
4
+ import { installHooks, installMcpServer, installPreToolUseHook } from "../hooks/installer.js";
5
5
  import { startCallbackServer, getRandomPort, generateToken, } from "../utils/callback-server.js";
6
6
  import { openBrowser, buildSetupUrl } from "../utils/browser.js";
7
7
  const __filename = fileURLToPath(import.meta.url);
@@ -80,6 +80,30 @@ export async function initCommand(options = {}) {
80
80
  else {
81
81
  console.error(`✗ ${hookResult.message}`);
82
82
  }
83
+ // Install MCP server
84
+ let mcpCommand;
85
+ let mcpArgs;
86
+ if (options.dev) {
87
+ const cliRoot = path.resolve(__dirname, "../..");
88
+ const indexPath = path.join(cliRoot, "src/index.ts");
89
+ mcpCommand = "npx";
90
+ mcpArgs = ["tsx", indexPath, "mcp-server"];
91
+ }
92
+ const mcpResult = installMcpServer({ command: mcpCommand, args: mcpArgs });
93
+ if (mcpResult.success) {
94
+ console.log(`✓ ${mcpResult.message}`);
95
+ }
96
+ else {
97
+ console.error(`✗ ${mcpResult.message}`);
98
+ }
99
+ // Install PreToolUse hook for session_id injection
100
+ const preToolUseResult = installPreToolUseHook();
101
+ if (preToolUseResult.success) {
102
+ console.log(`✓ ${preToolUseResult.message}`);
103
+ }
104
+ else {
105
+ console.error(`✗ ${preToolUseResult.message}`);
106
+ }
83
107
  console.log("\n✓ Setup complete!");
84
108
  }
85
109
  catch (error) {
@@ -0,0 +1 @@
1
+ export declare function mcpServerCommand(): Promise<void>;
@@ -0,0 +1,284 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { patchMake, patchToText } from "diff-match-patch-es";
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import * as os from "node:os";
8
+ import { PlanDocumentClient } from "../mcp/plan-document-client.js";
9
+ // Read session_id from file written by PreToolUse hook
10
+ function getSessionIdFromFile() {
11
+ try {
12
+ const sessionFile = path.join(os.homedir(), ".agentrace", "current-session.json");
13
+ if (fs.existsSync(sessionFile)) {
14
+ const content = fs.readFileSync(sessionFile, "utf-8");
15
+ const data = JSON.parse(content);
16
+ return data.session_id;
17
+ }
18
+ }
19
+ catch {
20
+ // Ignore errors, return undefined
21
+ }
22
+ return undefined;
23
+ }
24
+ // Tool schemas
25
+ const ListPlansSchema = z.object({
26
+ git_remote_url: z.string().describe("Git remote URL to filter plans"),
27
+ });
28
+ const ReadPlanSchema = z.object({
29
+ id: z.string().describe("Plan document ID"),
30
+ });
31
+ const CreatePlanSchema = z.object({
32
+ description: z.string().describe("Short description of the plan"),
33
+ body: z.string().describe("Plan content in Markdown format"),
34
+ });
35
+ const UpdatePlanSchema = z.object({
36
+ id: z.string().describe("Plan document ID"),
37
+ body: z.string().describe("Updated plan content in Markdown format"),
38
+ });
39
+ const SetPlanStatusSchema = z.object({
40
+ id: z.string().describe("Plan document ID"),
41
+ status: z.enum(["scratch", "draft", "planning", "pending", "implementation", "complete"]).describe("New status for the plan"),
42
+ });
43
+ // Tool descriptions with usage guidance
44
+ const TOOL_DESCRIPTIONS = {
45
+ list_plans: `List plan documents for a repository.
46
+
47
+ WHEN TO USE:
48
+ - When you need to check existing plans for the current repository
49
+ - When the user asks about available plans or implementation documents
50
+ - Before creating a new plan to avoid duplicates`,
51
+ read_plan: `Read a plan document by ID.
52
+
53
+ WHEN TO USE:
54
+ - When the user asks you to check or review a specific plan by ID
55
+ - When you need to understand an existing plan before making changes
56
+ - When the user references a plan ID in their request`,
57
+ create_plan: `Create a new plan document to record implementation or design plans.
58
+
59
+ WHEN TO USE:
60
+ - ALWAYS use this when you create a design or implementation plan
61
+ - When entering plan mode and documenting your approach
62
+ - When the user asks you to save or persist a plan
63
+ - When planning significant features, refactoring, or architectural changes
64
+
65
+ The plan will be saved to Agentrace server and can be reviewed by the team.
66
+ The project is automatically determined from the session's git repository.`,
67
+ update_plan: `Update an existing plan document.
68
+
69
+ WHEN TO USE:
70
+ - When the user asks you to modify a specific plan by ID
71
+ - When implementation details change and the plan needs updating
72
+ - When you need to add progress notes or completion status to a plan
73
+
74
+ Changes are tracked with diff patches for history.`,
75
+ set_plan_status: `Set the status of a plan document.
76
+
77
+ WHEN TO USE:
78
+ - When transitioning a plan from planning to implementation phase
79
+ - When marking a plan as complete after finishing the work
80
+ - When the user explicitly asks to change the status of a plan
81
+
82
+ Available statuses:
83
+ - scratch: Initial rough notes, starting point for discussion with AI
84
+ - draft: Plan not yet fully considered (optional intermediate status)
85
+ - planning: Plan is being designed/refined through discussion
86
+ - pending: Waiting for approval or blocked
87
+ - implementation: Active development is in progress
88
+ - complete: The work described in the plan is finished
89
+
90
+ BASIC FLOW: scratch → planning → implementation → complete
91
+ (draft and pending are optional auxiliary statuses)
92
+
93
+ STATUS TRANSITION GUIDELINES:
94
+ - scratch → planning: When you read a scratch plan (usually written by human), review its content and rewrite it into a more concrete plan, then change status to planning
95
+ - planning → implementation: When the plan is finalized after discussion, change status to implementation before starting work
96
+ - implementation → complete: When all work described in the plan is finished, change status to complete
97
+
98
+ CAUTION:
99
+ - When a plan is in "implementation" status, someone else might already be working on it. Check with the team before starting work on such plans.`,
100
+ };
101
+ export async function mcpServerCommand() {
102
+ const server = new McpServer({
103
+ name: "agentrace",
104
+ version: "1.0.0",
105
+ description: `Agentrace Plan Document Management Server.
106
+
107
+ This server provides tools to manage implementation and design plans.
108
+ Plans are stored on the Agentrace server and can be reviewed by the team.
109
+
110
+ IMPORTANT GUIDELINES:
111
+ - When you create a design or implementation plan, ALWAYS save it using create_plan
112
+ - When the user asks you to check or modify a plan by ID, use the appropriate tool
113
+ - Plans help track what you're working on and enable team collaboration`,
114
+ });
115
+ let client = null;
116
+ function getClient() {
117
+ if (!client) {
118
+ client = new PlanDocumentClient();
119
+ }
120
+ return client;
121
+ }
122
+ // list_plans tool
123
+ server.tool("list_plans", TOOL_DESCRIPTIONS.list_plans, ListPlansSchema.shape, async (args) => {
124
+ try {
125
+ const plans = await getClient().listPlans(args.git_remote_url);
126
+ if (plans.length === 0) {
127
+ return {
128
+ content: [
129
+ {
130
+ type: "text",
131
+ text: "No plans found for this repository.",
132
+ },
133
+ ],
134
+ };
135
+ }
136
+ const planList = plans.map((plan) => ({
137
+ id: plan.id,
138
+ description: plan.description,
139
+ status: plan.status,
140
+ updated_at: plan.updated_at,
141
+ collaborators: plan.collaborators.map((c) => c.display_name).join(", "),
142
+ }));
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: JSON.stringify(planList, null, 2),
148
+ },
149
+ ],
150
+ };
151
+ }
152
+ catch (error) {
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
158
+ },
159
+ ],
160
+ isError: true,
161
+ };
162
+ }
163
+ });
164
+ // read_plan tool
165
+ server.tool("read_plan", TOOL_DESCRIPTIONS.read_plan, ReadPlanSchema.shape, async (args) => {
166
+ try {
167
+ const plan = await getClient().getPlan(args.id);
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: `# ${plan.description}\n\nStatus: ${plan.status}\n\n${plan.body}`,
173
+ },
174
+ ],
175
+ };
176
+ }
177
+ catch (error) {
178
+ return {
179
+ content: [
180
+ {
181
+ type: "text",
182
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
183
+ },
184
+ ],
185
+ isError: true,
186
+ };
187
+ }
188
+ });
189
+ // create_plan tool
190
+ server.tool("create_plan", TOOL_DESCRIPTIONS.create_plan, CreatePlanSchema.shape, async (args) => {
191
+ try {
192
+ // Read session_id from file written by PreToolUse hook
193
+ const claudeSessionId = getSessionIdFromFile();
194
+ const plan = await getClient().createPlan({
195
+ description: args.description,
196
+ body: args.body,
197
+ claude_session_id: claudeSessionId,
198
+ });
199
+ return {
200
+ content: [
201
+ {
202
+ type: "text",
203
+ text: `Plan created successfully.\n\nID: ${plan.id}\nDescription: ${plan.description}`,
204
+ },
205
+ ],
206
+ };
207
+ }
208
+ catch (error) {
209
+ return {
210
+ content: [
211
+ {
212
+ type: "text",
213
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
214
+ },
215
+ ],
216
+ isError: true,
217
+ };
218
+ }
219
+ });
220
+ // update_plan tool
221
+ server.tool("update_plan", TOOL_DESCRIPTIONS.update_plan, UpdatePlanSchema.shape, async (args) => {
222
+ try {
223
+ // Read session_id from file written by PreToolUse hook
224
+ const claudeSessionId = getSessionIdFromFile();
225
+ // Get current plan to compute patch
226
+ const currentPlan = await getClient().getPlan(args.id);
227
+ // Compute patch using diff-match-patch
228
+ const patches = patchMake(currentPlan.body, args.body);
229
+ const patchText = patchToText(patches);
230
+ const plan = await getClient().updatePlan(args.id, {
231
+ body: args.body,
232
+ patch: patchText,
233
+ claude_session_id: claudeSessionId,
234
+ });
235
+ return {
236
+ content: [
237
+ {
238
+ type: "text",
239
+ text: `Plan updated successfully.\n\nID: ${plan.id}\nDescription: ${plan.description}`,
240
+ },
241
+ ],
242
+ };
243
+ }
244
+ catch (error) {
245
+ return {
246
+ content: [
247
+ {
248
+ type: "text",
249
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
250
+ },
251
+ ],
252
+ isError: true,
253
+ };
254
+ }
255
+ });
256
+ // set_plan_status tool
257
+ server.tool("set_plan_status", TOOL_DESCRIPTIONS.set_plan_status, SetPlanStatusSchema.shape, async (args) => {
258
+ try {
259
+ const plan = await getClient().setStatus(args.id, args.status);
260
+ return {
261
+ content: [
262
+ {
263
+ type: "text",
264
+ text: `Plan status updated successfully.\n\nID: ${plan.id}\nDescription: ${plan.description}\nStatus: ${plan.status}`,
265
+ },
266
+ ],
267
+ };
268
+ }
269
+ catch (error) {
270
+ return {
271
+ content: [
272
+ {
273
+ type: "text",
274
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
275
+ },
276
+ ],
277
+ isError: true,
278
+ };
279
+ }
280
+ });
281
+ // Start the server with stdio transport
282
+ const transport = new StdioServerTransport();
283
+ await server.connect(transport);
284
+ }
@@ -1,4 +1,4 @@
1
- import { uninstallHooks } from "../hooks/installer.js";
1
+ import { uninstallHooks, uninstallMcpServer, uninstallPreToolUseHook } from "../hooks/installer.js";
2
2
  import { loadConfig } from "../config/manager.js";
3
3
  export async function offCommand() {
4
4
  // Check if config exists
@@ -15,4 +15,19 @@ export async function offCommand() {
15
15
  else {
16
16
  console.error(`✗ ${result.message}`);
17
17
  }
18
+ // Remove PreToolUse hook
19
+ const preToolUseResult = uninstallPreToolUseHook();
20
+ if (preToolUseResult.success) {
21
+ console.log(`✓ ${preToolUseResult.message}`);
22
+ }
23
+ else {
24
+ console.error(`✗ ${preToolUseResult.message}`);
25
+ }
26
+ const mcpResult = uninstallMcpServer();
27
+ if (mcpResult.success) {
28
+ console.log(`✓ ${mcpResult.message}`);
29
+ }
30
+ else {
31
+ console.error(`✗ ${mcpResult.message}`);
32
+ }
18
33
  }
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { installHooks } from "../hooks/installer.js";
3
+ import { installHooks, installMcpServer, installPreToolUseHook } from "../hooks/installer.js";
4
4
  import { loadConfig } from "../config/manager.js";
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
@@ -26,4 +26,28 @@ export async function onCommand(options = {}) {
26
26
  else {
27
27
  console.error(`✗ ${result.message}`);
28
28
  }
29
+ // Install MCP server
30
+ let mcpCommand;
31
+ let mcpArgs;
32
+ if (options.dev) {
33
+ const cliRoot = path.resolve(__dirname, "../..");
34
+ const indexPath = path.join(cliRoot, "src/index.ts");
35
+ mcpCommand = "npx";
36
+ mcpArgs = ["tsx", indexPath, "mcp-server"];
37
+ }
38
+ const mcpResult = installMcpServer({ command: mcpCommand, args: mcpArgs });
39
+ if (mcpResult.success) {
40
+ console.log(`✓ ${mcpResult.message}`);
41
+ }
42
+ else {
43
+ console.error(`✗ ${mcpResult.message}`);
44
+ }
45
+ // Install PreToolUse hook for session_id injection
46
+ const preToolUseResult = installPreToolUseHook();
47
+ if (preToolUseResult.success) {
48
+ console.log(`✓ ${preToolUseResult.message}`);
49
+ }
50
+ else {
51
+ console.error(`✗ ${preToolUseResult.message}`);
52
+ }
29
53
  }
@@ -63,6 +63,11 @@ export async function sendCommand() {
63
63
  console.error("[agentrace] Warning: Missing session_id or transcript_path");
64
64
  process.exit(0);
65
65
  }
66
+ // For UserPromptSubmit, wait for transcript to be written
67
+ // (Claude hasn't started processing yet, so transcript may not be updated)
68
+ if (data.hook_event_name === "UserPromptSubmit") {
69
+ await sleep(10000);
70
+ }
66
71
  // Get new lines from transcript
67
72
  const { lines, totalLineCount } = getNewLines(transcriptPath, sessionId);
68
73
  if (lines.length === 0) {
@@ -109,6 +114,9 @@ export async function sendCommand() {
109
114
  // Always exit 0 to not block hooks
110
115
  process.exit(0);
111
116
  }
117
+ function sleep(ms) {
118
+ return new Promise((resolve) => setTimeout(resolve, ms));
119
+ }
112
120
  function readStdin() {
113
121
  return new Promise((resolve, reject) => {
114
122
  let data = "";
@@ -1,5 +1,5 @@
1
1
  import { deleteConfig } from "../config/manager.js";
2
- import { uninstallHooks } from "../hooks/installer.js";
2
+ import { uninstallHooks, uninstallMcpServer, uninstallPreToolUseHook } from "../hooks/installer.js";
3
3
  export async function uninstallCommand() {
4
4
  console.log("Uninstalling Agentrace...\n");
5
5
  // Remove hooks
@@ -10,6 +10,22 @@ export async function uninstallCommand() {
10
10
  else {
11
11
  console.error(`✗ ${hookResult.message}`);
12
12
  }
13
+ // Remove PreToolUse hook
14
+ const preToolUseResult = uninstallPreToolUseHook();
15
+ if (preToolUseResult.success) {
16
+ console.log(`✓ ${preToolUseResult.message}`);
17
+ }
18
+ else {
19
+ console.error(`✗ ${preToolUseResult.message}`);
20
+ }
21
+ // Remove MCP server
22
+ const mcpResult = uninstallMcpServer();
23
+ if (mcpResult.success) {
24
+ console.log(`✓ ${mcpResult.message}`);
25
+ }
26
+ else {
27
+ console.error(`✗ ${mcpResult.message}`);
28
+ }
13
29
  // Remove config
14
30
  const configRemoved = deleteConfig();
15
31
  if (configRemoved) {
@@ -10,3 +10,25 @@ export declare function uninstallHooks(): {
10
10
  message: string;
11
11
  };
12
12
  export declare function checkHooksInstalled(): boolean;
13
+ export interface InstallMcpServerOptions {
14
+ command?: string;
15
+ args?: string[];
16
+ }
17
+ export declare function installMcpServer(options?: InstallMcpServerOptions): {
18
+ success: boolean;
19
+ message: string;
20
+ };
21
+ export declare function uninstallMcpServer(): {
22
+ success: boolean;
23
+ message: string;
24
+ };
25
+ export declare function checkMcpServerInstalled(): boolean;
26
+ export declare function installPreToolUseHook(): {
27
+ success: boolean;
28
+ message: string;
29
+ };
30
+ export declare function uninstallPreToolUseHook(): {
31
+ success: boolean;
32
+ message: string;
33
+ };
34
+ export declare function checkPreToolUseHookInstalled(): boolean;
@@ -2,6 +2,11 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import * as os from "node:os";
4
4
  const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
5
+ // MCP servers are configured in ~/.claude.json, NOT in settings.json
6
+ const CLAUDE_CONFIG_PATH = path.join(os.homedir(), ".claude.json");
7
+ // Agentrace hooks directory
8
+ const AGENTRACE_HOOKS_DIR = path.join(os.homedir(), ".agentrace", "hooks");
9
+ const SESSION_ID_HOOK_PATH = path.join(AGENTRACE_HOOKS_DIR, "inject-session-id.js");
5
10
  const DEFAULT_COMMAND = "npx agentrace send";
6
11
  function createAgentraceHook(command) {
7
12
  return {
@@ -27,17 +32,49 @@ export function installHooks(options = {}) {
27
32
  if (!settings.hooks) {
28
33
  settings.hooks = {};
29
34
  }
30
- // Add Stop hook only (transcript diff is sent on each Stop)
35
+ // Add Stop hook (transcript diff is sent on each Stop)
31
36
  if (!settings.hooks.Stop) {
32
37
  settings.hooks.Stop = [];
33
38
  }
39
+ // Add UserPromptSubmit hook (transcript is sent when user sends a message)
40
+ if (!settings.hooks.UserPromptSubmit) {
41
+ settings.hooks.UserPromptSubmit = [];
42
+ }
43
+ // Add SubagentStop hook (transcript is sent when a subagent task completes)
44
+ if (!settings.hooks.SubagentStop) {
45
+ settings.hooks.SubagentStop = [];
46
+ }
47
+ // Add PostToolUse hook (transcript is sent after each tool use for real-time updates)
48
+ if (!settings.hooks.PostToolUse) {
49
+ settings.hooks.PostToolUse = [];
50
+ }
34
51
  const hasStopHook = settings.hooks.Stop.some((matcher) => matcher.hooks?.some(isAgentraceHook));
35
- if (hasStopHook) {
52
+ const hasUserPromptSubmitHook = settings.hooks.UserPromptSubmit.some((matcher) => matcher.hooks?.some(isAgentraceHook));
53
+ const hasSubagentStopHook = settings.hooks.SubagentStop.some((matcher) => matcher.hooks?.some(isAgentraceHook));
54
+ const hasPostToolUseHook = settings.hooks.PostToolUse.some((matcher) => matcher.hooks?.some(isAgentraceHook));
55
+ if (hasStopHook && hasUserPromptSubmitHook && hasSubagentStopHook && hasPostToolUseHook) {
36
56
  return { success: true, message: "Hooks already installed (skipped)" };
37
57
  }
38
- settings.hooks.Stop.push({
39
- hooks: [agentraceHook],
40
- });
58
+ if (!hasStopHook) {
59
+ settings.hooks.Stop.push({
60
+ hooks: [agentraceHook],
61
+ });
62
+ }
63
+ if (!hasUserPromptSubmitHook) {
64
+ settings.hooks.UserPromptSubmit.push({
65
+ hooks: [agentraceHook],
66
+ });
67
+ }
68
+ if (!hasSubagentStopHook) {
69
+ settings.hooks.SubagentStop.push({
70
+ hooks: [agentraceHook],
71
+ });
72
+ }
73
+ if (!hasPostToolUseHook) {
74
+ settings.hooks.PostToolUse.push({
75
+ hooks: [agentraceHook],
76
+ });
77
+ }
41
78
  // Ensure directory exists
42
79
  const dir = path.dirname(CLAUDE_SETTINGS_PATH);
43
80
  if (!fs.existsSync(dir)) {
@@ -69,6 +106,27 @@ export function uninstallHooks() {
69
106
  delete settings.hooks.Stop;
70
107
  }
71
108
  }
109
+ // Remove agentrace hooks from UserPromptSubmit
110
+ if (settings.hooks.UserPromptSubmit) {
111
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter((matcher) => !matcher.hooks?.some(isAgentraceHook));
112
+ if (settings.hooks.UserPromptSubmit.length === 0) {
113
+ delete settings.hooks.UserPromptSubmit;
114
+ }
115
+ }
116
+ // Remove agentrace hooks from SubagentStop
117
+ if (settings.hooks.SubagentStop) {
118
+ settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter((matcher) => !matcher.hooks?.some(isAgentraceHook));
119
+ if (settings.hooks.SubagentStop.length === 0) {
120
+ delete settings.hooks.SubagentStop;
121
+ }
122
+ }
123
+ // Remove agentrace hooks from PostToolUse
124
+ if (settings.hooks.PostToolUse) {
125
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((matcher) => !matcher.hooks?.some(isAgentraceHook));
126
+ if (settings.hooks.PostToolUse.length === 0) {
127
+ delete settings.hooks.PostToolUse;
128
+ }
129
+ }
72
130
  // Clean up empty hooks object
73
131
  if (Object.keys(settings.hooks).length === 0) {
74
132
  delete settings.hooks;
@@ -92,7 +150,221 @@ export function checkHooksInstalled() {
92
150
  const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
93
151
  const settings = JSON.parse(content);
94
152
  const hasStopHook = settings.hooks?.Stop?.some((matcher) => matcher.hooks?.some(isAgentraceHook));
95
- return !!hasStopHook;
153
+ const hasUserPromptSubmitHook = settings.hooks?.UserPromptSubmit?.some((matcher) => matcher.hooks?.some(isAgentraceHook));
154
+ const hasSubagentStopHook = settings.hooks?.SubagentStop?.some((matcher) => matcher.hooks?.some(isAgentraceHook));
155
+ const hasPostToolUseHook = settings.hooks?.PostToolUse?.some((matcher) => matcher.hooks?.some(isAgentraceHook));
156
+ return !!hasStopHook && !!hasUserPromptSubmitHook && !!hasSubagentStopHook && !!hasPostToolUseHook;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ }
162
+ // MCP Server installer functions
163
+ const MCP_SERVER_NAME = "agentrace";
164
+ export function installMcpServer(options = {}) {
165
+ const command = options.command || "npx";
166
+ const args = options.args || ["agentrace", "mcp-server"];
167
+ try {
168
+ let config = {};
169
+ // Load existing config if file exists
170
+ // MCP servers are configured in ~/.claude.json (NOT settings.json)
171
+ if (fs.existsSync(CLAUDE_CONFIG_PATH)) {
172
+ const content = fs.readFileSync(CLAUDE_CONFIG_PATH, "utf-8");
173
+ config = JSON.parse(content);
174
+ }
175
+ // Initialize mcpServers structure if not present
176
+ if (!config.mcpServers) {
177
+ config.mcpServers = {};
178
+ }
179
+ // Check if already installed
180
+ if (config.mcpServers[MCP_SERVER_NAME]) {
181
+ // Update existing config
182
+ config.mcpServers[MCP_SERVER_NAME] = { command, args };
183
+ fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2));
184
+ return { success: true, message: "MCP server config updated" };
185
+ }
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
+ }
192
+ catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ return { success: false, message: `Failed to install MCP server: ${message}` };
195
+ }
196
+ }
197
+ export function uninstallMcpServer() {
198
+ try {
199
+ if (!fs.existsSync(CLAUDE_CONFIG_PATH)) {
200
+ return { success: true, message: "No config file found" };
201
+ }
202
+ const content = fs.readFileSync(CLAUDE_CONFIG_PATH, "utf-8");
203
+ const config = JSON.parse(content);
204
+ if (!config.mcpServers || !config.mcpServers[MCP_SERVER_NAME]) {
205
+ return { success: true, message: "MCP server not configured" };
206
+ }
207
+ // Remove agentrace MCP server
208
+ delete config.mcpServers[MCP_SERVER_NAME];
209
+ // Clean up empty mcpServers object
210
+ if (Object.keys(config.mcpServers).length === 0) {
211
+ delete config.mcpServers;
212
+ }
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
+ }
219
+ catch (error) {
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ return { success: false, message: `Failed to uninstall MCP server: ${message}` };
222
+ }
223
+ }
224
+ export function checkMcpServerInstalled() {
225
+ try {
226
+ if (!fs.existsSync(CLAUDE_CONFIG_PATH)) {
227
+ return false;
228
+ }
229
+ const content = fs.readFileSync(CLAUDE_CONFIG_PATH, "utf-8");
230
+ const config = JSON.parse(content);
231
+ return !!config.mcpServers?.[MCP_SERVER_NAME];
232
+ }
233
+ catch {
234
+ return false;
235
+ }
236
+ }
237
+ // PreToolUse hook for injecting session_id into agentrace MCP tools
238
+ const SESSION_ID_HOOK_SCRIPT = `#!/usr/bin/env node
239
+ // Agentrace PreToolUse hook: Writes session_id to file for MCP tools
240
+ // This hook is called before agentrace MCP tools (create_plan, update_plan)
241
+
242
+ const fs = require('fs');
243
+ const os = require('os');
244
+ const path = require('path');
245
+
246
+ const sessionFile = path.join(os.homedir(), '.agentrace', 'current-session.json');
247
+
248
+ let input = '';
249
+ process.stdin.setEncoding('utf8');
250
+ process.stdin.on('data', chunk => { input += chunk; });
251
+ process.stdin.on('end', () => {
252
+ try {
253
+ const data = JSON.parse(input);
254
+ const sessionId = data.session_id;
255
+
256
+ // Write session_id to file for MCP server to read
257
+ fs.writeFileSync(sessionFile, JSON.stringify({ session_id: sessionId }));
258
+
259
+ // Allow the tool to proceed
260
+ const output = {
261
+ hookSpecificOutput: {
262
+ hookEventName: "PreToolUse",
263
+ permissionDecision: "allow"
264
+ }
265
+ };
266
+ console.log(JSON.stringify(output));
267
+ } catch (e) {
268
+ process.stderr.write('Error: ' + e.message);
269
+ process.exit(1);
270
+ }
271
+ });
272
+ `;
273
+ const AGENTRACE_MCP_TOOLS_MATCHER = "mcp__agentrace__create_plan|mcp__agentrace__update_plan";
274
+ function isAgentracePreToolUseHook(matcher) {
275
+ return matcher.matcher === AGENTRACE_MCP_TOOLS_MATCHER &&
276
+ matcher.hooks?.some(h => h.command?.includes("inject-session-id"));
277
+ }
278
+ export function installPreToolUseHook() {
279
+ try {
280
+ // Create hooks directory if not exists
281
+ if (!fs.existsSync(AGENTRACE_HOOKS_DIR)) {
282
+ fs.mkdirSync(AGENTRACE_HOOKS_DIR, { recursive: true });
283
+ }
284
+ // Write hook script
285
+ fs.writeFileSync(SESSION_ID_HOOK_PATH, SESSION_ID_HOOK_SCRIPT, { mode: 0o755 });
286
+ // Load existing settings
287
+ let settings = {};
288
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
289
+ const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
290
+ settings = JSON.parse(content);
291
+ }
292
+ // Initialize hooks structure if not present
293
+ if (!settings.hooks) {
294
+ settings.hooks = {};
295
+ }
296
+ if (!settings.hooks.PreToolUse) {
297
+ settings.hooks.PreToolUse = [];
298
+ }
299
+ // Check if already installed
300
+ const hasPreToolUseHook = settings.hooks.PreToolUse.some(isAgentracePreToolUseHook);
301
+ if (hasPreToolUseHook) {
302
+ return { success: true, message: "PreToolUse hook already installed (skipped)" };
303
+ }
304
+ // Add PreToolUse hook
305
+ settings.hooks.PreToolUse.push({
306
+ matcher: AGENTRACE_MCP_TOOLS_MATCHER,
307
+ hooks: [
308
+ {
309
+ type: "command",
310
+ command: SESSION_ID_HOOK_PATH,
311
+ },
312
+ ],
313
+ });
314
+ // Ensure directory exists
315
+ const dir = path.dirname(CLAUDE_SETTINGS_PATH);
316
+ if (!fs.existsSync(dir)) {
317
+ fs.mkdirSync(dir, { recursive: true });
318
+ }
319
+ // Write settings
320
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
321
+ return { success: true, message: `PreToolUse hook installed to ${SESSION_ID_HOOK_PATH}` };
322
+ }
323
+ catch (error) {
324
+ const message = error instanceof Error ? error.message : String(error);
325
+ return { success: false, message: `Failed to install PreToolUse hook: ${message}` };
326
+ }
327
+ }
328
+ export function uninstallPreToolUseHook() {
329
+ try {
330
+ // Remove hook script
331
+ if (fs.existsSync(SESSION_ID_HOOK_PATH)) {
332
+ fs.unlinkSync(SESSION_ID_HOOK_PATH);
333
+ }
334
+ // Remove from settings
335
+ if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
336
+ return { success: true, message: "No settings file found" };
337
+ }
338
+ const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
339
+ const settings = JSON.parse(content);
340
+ if (!settings.hooks?.PreToolUse) {
341
+ return { success: true, message: "No PreToolUse hooks configured" };
342
+ }
343
+ // Remove agentrace PreToolUse hooks
344
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((matcher) => !isAgentracePreToolUseHook(matcher));
345
+ if (settings.hooks.PreToolUse.length === 0) {
346
+ delete settings.hooks.PreToolUse;
347
+ }
348
+ // Clean up empty hooks object
349
+ if (Object.keys(settings.hooks).length === 0) {
350
+ delete settings.hooks;
351
+ }
352
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
353
+ return { success: true, message: "PreToolUse hook removed" };
354
+ }
355
+ catch (error) {
356
+ const message = error instanceof Error ? error.message : String(error);
357
+ return { success: false, message: `Failed to uninstall PreToolUse hook: ${message}` };
358
+ }
359
+ }
360
+ export function checkPreToolUseHookInstalled() {
361
+ try {
362
+ if (!fs.existsSync(CLAUDE_SETTINGS_PATH) || !fs.existsSync(SESSION_ID_HOOK_PATH)) {
363
+ return false;
364
+ }
365
+ const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
366
+ const settings = JSON.parse(content);
367
+ return settings.hooks?.PreToolUse?.some(isAgentracePreToolUseHook) ?? false;
96
368
  }
97
369
  catch {
98
370
  return false;
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { sendCommand } from "./commands/send.js";
6
6
  import { uninstallCommand } from "./commands/uninstall.js";
7
7
  import { onCommand } from "./commands/on.js";
8
8
  import { offCommand } from "./commands/off.js";
9
+ import { mcpServerCommand } from "./commands/mcp-server.js";
9
10
  const program = new Command();
10
11
  program.name("agentrace").description("CLI for Agentrace").version("0.1.0");
11
12
  program
@@ -47,4 +48,10 @@ program
47
48
  .action(async () => {
48
49
  await offCommand();
49
50
  });
51
+ program
52
+ .command("mcp-server")
53
+ .description("Run MCP server for Claude Code integration (stdio)")
54
+ .action(async () => {
55
+ await mcpServerCommand();
56
+ });
50
57
  program.parse();
@@ -0,0 +1,57 @@
1
+ export type PlanDocumentStatus = "scratch" | "draft" | "planning" | "pending" | "implementation" | "complete";
2
+ export interface Project {
3
+ id: string;
4
+ canonical_git_repository: string;
5
+ }
6
+ export interface PlanDocument {
7
+ id: string;
8
+ description: string;
9
+ body: string;
10
+ project: Project | null;
11
+ status: PlanDocumentStatus;
12
+ collaborators: {
13
+ id: string;
14
+ display_name: string;
15
+ }[];
16
+ created_at: string;
17
+ updated_at: string;
18
+ }
19
+ export interface PlanDocumentEvent {
20
+ id: string;
21
+ plan_document_id: string;
22
+ claude_session_id: string | null;
23
+ user_id: string | null;
24
+ user_name: string | null;
25
+ patch: string;
26
+ created_at: string;
27
+ }
28
+ export interface ListPlansResponse {
29
+ plans: PlanDocument[];
30
+ }
31
+ export interface ListEventsResponse {
32
+ events: PlanDocumentEvent[];
33
+ }
34
+ export interface CreatePlanRequest {
35
+ description: string;
36
+ body: string;
37
+ claude_session_id?: string;
38
+ }
39
+ export interface UpdatePlanRequest {
40
+ description?: string;
41
+ body?: string;
42
+ patch?: string;
43
+ claude_session_id?: string;
44
+ }
45
+ export declare class PlanDocumentClient {
46
+ private serverUrl;
47
+ private apiKey;
48
+ constructor();
49
+ private request;
50
+ listPlans(gitRemoteUrl?: string): Promise<PlanDocument[]>;
51
+ getPlan(id: string): Promise<PlanDocument>;
52
+ getPlanEvents(id: string): Promise<PlanDocumentEvent[]>;
53
+ createPlan(req: CreatePlanRequest): Promise<PlanDocument>;
54
+ updatePlan(id: string, req: UpdatePlanRequest): Promise<PlanDocument>;
55
+ deletePlan(id: string): Promise<void>;
56
+ setStatus(id: string, status: PlanDocumentStatus): Promise<PlanDocument>;
57
+ }
@@ -0,0 +1,61 @@
1
+ import { loadConfig } from "../config/manager.js";
2
+ export class PlanDocumentClient {
3
+ serverUrl;
4
+ apiKey;
5
+ constructor() {
6
+ const config = loadConfig();
7
+ if (!config) {
8
+ throw new Error("Agentrace is not configured. Run 'npx agentrace init' first.");
9
+ }
10
+ this.serverUrl = config.server_url;
11
+ this.apiKey = config.api_key;
12
+ }
13
+ async request(method, path, body) {
14
+ const url = `${this.serverUrl}${path}`;
15
+ const headers = {
16
+ "Authorization": `Bearer ${this.apiKey}`,
17
+ "Content-Type": "application/json",
18
+ };
19
+ const response = await fetch(url, {
20
+ method,
21
+ headers,
22
+ body: body ? JSON.stringify(body) : undefined,
23
+ });
24
+ if (!response.ok) {
25
+ const errorText = await response.text();
26
+ throw new Error(`API request failed: ${response.status} ${errorText}`);
27
+ }
28
+ // Handle 204 No Content
29
+ if (response.status === 204) {
30
+ return undefined;
31
+ }
32
+ return response.json();
33
+ }
34
+ async listPlans(gitRemoteUrl) {
35
+ let path = "/api/plans";
36
+ if (gitRemoteUrl) {
37
+ path += `?git_remote_url=${encodeURIComponent(gitRemoteUrl)}`;
38
+ }
39
+ const response = await this.request("GET", path);
40
+ return response.plans;
41
+ }
42
+ async getPlan(id) {
43
+ return this.request("GET", `/api/plans/${id}`);
44
+ }
45
+ async getPlanEvents(id) {
46
+ const response = await this.request("GET", `/api/plans/${id}/events`);
47
+ return response.events;
48
+ }
49
+ async createPlan(req) {
50
+ return this.request("POST", "/api/plans", req);
51
+ }
52
+ async updatePlan(id, req) {
53
+ return this.request("PATCH", `/api/plans/${id}`, req);
54
+ }
55
+ async deletePlan(id) {
56
+ await this.request("DELETE", `/api/plans/${id}`);
57
+ }
58
+ async setStatus(id, status) {
59
+ return this.request("PATCH", `/api/plans/${id}/status`, { status });
60
+ }
61
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentrace",
3
- "version": "0.0.1",
4
- "description": "CLI for Agentrace - Claude Code session tracker",
3
+ "version": "0.0.3",
4
+ "description": "CLI for AgenTrace - Claude Code session tracker",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agentrace": "./dist/index.js"
@@ -35,7 +35,10 @@
35
35
  "url": "https://github.com/satetsu888/agentrace/issues"
36
36
  },
37
37
  "dependencies": {
38
- "commander": "^12.0.0"
38
+ "@modelcontextprotocol/sdk": "^1.0.0",
39
+ "commander": "^12.0.0",
40
+ "diff-match-patch-es": "^1.0.0",
41
+ "zod": "^3.25.0"
39
42
  },
40
43
  "devDependencies": {
41
44
  "@types/node": "^20.0.0",