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 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ require('child_process').spawnSync(
3
+ 'npx', ['tsx', __dirname + '/cli.ts', ...process.argv.slice(2)],
4
+ { stdio: 'inherit' }
5
+ );
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
+ }
@@ -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
+ }
@@ -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,6 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+
3
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
4
+ const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
5
+
6
+ export const supabase = createClient(url, key);
@@ -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
+ }