claudetracer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/bin/cli.js +5 -0
- package/bin/cli.ts +12 -0
- package/bin/dashboard.ts +16 -0
- package/bin/init.ts +56 -0
- package/package.json +24 -0
- package/src/app/page.tsx +263 -0
- package/src/diff/index.ts +147 -0
- package/src/injector/index.ts +177 -0
- package/src/lib/supabase.ts +6 -0
- package/src/recorder/index.ts +87 -0
- package/src/server/index.ts +229 -0
- package/test.txt +1 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Tracer — Agent Replay & Diff Memory
|
|
2
|
+
|
|
3
|
+
Tracer is a missing infrastructure layer for AI coding agents.
|
|
4
|
+
|
|
5
|
+
Agents repeat the same mistakes across sessions because they have no memory
|
|
6
|
+
of what they tried, what failed, and what the correct action was.
|
|
7
|
+
Tracer closes that loop.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
1. **Records** every action an agent takes during a session (reads, writes, commands)
|
|
12
|
+
2. **Diffs** what the agent wrote against what the developer actually committed to git
|
|
13
|
+
3. **Surfaces** deviations in a dashboard where developers annotate corrections
|
|
14
|
+
4. **Injects** those corrections as context before the next session starts
|
|
15
|
+
|
|
16
|
+
The agent never makes the same mistake twice.
|
|
17
|
+
|
|
18
|
+
## Stack
|
|
19
|
+
|
|
20
|
+
- MCP server (Node.js + TypeScript) — intercepts Claude Code tool calls
|
|
21
|
+
- Supabase — stores sessions, diffs, corrections
|
|
22
|
+
- Next.js dashboard — view deviations, publish corrections
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
### 1. Install dependencies
|
|
27
|
+
cd agent-tracer && npm install
|
|
28
|
+
|
|
29
|
+
### 2. Add Supabase credentials to .env
|
|
30
|
+
SUPABASE_URL=your_url
|
|
31
|
+
SUPABASE_ANON_KEY=your_key
|
|
32
|
+
|
|
33
|
+
### 3. Register as MCP server in ~/.claude.json
|
|
34
|
+
Add tracer to your mcpServers config pointing to src/server/index.ts
|
|
35
|
+
|
|
36
|
+
### 4. Run the dashboard
|
|
37
|
+
cd tracer-dashboard && npm run dev
|
|
38
|
+
|
|
39
|
+
## MCP Tools
|
|
40
|
+
|
|
41
|
+
| Tool | What it does |
|
|
42
|
+
|------|-------------|
|
|
43
|
+
| `get_context` | Returns past corrections before a session starts |
|
|
44
|
+
| `read_file` | Reads a file and logs the action |
|
|
45
|
+
| `write_file` | Writes a file and logs the action |
|
|
46
|
+
| `run_command` | Runs a shell command and logs the action |
|
|
47
|
+
| `close_session` | Computes git diff and saves deviations to Supabase |
|
|
48
|
+
|
|
49
|
+
## Dashboard
|
|
50
|
+
|
|
51
|
+
Open http://localhost:3000 to:
|
|
52
|
+
- See all recorded sessions
|
|
53
|
+
- View deviations (what the agent wrote vs what was committed)
|
|
54
|
+
- Publish corrections that get injected into the next session
|
|
55
|
+
|
|
56
|
+
## Why this matters
|
|
57
|
+
|
|
58
|
+
Nia by Nozomio solves external context — giving agents the right docs to read.
|
|
59
|
+
Tracer solves execution memory — giving agents the right corrections from past runs.
|
|
60
|
+
|
|
61
|
+
They are complementary layers. Nia tells agents what to read.
|
|
62
|
+
Tracer tells agents what not to do.
|
|
63
|
+
|
|
64
|
+
## Built by
|
|
65
|
+
|
|
66
|
+
Nursultan Orynbassar
|
package/bin/cli.js
ADDED
package/bin/cli.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const command = process.argv[2];
|
|
3
|
+
|
|
4
|
+
if (command === "init") {
|
|
5
|
+
require("./init");
|
|
6
|
+
} else if (command === "dashboard") {
|
|
7
|
+
require("./dashboard");
|
|
8
|
+
} else {
|
|
9
|
+
console.log("\nUsage:");
|
|
10
|
+
console.log(" npx tracerit init — Set up Tracerit with Claude Code");
|
|
11
|
+
console.log(" npx tracerit dashboard — Open the Tracerit dashboard\n");
|
|
12
|
+
}
|
package/bin/dashboard.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
const dashboardPath = path.join(__dirname, "../tracer-dashboard");
|
|
6
|
+
|
|
7
|
+
console.log("\n🚀 Starting Tracerit Dashboard...\n");
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
execSync("npm run dev", {
|
|
11
|
+
cwd: dashboardPath,
|
|
12
|
+
stdio: "inherit",
|
|
13
|
+
});
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error("Dashboard failed to start. Make sure tracer-dashboard is set up.");
|
|
16
|
+
}
|
package/bin/init.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as readline from "readline";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const ask = (question: string): Promise<string> =>
|
|
13
|
+
new Promise((resolve) => rl.question(question, resolve));
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
console.log("\n🔍 Tracerit — Agent Memory for Claude Code\n");
|
|
17
|
+
|
|
18
|
+
const supabaseUrl = await ask("Enter your Supabase URL: ");
|
|
19
|
+
const supabaseKey = await ask("Enter your Supabase Anon Key: ");
|
|
20
|
+
|
|
21
|
+
// Write .env file
|
|
22
|
+
const envPath = path.join(__dirname, "../.env");
|
|
23
|
+
fs.writeFileSync(envPath, `SUPABASE_URL=${supabaseUrl}\nSUPABASE_ANON_KEY=${supabaseKey}\n`);
|
|
24
|
+
console.log("✅ .env file created");
|
|
25
|
+
|
|
26
|
+
// Update ~/.claude.json
|
|
27
|
+
const claudeConfigPath = path.join(os.homedir(), ".claude.json");
|
|
28
|
+
let claudeConfig: any = {};
|
|
29
|
+
|
|
30
|
+
if (fs.existsSync(claudeConfigPath)) {
|
|
31
|
+
claudeConfig = JSON.parse(fs.readFileSync(claudeConfigPath, "utf-8"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!claudeConfig.mcpServers) claudeConfig.mcpServers = {};
|
|
35
|
+
|
|
36
|
+
const serverPath = path.join(__dirname, "../src/server/index.ts");
|
|
37
|
+
|
|
38
|
+
claudeConfig.mcpServers.tracer = {
|
|
39
|
+
command: "npx",
|
|
40
|
+
args: ["tsx", serverPath],
|
|
41
|
+
env: {
|
|
42
|
+
SUPABASE_URL: supabaseUrl,
|
|
43
|
+
SUPABASE_ANON_KEY: supabaseKey,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2));
|
|
48
|
+
console.log("✅ Claude Code configured");
|
|
49
|
+
|
|
50
|
+
console.log("\n✅ Tracerit is ready! Open Claude Code and start coding.");
|
|
51
|
+
console.log("👉 Run 'npx tracerit dashboard' to view your sessions.\n");
|
|
52
|
+
|
|
53
|
+
rl.close();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claudetracer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Agent replay and memory for Claude Code",
|
|
5
|
+
"bin": {
|
|
6
|
+
"tracerit": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"start": "tsx src/server/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
14
|
+
"@supabase/supabase-js": "^2.0.0",
|
|
15
|
+
"dotenv": "^16.0.0",
|
|
16
|
+
"uuid": "^9.0.0",
|
|
17
|
+
"zod": "^3.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^20.0.0",
|
|
21
|
+
"tsx": "^4.0.0",
|
|
22
|
+
"typescript": "^5.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { supabase } from "@/lib/supabase";
|
|
5
|
+
|
|
6
|
+
// ── TYPES ──
|
|
7
|
+
interface Session {
|
|
8
|
+
id: string;
|
|
9
|
+
project_path: string;
|
|
10
|
+
ended_at: string;
|
|
11
|
+
created_at: string;
|
|
12
|
+
deviation_count?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Diff {
|
|
16
|
+
id: number;
|
|
17
|
+
session_id: string;
|
|
18
|
+
file_path: string;
|
|
19
|
+
agent_wrote: string;
|
|
20
|
+
final_version: string;
|
|
21
|
+
was_reverted: boolean;
|
|
22
|
+
lines_added: number;
|
|
23
|
+
lines_removed: number;
|
|
24
|
+
computed_at: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Correction {
|
|
28
|
+
id: number;
|
|
29
|
+
session_id: string;
|
|
30
|
+
file_path: string;
|
|
31
|
+
note: string;
|
|
32
|
+
agent_wrote: string;
|
|
33
|
+
correct_version: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function Home() {
|
|
37
|
+
const [sessions, setSessions] = useState<Session[]>([]);
|
|
38
|
+
const [selectedSession, setSelectedSession] = useState<Session | null>(null);
|
|
39
|
+
const [diffs, setDiffs] = useState<Diff[]>([]);
|
|
40
|
+
const [selectedDiff, setSelectedDiff] = useState<Diff | null>(null);
|
|
41
|
+
const [note, setNote] = useState("");
|
|
42
|
+
const [correctVersion, setCorrectVersion] = useState("");
|
|
43
|
+
const [saving, setSaving] = useState(false);
|
|
44
|
+
const [saved, setSaved] = useState(false);
|
|
45
|
+
const [loading, setLoading] = useState(true);
|
|
46
|
+
|
|
47
|
+
// ── LOAD SESSIONS ──
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
async function loadSessions() {
|
|
50
|
+
const { data } = await supabase
|
|
51
|
+
.from("sessions")
|
|
52
|
+
.select("*")
|
|
53
|
+
.order("created_at", { ascending: false })
|
|
54
|
+
.limit(20);
|
|
55
|
+
setSessions(data ?? []);
|
|
56
|
+
setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
loadSessions();
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
// ── LOAD DIFFS FOR SELECTED SESSION ──
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!selectedSession) return;
|
|
64
|
+
async function loadDiffs() {
|
|
65
|
+
const { data } = await supabase
|
|
66
|
+
.from("diffs")
|
|
67
|
+
.select("*")
|
|
68
|
+
.eq("session_id", selectedSession!.id)
|
|
69
|
+
.order("computed_at", { ascending: false });
|
|
70
|
+
setDiffs(data ?? []);
|
|
71
|
+
setSelectedDiff(null);
|
|
72
|
+
}
|
|
73
|
+
loadDiffs();
|
|
74
|
+
}, [selectedSession]);
|
|
75
|
+
|
|
76
|
+
// ── PUBLISH CORRECTION ──
|
|
77
|
+
async function publishCorrection() {
|
|
78
|
+
if (!selectedDiff || !note) return;
|
|
79
|
+
setSaving(true);
|
|
80
|
+
|
|
81
|
+
await supabase.from("corrections").insert({
|
|
82
|
+
session_id: selectedDiff.session_id,
|
|
83
|
+
action_id: null,
|
|
84
|
+
file_path: selectedDiff.file_path,
|
|
85
|
+
agent_wrote: selectedDiff.agent_wrote,
|
|
86
|
+
correct_version: correctVersion || selectedDiff.final_version,
|
|
87
|
+
note,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
setSaving(false);
|
|
91
|
+
setSaved(true);
|
|
92
|
+
setNote("");
|
|
93
|
+
setCorrectVersion("");
|
|
94
|
+
setTimeout(() => setSaved(false), 2000);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="min-h-screen bg-gray-50 text-gray-900 font-sans">
|
|
99
|
+
{/* Header */}
|
|
100
|
+
<div className="border-b border-gray-200 bg-white px-6 py-4 flex items-center justify-between">
|
|
101
|
+
<div className="flex items-center gap-3">
|
|
102
|
+
<div className="w-7 h-7 rounded-md bg-teal-500 flex items-center justify-center">
|
|
103
|
+
<span className="text-white text-xs font-bold">T</span>
|
|
104
|
+
</div>
|
|
105
|
+
<span className="font-semibold text-lg">Tracer</span>
|
|
106
|
+
<span className="text-gray-400 text-sm">Agent Replay & Diff Memory</span>
|
|
107
|
+
</div>
|
|
108
|
+
<span className="text-xs text-gray-400">{sessions.length} sessions recorded</span>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div className="flex h-[calc(100vh-57px)]">
|
|
112
|
+
|
|
113
|
+
{/* ── LEFT: Sessions list ── */}
|
|
114
|
+
<div className="w-64 border-r border-gray-200 bg-white overflow-y-auto">
|
|
115
|
+
<div className="px-4 py-3 border-b border-gray-100">
|
|
116
|
+
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide">Sessions</p>
|
|
117
|
+
</div>
|
|
118
|
+
{loading && (
|
|
119
|
+
<div className="px-4 py-8 text-sm text-gray-400 text-center">Loading...</div>
|
|
120
|
+
)}
|
|
121
|
+
{!loading && sessions.length === 0 && (
|
|
122
|
+
<div className="px-4 py-8 text-sm text-gray-400 text-center">
|
|
123
|
+
No sessions yet. Run the MCP server and start coding.
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
{sessions.map((s) => (
|
|
127
|
+
<button
|
|
128
|
+
key={s.id}
|
|
129
|
+
onClick={() => setSelectedSession(s)}
|
|
130
|
+
className={`w-full text-left px-4 py-3 border-b border-gray-100 hover:bg-gray-50 transition-colors ${
|
|
131
|
+
selectedSession?.id === s.id ? "bg-teal-50 border-l-2 border-l-teal-500" : ""
|
|
132
|
+
}`}
|
|
133
|
+
>
|
|
134
|
+
<p className="text-xs font-mono text-gray-500 truncate">{s.id.slice(0, 8)}...</p>
|
|
135
|
+
<p className="text-xs text-gray-400 mt-0.5 truncate">
|
|
136
|
+
{s.project_path?.split("/").pop() ?? "unknown project"}
|
|
137
|
+
</p>
|
|
138
|
+
<p className="text-xs text-gray-300 mt-0.5">
|
|
139
|
+
{new Date(s.created_at).toLocaleDateString()}
|
|
140
|
+
</p>
|
|
141
|
+
</button>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* ── MIDDLE: Diffs list ── */}
|
|
146
|
+
<div className="w-72 border-r border-gray-200 bg-white overflow-y-auto">
|
|
147
|
+
<div className="px-4 py-3 border-b border-gray-100">
|
|
148
|
+
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
|
149
|
+
{selectedSession ? `Deviations (${diffs.length})` : "Select a session"}
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
{selectedSession && diffs.length === 0 && (
|
|
153
|
+
<div className="px-4 py-8 text-sm text-gray-400 text-center">
|
|
154
|
+
No deviations found. Agent matched final code.
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
{diffs.map((d) => (
|
|
158
|
+
<button
|
|
159
|
+
key={d.id}
|
|
160
|
+
onClick={() => setSelectedDiff(d)}
|
|
161
|
+
className={`w-full text-left px-4 py-3 border-b border-gray-100 hover:bg-gray-50 transition-colors ${
|
|
162
|
+
selectedDiff?.id === d.id ? "bg-teal-50 border-l-2 border-l-teal-500" : ""
|
|
163
|
+
}`}
|
|
164
|
+
>
|
|
165
|
+
<div className="flex items-center gap-2 mb-1">
|
|
166
|
+
{d.was_reverted ? (
|
|
167
|
+
<span className="text-xs bg-red-100 text-red-600 px-1.5 py-0.5 rounded">reverted</span>
|
|
168
|
+
) : (
|
|
169
|
+
<span className="text-xs bg-yellow-100 text-yellow-600 px-1.5 py-0.5 rounded">modified</span>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
<p className="text-xs font-mono text-gray-600 truncate">{d.file_path.split("/").pop()}</p>
|
|
173
|
+
<p className="text-xs text-gray-400 mt-0.5 truncate">{d.file_path}</p>
|
|
174
|
+
<p className="text-xs text-gray-300 mt-1">
|
|
175
|
+
+{d.lines_added} / -{d.lines_removed} lines
|
|
176
|
+
</p>
|
|
177
|
+
</button>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* ── RIGHT: Diff detail + correction form ── */}
|
|
182
|
+
<div className="flex-1 overflow-y-auto bg-gray-50">
|
|
183
|
+
{!selectedDiff && (
|
|
184
|
+
<div className="flex items-center justify-center h-full text-sm text-gray-400">
|
|
185
|
+
Select a deviation to annotate it
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
{selectedDiff && (
|
|
189
|
+
<div className="p-6 space-y-6">
|
|
190
|
+
{/* File path */}
|
|
191
|
+
<div>
|
|
192
|
+
<p className="text-xs text-gray-400 mb-1">File</p>
|
|
193
|
+
<p className="font-mono text-sm text-gray-700">{selectedDiff.file_path}</p>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Side by side diff */}
|
|
197
|
+
<div className="grid grid-cols-2 gap-4">
|
|
198
|
+
<div>
|
|
199
|
+
<p className="text-xs text-gray-400 mb-2">Agent wrote</p>
|
|
200
|
+
<pre className="bg-red-50 border border-red-100 rounded-lg p-3 text-xs font-mono text-red-800 overflow-x-auto whitespace-pre-wrap">
|
|
201
|
+
{selectedDiff.agent_wrote?.slice(0, 800) ?? "—"}
|
|
202
|
+
</pre>
|
|
203
|
+
</div>
|
|
204
|
+
<div>
|
|
205
|
+
<p className="text-xs text-gray-400 mb-2">Final committed version</p>
|
|
206
|
+
<pre className="bg-green-50 border border-green-100 rounded-lg p-3 text-xs font-mono text-green-800 overflow-x-auto whitespace-pre-wrap">
|
|
207
|
+
{selectedDiff.final_version?.slice(0, 800) ?? "File was deleted"}
|
|
208
|
+
</pre>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Correction form */}
|
|
213
|
+
<div className="bg-white border border-gray-200 rounded-xl p-5 space-y-4">
|
|
214
|
+
<p className="text-sm font-medium text-gray-700">Publish a correction</p>
|
|
215
|
+
<p className="text-xs text-gray-400">
|
|
216
|
+
This note will be injected into the agent's context before the next session.
|
|
217
|
+
</p>
|
|
218
|
+
|
|
219
|
+
<div>
|
|
220
|
+
<label className="text-xs text-gray-500 mb-1 block">
|
|
221
|
+
What should the agent know? <span className="text-red-400">*</span>
|
|
222
|
+
</label>
|
|
223
|
+
<textarea
|
|
224
|
+
value={note}
|
|
225
|
+
onChange={(e) => setNote(e.target.value)}
|
|
226
|
+
placeholder="e.g. Don't use db.query() here — this project uses db.run() with async/await"
|
|
227
|
+
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-teal-400"
|
|
228
|
+
rows={3}
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div>
|
|
233
|
+
<label className="text-xs text-gray-500 mb-1 block">
|
|
234
|
+
Correct version (optional — defaults to committed version)
|
|
235
|
+
</label>
|
|
236
|
+
<textarea
|
|
237
|
+
value={correctVersion}
|
|
238
|
+
onChange={(e) => setCorrectVersion(e.target.value)}
|
|
239
|
+
placeholder="Paste the correct code snippet..."
|
|
240
|
+
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm font-mono resize-none focus:outline-none focus:ring-1 focus:ring-teal-400"
|
|
241
|
+
rows={4}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<button
|
|
246
|
+
onClick={publishCorrection}
|
|
247
|
+
disabled={!note || saving}
|
|
248
|
+
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
249
|
+
saved
|
|
250
|
+
? "bg-green-500 text-white"
|
|
251
|
+
: "bg-teal-500 text-white hover:bg-teal-600 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
252
|
+
}`}
|
|
253
|
+
>
|
|
254
|
+
{saving ? "Saving..." : saved ? "Correction published ✓" : "Publish correction"}
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { createClient } from "@supabase/supabase-js";
|
|
5
|
+
import * as dotenv from "dotenv";
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
// ── TYPES ──
|
|
10
|
+
export interface FileDiff {
|
|
11
|
+
filePath: string;
|
|
12
|
+
agentWrote: string;
|
|
13
|
+
finalVersion: string;
|
|
14
|
+
wasReverted: boolean;
|
|
15
|
+
linesAdded: number;
|
|
16
|
+
linesRemoved: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SessionDiff {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
projectPath: string;
|
|
22
|
+
diffs: FileDiff[];
|
|
23
|
+
computedAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── HELPERS ──
|
|
27
|
+
// Get the list of files the agent touched in this session
|
|
28
|
+
// We read from the local .tracer/actions.jsonl file
|
|
29
|
+
function getAgentTouchedFiles(sessionId: string): Map<string, string> {
|
|
30
|
+
const logFile = process.env.TRACER_LOG_DIR
|
|
31
|
+
? join(process.env.TRACER_LOG_DIR, "actions.jsonl")
|
|
32
|
+
: join(process.env.HOME || "", ".tracer", "actions.jsonl");
|
|
33
|
+
if (!existsSync(logFile)) return new Map();
|
|
34
|
+
|
|
35
|
+
const lines = readFileSync(logFile, "utf-8")
|
|
36
|
+
.split("\n")
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.map((l) => JSON.parse(l));
|
|
39
|
+
|
|
40
|
+
const fileMap = new Map<string, string>();
|
|
41
|
+
|
|
42
|
+
for (const action of lines) {
|
|
43
|
+
// Only care about write_file actions from this session
|
|
44
|
+
if (action.sessionId === sessionId && action.tool === "write_file") {
|
|
45
|
+
fileMap.set(action.input.path, action.input.content ?? "");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return fileMap;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Count how many lines differ between two strings
|
|
53
|
+
function countLineDiff(a: string, b: string): { added: number; removed: number } {
|
|
54
|
+
const aLines = a.split("\n");
|
|
55
|
+
const bLines = b.split("\n");
|
|
56
|
+
|
|
57
|
+
const aSet = new Set(aLines);
|
|
58
|
+
const bSet = new Set(bLines);
|
|
59
|
+
|
|
60
|
+
const added = bLines.filter((l) => !aSet.has(l)).length;
|
|
61
|
+
const removed = aLines.filter((l) => !bSet.has(l)).length;
|
|
62
|
+
|
|
63
|
+
return { added, removed };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── MAIN FUNCTION ──
|
|
67
|
+
// Call this after a session ends and the developer has committed their final code
|
|
68
|
+
// projectPath = absolute path to the git repo being worked on
|
|
69
|
+
export async function computeDiff(
|
|
70
|
+
sessionId: string,
|
|
71
|
+
projectPath: string
|
|
72
|
+
): Promise<SessionDiff> {
|
|
73
|
+
const agentFiles = getAgentTouchedFiles(sessionId);
|
|
74
|
+
const diffs: FileDiff[] = [];
|
|
75
|
+
|
|
76
|
+
for (const [filePath, agentWrote] of agentFiles) {
|
|
77
|
+
let finalVersion = "";
|
|
78
|
+
let wasReverted = false;
|
|
79
|
+
|
|
80
|
+
// Try to read the current committed version of the file
|
|
81
|
+
try {
|
|
82
|
+
const relativePath = filePath.replace(projectPath + "/", "");
|
|
83
|
+
const gitContent = execSync(`git show HEAD:${relativePath}`, {
|
|
84
|
+
cwd: projectPath,
|
|
85
|
+
encoding: "utf-8",
|
|
86
|
+
});
|
|
87
|
+
finalVersion = gitContent;
|
|
88
|
+
} catch {
|
|
89
|
+
// File doesn't exist in git — agent created it but it was deleted
|
|
90
|
+
finalVersion = "";
|
|
91
|
+
wasReverted = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If the agent version and final version are different — that's a deviation
|
|
95
|
+
const isDifferent = agentWrote.trim() !== finalVersion.trim();
|
|
96
|
+
|
|
97
|
+
if (isDifferent) {
|
|
98
|
+
const { added, removed } = countLineDiff(agentWrote, finalVersion);
|
|
99
|
+
diffs.push({
|
|
100
|
+
filePath,
|
|
101
|
+
agentWrote,
|
|
102
|
+
finalVersion,
|
|
103
|
+
wasReverted: finalVersion === "",
|
|
104
|
+
linesAdded: added,
|
|
105
|
+
linesRemoved: removed,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sessionDiff: SessionDiff = {
|
|
111
|
+
sessionId,
|
|
112
|
+
projectPath,
|
|
113
|
+
diffs,
|
|
114
|
+
computedAt: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Save to Supabase
|
|
118
|
+
await saveDiff(sessionDiff);
|
|
119
|
+
|
|
120
|
+
return sessionDiff;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── SAVE TO SUPABASE ──
|
|
124
|
+
async function saveDiff(sessionDiff: SessionDiff): Promise<void> {
|
|
125
|
+
const url = process.env.SUPABASE_URL;
|
|
126
|
+
const key = process.env.SUPABASE_ANON_KEY;
|
|
127
|
+
if (!url || !key) return;
|
|
128
|
+
|
|
129
|
+
const client = createClient(url, key);
|
|
130
|
+
|
|
131
|
+
for (const diff of sessionDiff.diffs) {
|
|
132
|
+
const { error } = await client.from("diffs").insert({
|
|
133
|
+
session_id: sessionDiff.sessionId,
|
|
134
|
+
file_path: diff.filePath,
|
|
135
|
+
agent_wrote: diff.agentWrote,
|
|
136
|
+
final_version: diff.finalVersion,
|
|
137
|
+
was_reverted: diff.wasReverted,
|
|
138
|
+
lines_added: diff.linesAdded,
|
|
139
|
+
lines_removed: diff.linesRemoved,
|
|
140
|
+
computed_at: sessionDiff.computedAt,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (error) {
|
|
144
|
+
console.error("[Tracer] Failed to save diff:", error.message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import * as dotenv from "dotenv";
|
|
5
|
+
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
// ── TYPES ──
|
|
9
|
+
interface Correction {
|
|
10
|
+
filePath: string;
|
|
11
|
+
note: string;
|
|
12
|
+
agentWrote: string;
|
|
13
|
+
correctVersion: string;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Deviation {
|
|
18
|
+
filePath: string;
|
|
19
|
+
wasReverted: boolean;
|
|
20
|
+
linesAdded: number;
|
|
21
|
+
linesRemoved: number;
|
|
22
|
+
computedAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── FETCH CORRECTIONS FROM SUPABASE ──
|
|
26
|
+
// Gets the most recent corrections a developer published
|
|
27
|
+
// These are things the developer explicitly flagged as wrong
|
|
28
|
+
async function fetchCorrections(projectPath: string): Promise<Correction[]> {
|
|
29
|
+
const url = process.env.SUPABASE_URL;
|
|
30
|
+
const key = process.env.SUPABASE_ANON_KEY;
|
|
31
|
+
if (!url || !key) return [];
|
|
32
|
+
|
|
33
|
+
const client = createClient(url, key);
|
|
34
|
+
|
|
35
|
+
const { data, error } = await client
|
|
36
|
+
.from("corrections")
|
|
37
|
+
.select("file_path, note, agent_wrote, correct_version, created_at")
|
|
38
|
+
.order("created_at", { ascending: false })
|
|
39
|
+
.limit(20);
|
|
40
|
+
|
|
41
|
+
if (error) {
|
|
42
|
+
console.error("[Tracer] Failed to fetch corrections:", error.message);
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (data ?? []).map((r: any) => ({
|
|
47
|
+
filePath: r.file_path,
|
|
48
|
+
note: r.note,
|
|
49
|
+
agentWrote: r.agent_wrote,
|
|
50
|
+
correctVersion: r.correct_version,
|
|
51
|
+
createdAt: r.created_at,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── FETCH RECENT DEVIATIONS FROM SUPABASE ──
|
|
56
|
+
// Gets files the agent recently got wrong even without a correction note
|
|
57
|
+
// Pattern: if the agent touched a file and it was reverted 2+ times, flag it
|
|
58
|
+
async function fetchPatternDeviations(projectPath: string): Promise<Deviation[]> {
|
|
59
|
+
const url = process.env.SUPABASE_URL;
|
|
60
|
+
const key = process.env.SUPABASE_ANON_KEY;
|
|
61
|
+
if (!url || !key) return [];
|
|
62
|
+
|
|
63
|
+
const client = createClient(url, key);
|
|
64
|
+
|
|
65
|
+
const { data, error } = await client
|
|
66
|
+
.from("diffs")
|
|
67
|
+
.select("file_path, was_reverted, lines_added, lines_removed, computed_at")
|
|
68
|
+
.eq("was_reverted", true)
|
|
69
|
+
.order("computed_at", { ascending: false })
|
|
70
|
+
.limit(10);
|
|
71
|
+
|
|
72
|
+
if (error) {
|
|
73
|
+
console.error("[Tracer] Failed to fetch deviations:", error.message);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (data ?? []).map((r: any) => ({
|
|
78
|
+
filePath: r.file_path,
|
|
79
|
+
wasReverted: r.was_reverted,
|
|
80
|
+
linesAdded: r.lines_added,
|
|
81
|
+
linesRemoved: r.lines_removed,
|
|
82
|
+
computedAt: r.computed_at,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── LOCAL FALLBACK ──
|
|
87
|
+
// If Supabase has no data yet, read from local .tracer/actions.jsonl
|
|
88
|
+
function getLocalContext(): string {
|
|
89
|
+
const logFile = process.env.TRACER_LOG_DIR
|
|
90
|
+
? join(process.env.TRACER_LOG_DIR, "actions.jsonl")
|
|
91
|
+
: join(process.env.HOME || "", ".tracer", "actions.jsonl");
|
|
92
|
+
if (!existsSync(logFile)) return "";
|
|
93
|
+
|
|
94
|
+
const lines = readFileSync(logFile, "utf-8")
|
|
95
|
+
.split("\n")
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map((l) => JSON.parse(l));
|
|
98
|
+
|
|
99
|
+
// Find errored actions
|
|
100
|
+
const errors = lines.filter((l) => l.error !== null);
|
|
101
|
+
if (errors.length === 0) return "";
|
|
102
|
+
|
|
103
|
+
const errorSummary = errors
|
|
104
|
+
.slice(-5) // last 5 errors
|
|
105
|
+
.map((e) => `- Tool: ${e.tool}, Input: ${JSON.stringify(e.input)}, Error: ${e.error}`)
|
|
106
|
+
.join("\n");
|
|
107
|
+
|
|
108
|
+
return `Recent local errors:\n${errorSummary}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── MAIN FUNCTION ──
|
|
112
|
+
// This is what gets called before a new session starts
|
|
113
|
+
// Returns a string that gets prepended to the agent's context
|
|
114
|
+
export async function buildContextPrefix(projectPath: string): Promise<string> {
|
|
115
|
+
const corrections = await fetchCorrections(projectPath);
|
|
116
|
+
const deviations = await fetchPatternDeviations(projectPath);
|
|
117
|
+
const localContext = getLocalContext();
|
|
118
|
+
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
|
|
121
|
+
lines.push("# Tracer: Agent Memory");
|
|
122
|
+
lines.push("The following is a record of past mistakes and corrections on this project.");
|
|
123
|
+
lines.push("Use this to avoid repeating the same errors.\n");
|
|
124
|
+
|
|
125
|
+
// Corrections section — developer annotated these explicitly
|
|
126
|
+
if (corrections.length > 0) {
|
|
127
|
+
lines.push("## Developer Corrections");
|
|
128
|
+
for (const c of corrections) {
|
|
129
|
+
lines.push(`### File: ${c.filePath}`);
|
|
130
|
+
lines.push(`Note: ${c.note}`);
|
|
131
|
+
if (c.agentWrote) {
|
|
132
|
+
lines.push(`Agent wrote:\n\`\`\`\n${c.agentWrote.slice(0, 200)}\n\`\`\``);
|
|
133
|
+
}
|
|
134
|
+
if (c.correctVersion) {
|
|
135
|
+
lines.push(`Correct version:\n\`\`\`\n${c.correctVersion.slice(0, 200)}\n\`\`\``);
|
|
136
|
+
}
|
|
137
|
+
lines.push("");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Deviations section — automatically detected reversions
|
|
142
|
+
if (deviations.length > 0) {
|
|
143
|
+
lines.push("## Automatically Detected Reversions");
|
|
144
|
+
lines.push("These files were written by the agent but reverted by the developer:");
|
|
145
|
+
for (const d of deviations) {
|
|
146
|
+
lines.push(`- ${d.filePath} (reverted on ${d.computedAt.slice(0, 10)})`);
|
|
147
|
+
}
|
|
148
|
+
lines.push("");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Local errors section
|
|
152
|
+
if (localContext) {
|
|
153
|
+
lines.push("## Recent Errors");
|
|
154
|
+
lines.push(localContext);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If nothing to inject, return empty
|
|
158
|
+
if (corrections.length === 0 && deviations.length === 0 && !localContext) {
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── CLI RUNNER ──
|
|
166
|
+
// Run this file directly to preview what context would be injected
|
|
167
|
+
// Usage: tsx src/injector/index.ts /path/to/your/project
|
|
168
|
+
if (require.main === module) {
|
|
169
|
+
const projectPath = process.argv[2] || process.cwd();
|
|
170
|
+
buildContextPrefix(projectPath).then((prefix) => {
|
|
171
|
+
if (!prefix) {
|
|
172
|
+
console.log("[Tracer] No context to inject yet. Run some sessions first.");
|
|
173
|
+
} else {
|
|
174
|
+
console.log(prefix);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
import * as dotenv from "dotenv";
|
|
3
|
+
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
// ── TYPES ──
|
|
7
|
+
export interface Action {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
tool: string;
|
|
10
|
+
input: Record<string, any>;
|
|
11
|
+
output: string | null;
|
|
12
|
+
error: string | null;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── SUPABASE CLIENT ──
|
|
17
|
+
// We lazy-initialize so the server still boots even without .env set up yet
|
|
18
|
+
let supabase: ReturnType<typeof createClient> | null = null;
|
|
19
|
+
|
|
20
|
+
function getClient() {
|
|
21
|
+
if (supabase) return supabase;
|
|
22
|
+
|
|
23
|
+
const url = process.env.SUPABASE_URL;
|
|
24
|
+
const key = process.env.SUPABASE_ANON_KEY;
|
|
25
|
+
|
|
26
|
+
if (!url || !key) {
|
|
27
|
+
console.error("[Tracer] Warning: Supabase not configured. Logging to console only.");
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
supabase = createClient(url, key);
|
|
32
|
+
return supabase;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── LOCAL FALLBACK ──
|
|
36
|
+
// If Supabase isn't configured yet, we write to a local JSON file
|
|
37
|
+
// This means the server works on Day 1 even before Supabase is set up
|
|
38
|
+
import { appendFileSync, existsSync, mkdirSync } from "fs";
|
|
39
|
+
import { join } from "path";
|
|
40
|
+
|
|
41
|
+
const LOG_DIR = process.env.TRACER_LOG_DIR || join(process.env.HOME || "", ".tracer");
|
|
42
|
+
const LOG_FILE = join(LOG_DIR, "actions.jsonl");
|
|
43
|
+
|
|
44
|
+
function logLocally(action: Action) {
|
|
45
|
+
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
|
|
46
|
+
appendFileSync(LOG_FILE, JSON.stringify(action) + "\n", "utf-8");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── MAIN FUNCTION ──
|
|
50
|
+
// This is what gets called from server/index.ts on every agent action
|
|
51
|
+
export async function recordAction(action: Action): Promise<void> {
|
|
52
|
+
// Always log locally as backup
|
|
53
|
+
logLocally(action);
|
|
54
|
+
|
|
55
|
+
const client = getClient();
|
|
56
|
+
if (!client) return;
|
|
57
|
+
|
|
58
|
+
const { error } = await client.from("actions").insert({
|
|
59
|
+
session_id: action.sessionId,
|
|
60
|
+
tool: action.tool,
|
|
61
|
+
input: action.input,
|
|
62
|
+
output: action.output,
|
|
63
|
+
error: action.error,
|
|
64
|
+
timestamp: action.timestamp,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (error) {
|
|
68
|
+
console.error("[Tracer] Failed to save action to Supabase:", error.message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── SESSION CLOSER ──
|
|
73
|
+
// Call this when a session ends to mark it complete in Supabase
|
|
74
|
+
export async function closeSession(sessionId: string, projectPath: string): Promise<void> {
|
|
75
|
+
const client = getClient();
|
|
76
|
+
if (!client) return;
|
|
77
|
+
|
|
78
|
+
const { error } = await client.from("sessions").insert({
|
|
79
|
+
id: sessionId,
|
|
80
|
+
project_path: projectPath,
|
|
81
|
+
ended_at: new Date().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (error) {
|
|
85
|
+
console.error("[Tracer] Failed to close session:", error.message);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
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 { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { recordAction } from "../recorder/index.js";
|
|
6
|
+
import { computeDiff } from "../diff/index.js";
|
|
7
|
+
import { closeSession } from "../recorder/index.js";
|
|
8
|
+
import { buildContextPrefix } from "../injector/index.js";
|
|
9
|
+
|
|
10
|
+
// Each session gets a unique ID when the MCP server starts
|
|
11
|
+
const SESSION_ID = uuidv4();
|
|
12
|
+
const SESSION_START = new Date().toISOString();
|
|
13
|
+
|
|
14
|
+
console.error(`[Tracer] Session started: ${SESSION_ID}`);
|
|
15
|
+
|
|
16
|
+
// This is the MCP server — it's what Claude Code connects to
|
|
17
|
+
const server = new McpServer({
|
|
18
|
+
name: "tracer",
|
|
19
|
+
version: "0.1.0",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ── TOOL 1: read_file ──
|
|
23
|
+
// Claude Code calls this when it wants to read a file
|
|
24
|
+
// We intercept it, log it, then actually read the file
|
|
25
|
+
server.tool(
|
|
26
|
+
"read_file",
|
|
27
|
+
"Read the contents of a file",
|
|
28
|
+
{
|
|
29
|
+
path: z.string().describe("Path to the file to read"),
|
|
30
|
+
},
|
|
31
|
+
async ({ path }) => {
|
|
32
|
+
const fs = await import("fs/promises");
|
|
33
|
+
|
|
34
|
+
let content = "";
|
|
35
|
+
let error = null;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
content = await fs.readFile(path, "utf-8");
|
|
39
|
+
} catch (e: any) {
|
|
40
|
+
error = e.message;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Record this action regardless of success or failure
|
|
44
|
+
await recordAction({
|
|
45
|
+
sessionId: SESSION_ID,
|
|
46
|
+
tool: "read_file",
|
|
47
|
+
input: { path },
|
|
48
|
+
output: error ? null : content.slice(0, 500), // store first 500 chars
|
|
49
|
+
error,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (error) {
|
|
54
|
+
return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { content: [{ type: "text", text: content }] };
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// ── TOOL 2: write_file ──
|
|
62
|
+
// Claude Code calls this when it wants to write or edit a file
|
|
63
|
+
// This is the most important one to track — writes = agent decisions
|
|
64
|
+
server.tool(
|
|
65
|
+
"write_file",
|
|
66
|
+
"Write content to a file",
|
|
67
|
+
{
|
|
68
|
+
path: z.string().describe("Path to the file to write"),
|
|
69
|
+
content: z.string().describe("Content to write to the file"),
|
|
70
|
+
},
|
|
71
|
+
async ({ path, content }) => {
|
|
72
|
+
const fs = await import("fs/promises");
|
|
73
|
+
const pathModule = await import("path");
|
|
74
|
+
|
|
75
|
+
let error = null;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Make sure the directory exists before writing
|
|
79
|
+
await fs.mkdir(pathModule.dirname(path), { recursive: true });
|
|
80
|
+
await fs.writeFile(path, content, "utf-8");
|
|
81
|
+
} catch (e: any) {
|
|
82
|
+
error = e.message;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await recordAction({
|
|
86
|
+
sessionId: SESSION_ID,
|
|
87
|
+
tool: "write_file",
|
|
88
|
+
input: { path, content: content.slice(0, 500) },
|
|
89
|
+
output: error ? null : "success",
|
|
90
|
+
error,
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (error) {
|
|
95
|
+
return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { content: [{ type: "text", text: `Written to ${path}` }] };
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// ── TOOL 3: run_command ──
|
|
103
|
+
// Claude Code calls this when it runs terminal commands
|
|
104
|
+
server.tool(
|
|
105
|
+
"run_command",
|
|
106
|
+
"Run a shell command",
|
|
107
|
+
{
|
|
108
|
+
command: z.string().describe("The shell command to run"),
|
|
109
|
+
},
|
|
110
|
+
async ({ command }) => {
|
|
111
|
+
const { exec } = await import("child_process");
|
|
112
|
+
const { promisify } = await import("util");
|
|
113
|
+
const execAsync = promisify(exec);
|
|
114
|
+
|
|
115
|
+
let stdout = "";
|
|
116
|
+
let stderr = "";
|
|
117
|
+
let error = null;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await execAsync(command, { timeout: 30000 });
|
|
121
|
+
stdout = result.stdout;
|
|
122
|
+
stderr = result.stderr;
|
|
123
|
+
} catch (e: any) {
|
|
124
|
+
error = e.message;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await recordAction({
|
|
128
|
+
sessionId: SESSION_ID,
|
|
129
|
+
tool: "run_command",
|
|
130
|
+
input: { command },
|
|
131
|
+
output: stdout.slice(0, 500),
|
|
132
|
+
error: error || stderr.slice(0, 200) || null,
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: error ? `Error: ${error}` : stdout || stderr || "Done",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// ── TOOL 4: close_session ──
|
|
148
|
+
// Called when the agent session ends
|
|
149
|
+
// Triggers the diff engine and saves everything to Supabase
|
|
150
|
+
server.tool(
|
|
151
|
+
"close_session",
|
|
152
|
+
"Close the current session and compute diffs against git",
|
|
153
|
+
{
|
|
154
|
+
projectPath: z.string().describe("Absolute path to the git repo"),
|
|
155
|
+
},
|
|
156
|
+
async ({ projectPath }) => {
|
|
157
|
+
console.error(`[Tracer] Closing session ${SESSION_ID}...`);
|
|
158
|
+
|
|
159
|
+
// Save session record to Supabase
|
|
160
|
+
await closeSession(SESSION_ID, projectPath);
|
|
161
|
+
|
|
162
|
+
// Compute what the agent did vs what was committed
|
|
163
|
+
const result = await computeDiff(SESSION_ID, projectPath);
|
|
164
|
+
|
|
165
|
+
const deviationCount = result.diffs.length;
|
|
166
|
+
|
|
167
|
+
console.error(`[Tracer] Session closed. Found ${deviationCount} deviations.`);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: "text",
|
|
173
|
+
text: JSON.stringify({
|
|
174
|
+
sessionId: SESSION_ID,
|
|
175
|
+
deviations: deviationCount,
|
|
176
|
+
files: result.diffs.map((d) => ({
|
|
177
|
+
path: d.filePath,
|
|
178
|
+
wasReverted: d.wasReverted,
|
|
179
|
+
linesChanged: d.linesAdded + d.linesRemoved,
|
|
180
|
+
})),
|
|
181
|
+
}, null, 2),
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// ── TOOL 5: get_context ──
|
|
189
|
+
// Called at the start of a new session
|
|
190
|
+
// Returns all past corrections and deviations as a context string
|
|
191
|
+
server.tool(
|
|
192
|
+
"get_context",
|
|
193
|
+
"Get past corrections and known mistakes for this project",
|
|
194
|
+
{
|
|
195
|
+
projectPath: z.string().describe("Absolute path to the project"),
|
|
196
|
+
},
|
|
197
|
+
async ({ projectPath }) => {
|
|
198
|
+
const prefix = await buildContextPrefix(projectPath);
|
|
199
|
+
|
|
200
|
+
if (!prefix) {
|
|
201
|
+
return {
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: "text",
|
|
205
|
+
text: "No past corrections found for this project. Starting fresh.",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: prefix,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// ── START SERVER ──
|
|
223
|
+
async function main() {
|
|
224
|
+
const transport = new StdioServerTransport();
|
|
225
|
+
await server.connect(transport);
|
|
226
|
+
console.error(`[Tracer] MCP server running. Listening for agent actions...`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
main().catch(console.error);
|
package/test.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hello from tracer
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules"]
|
|
15
|
+
}
|