claude-baton 2.0.1
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/LICENSE +21 -0
- package/README.md +115 -0
- package/bin/claude-baton.js +2 -0
- package/commands/memo-checkpoint.md +43 -0
- package/commands/memo-eod.md +42 -0
- package/commands/memo-resume.md +76 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +392 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +251 -0
- package/dist/llm.d.ts +16 -0
- package/dist/llm.js +119 -0
- package/dist/store.d.ts +28 -0
- package/dist/store.js +249 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +10 -0
- package/package.json +60 -0
- package/prompts/auto_checkpoint.txt +30 -0
- package/prompts/daily_summary.txt +29 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync, writeFileSync, copyFileSync, existsSync, statSync, readdirSync, unlinkSync, rmSync, } from "fs";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import { createInterface } from "readline";
|
|
8
|
+
import { initDatabase, getDefaultDbPath, saveDatabase, countAll, listProjects, insertCheckpoint, getAllCheckpoints, getAllDailySummaries, deleteProjectData, deleteAllData, } from "./store.js";
|
|
9
|
+
import { ensureDir } from "./utils.js";
|
|
10
|
+
import { callClaudeJson } from "./llm.js";
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
function readPromptTemplate(name) {
|
|
13
|
+
return readFileSync(path.join(__dirname, "..", "prompts", name), "utf-8");
|
|
14
|
+
}
|
|
15
|
+
function gitCmd(cmd) {
|
|
16
|
+
try {
|
|
17
|
+
return execSync(cmd, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function handleAutoCheckpoint() {
|
|
24
|
+
try {
|
|
25
|
+
// Read hook metadata from stdin
|
|
26
|
+
let stdinData = "";
|
|
27
|
+
try {
|
|
28
|
+
stdinData = readFileSync(0, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
console.error("[claude-baton] No stdin data, skipping auto-checkpoint");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let transcriptPath;
|
|
35
|
+
try {
|
|
36
|
+
const metadata = JSON.parse(stdinData);
|
|
37
|
+
transcriptPath = metadata?.input?.metadata?.transcript_path;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
console.error("[claude-baton] Could not parse hook metadata, skipping auto-checkpoint");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
44
|
+
console.error("[claude-baton] Transcript not found, skipping auto-checkpoint");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Read and truncate transcript
|
|
48
|
+
const fullTranscript = readFileSync(transcriptPath, "utf-8");
|
|
49
|
+
const MAX_CHARS = 50000;
|
|
50
|
+
const transcript = fullTranscript.length > MAX_CHARS
|
|
51
|
+
? fullTranscript.slice(-MAX_CHARS)
|
|
52
|
+
: fullTranscript;
|
|
53
|
+
// Gather git state
|
|
54
|
+
const branch = gitCmd("git branch --show-current");
|
|
55
|
+
const status = gitCmd("git status --short");
|
|
56
|
+
const log = gitCmd("git log --oneline -10");
|
|
57
|
+
// Build prompt
|
|
58
|
+
const template = readPromptTemplate("auto_checkpoint.txt");
|
|
59
|
+
const prompt = template.replace("{TRANSCRIPT}", transcript);
|
|
60
|
+
// Call LLM
|
|
61
|
+
const result = await callClaudeJson(prompt, "haiku", 30000);
|
|
62
|
+
// Save checkpoint
|
|
63
|
+
const dbPath = getDefaultDbPath();
|
|
64
|
+
const db = await initDatabase(dbPath);
|
|
65
|
+
const projectPath = process.cwd();
|
|
66
|
+
const sessionId = new Date().toISOString();
|
|
67
|
+
const uncommittedFiles = status
|
|
68
|
+
? status.split("\n").map((l) => l.trim())
|
|
69
|
+
: [];
|
|
70
|
+
insertCheckpoint(db, projectPath, sessionId, result.current_state || "Unknown", result.what_was_built || "Unknown", result.next_steps || "Unknown", {
|
|
71
|
+
branch: branch || undefined,
|
|
72
|
+
decisionsMade: result.decisions_made || undefined,
|
|
73
|
+
blockers: result.blockers || undefined,
|
|
74
|
+
uncommittedFiles,
|
|
75
|
+
gitSnapshot: log || undefined,
|
|
76
|
+
planReference: result.plan_reference || undefined,
|
|
77
|
+
}, dbPath);
|
|
78
|
+
console.error("[claude-baton] Auto-checkpoint saved before compaction");
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
82
|
+
console.error(`[claude-baton] Auto-checkpoint failed: ${msg}`);
|
|
83
|
+
// Exit gracefully — don't block compaction
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// --- Setup command ---
|
|
87
|
+
export async function handleSetup() {
|
|
88
|
+
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
89
|
+
const settingsDir = path.dirname(settingsPath);
|
|
90
|
+
ensureDir(settingsDir);
|
|
91
|
+
let settings = {};
|
|
92
|
+
if (existsSync(settingsPath)) {
|
|
93
|
+
try {
|
|
94
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
settings = {};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Register MCP server
|
|
101
|
+
const mcpServers = (settings.mcpServers ?? {});
|
|
102
|
+
mcpServers["claude-baton"] = {
|
|
103
|
+
command: "npx",
|
|
104
|
+
args: ["-y", "claude-baton", "serve"],
|
|
105
|
+
};
|
|
106
|
+
settings.mcpServers = mcpServers;
|
|
107
|
+
// Register PreCompact hook (idempotent — skip if already present)
|
|
108
|
+
const hooks = (settings.hooks ?? {});
|
|
109
|
+
const preCompactHooks = (hooks.PreCompact ?? []);
|
|
110
|
+
const hasMemoriaHook = preCompactHooks.some((h) => h.command && h.command.includes("claude-baton"));
|
|
111
|
+
if (!hasMemoriaHook) {
|
|
112
|
+
preCompactHooks.push({
|
|
113
|
+
type: "command",
|
|
114
|
+
command: "npx -y claude-baton auto-checkpoint",
|
|
115
|
+
});
|
|
116
|
+
hooks.PreCompact = preCompactHooks;
|
|
117
|
+
settings.hooks = hooks;
|
|
118
|
+
console.error(" Registered PreCompact hook");
|
|
119
|
+
}
|
|
120
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
121
|
+
const dbPath = getDefaultDbPath();
|
|
122
|
+
await initDatabase(dbPath);
|
|
123
|
+
const cmdResult = installCommands();
|
|
124
|
+
console.error(`Setup complete.`);
|
|
125
|
+
console.error(` Database: ${dbPath}`);
|
|
126
|
+
console.error(` MCP server: registered`);
|
|
127
|
+
console.error(` Commands: ${cmdResult.installed} installed, ${cmdResult.skipped} skipped`);
|
|
128
|
+
}
|
|
129
|
+
// --- Install memo- commands ---
|
|
130
|
+
export function installCommands() {
|
|
131
|
+
const sourceDir = path.join(__dirname, "..", "commands");
|
|
132
|
+
const targetDir = path.join(os.homedir(), ".claude", "commands");
|
|
133
|
+
ensureDir(targetDir);
|
|
134
|
+
let installed = 0;
|
|
135
|
+
let skipped = 0;
|
|
136
|
+
const files = readdirSync(sourceDir).filter((f) => f.endsWith(".md"));
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
const targetPath = path.join(targetDir, file);
|
|
139
|
+
if (existsSync(targetPath)) {
|
|
140
|
+
const name = file.replace(".md", "");
|
|
141
|
+
console.error(` Skipping ${name} -- already exists`);
|
|
142
|
+
skipped++;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
copyFileSync(path.join(sourceDir, file), targetPath);
|
|
146
|
+
const name = file.replace(".md", "");
|
|
147
|
+
console.error(` Installed /${name}`);
|
|
148
|
+
installed++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { installed, skipped };
|
|
152
|
+
}
|
|
153
|
+
// --- Uninstall command ---
|
|
154
|
+
export async function handleUninstall(opts) {
|
|
155
|
+
// 1. Remove MCP server from settings.json
|
|
156
|
+
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
157
|
+
if (existsSync(settingsPath)) {
|
|
158
|
+
try {
|
|
159
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
160
|
+
// Remove MCP server registration
|
|
161
|
+
if (settings.mcpServers &&
|
|
162
|
+
typeof settings.mcpServers === "object" &&
|
|
163
|
+
settings.mcpServers["claude-baton"]) {
|
|
164
|
+
delete settings.mcpServers["claude-baton"];
|
|
165
|
+
if (Object.keys(settings.mcpServers).length === 0) {
|
|
166
|
+
delete settings.mcpServers;
|
|
167
|
+
}
|
|
168
|
+
console.error(" Removed MCP server from settings.json");
|
|
169
|
+
}
|
|
170
|
+
// Remove PreCompact hook
|
|
171
|
+
if (settings.hooks &&
|
|
172
|
+
typeof settings.hooks === "object" &&
|
|
173
|
+
settings.hooks.PreCompact) {
|
|
174
|
+
const hooksObj = settings.hooks;
|
|
175
|
+
const preCompact = hooksObj.PreCompact;
|
|
176
|
+
const filtered = preCompact.filter((h) => !h.command || !h.command.includes("claude-baton"));
|
|
177
|
+
if (filtered.length === 0) {
|
|
178
|
+
delete hooksObj.PreCompact;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
hooksObj.PreCompact = filtered;
|
|
182
|
+
}
|
|
183
|
+
if (Object.keys(hooksObj).length === 0) {
|
|
184
|
+
delete settings.hooks;
|
|
185
|
+
}
|
|
186
|
+
console.error(" Removed PreCompact hook");
|
|
187
|
+
}
|
|
188
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
console.error(" Warning: could not parse settings.json");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// 2. Remove memo-* command files
|
|
195
|
+
const commandsDir = path.join(os.homedir(), ".claude", "commands");
|
|
196
|
+
let commandsRemoved = 0;
|
|
197
|
+
if (existsSync(commandsDir)) {
|
|
198
|
+
const files = readdirSync(commandsDir).filter((f) => f.startsWith("memo-") && f.endsWith(".md"));
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
unlinkSync(path.join(commandsDir, file));
|
|
201
|
+
commandsRemoved++;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
console.error(` Removed ${commandsRemoved} slash commands`);
|
|
205
|
+
// 3. Optionally remove database
|
|
206
|
+
const dbDir = path.join(os.homedir(), ".claude-baton");
|
|
207
|
+
if (!opts.keepData && existsSync(dbDir)) {
|
|
208
|
+
if (!opts.force) {
|
|
209
|
+
const answer = await askConfirmation(" Delete database (~/.claude-baton)? This cannot be undone. [y/N] ");
|
|
210
|
+
if (answer.toLowerCase() !== "y") {
|
|
211
|
+
console.error(" Kept database.");
|
|
212
|
+
console.error("Uninstall complete (database preserved).");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
rmSync(dbDir, { recursive: true });
|
|
217
|
+
console.error(" Deleted database");
|
|
218
|
+
}
|
|
219
|
+
else if (opts.keepData) {
|
|
220
|
+
console.error(" Kept database (--keep-data)");
|
|
221
|
+
}
|
|
222
|
+
console.error("Uninstall complete. Run 'npm uninstall -g claude-baton' to remove the binary.");
|
|
223
|
+
}
|
|
224
|
+
// --- Status command ---
|
|
225
|
+
export async function handleStatus(opts) {
|
|
226
|
+
const dbPath = getDefaultDbPath();
|
|
227
|
+
if (!existsSync(dbPath)) {
|
|
228
|
+
console.error("No database found. Run 'claude-baton setup' first.");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const db = await initDatabase(dbPath);
|
|
232
|
+
const projectPath = opts.project ?? process.cwd();
|
|
233
|
+
const counts = countAll(db, projectPath);
|
|
234
|
+
const dbSize = statSync(dbPath).size;
|
|
235
|
+
console.log(`Project: ${projectPath}`);
|
|
236
|
+
console.log(`Database: ${dbPath} (${(dbSize / 1024).toFixed(1)} KB)`);
|
|
237
|
+
console.log();
|
|
238
|
+
console.log("Counts:");
|
|
239
|
+
for (const [key, value] of Object.entries(counts)) {
|
|
240
|
+
console.log(` ${key}: ${value}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// --- Projects command ---
|
|
244
|
+
export async function handleProjects() {
|
|
245
|
+
const dbPath = getDefaultDbPath();
|
|
246
|
+
if (!existsSync(dbPath)) {
|
|
247
|
+
console.error("No database found. Run 'claude-baton setup' first.");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const db = await initDatabase(dbPath);
|
|
251
|
+
const projects = listProjects(db);
|
|
252
|
+
if (projects.length === 0) {
|
|
253
|
+
console.log("No projects with checkpoints yet.");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
for (const p of projects) {
|
|
257
|
+
console.log(`${p.project_path} (${p.checkpoint_count} checkpoints)`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// --- Export command ---
|
|
261
|
+
export async function handleExport(opts) {
|
|
262
|
+
const dbPath = getDefaultDbPath();
|
|
263
|
+
if (!existsSync(dbPath)) {
|
|
264
|
+
console.error("No database found. Run 'claude-baton setup' first.");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const db = await initDatabase(dbPath);
|
|
268
|
+
const projectPath = opts.project;
|
|
269
|
+
const data = {
|
|
270
|
+
version: 2,
|
|
271
|
+
exported_at: new Date().toISOString(),
|
|
272
|
+
checkpoints: getAllCheckpoints(db, projectPath),
|
|
273
|
+
daily_summaries: getAllDailySummaries(db, projectPath),
|
|
274
|
+
};
|
|
275
|
+
console.log(JSON.stringify(data, null, 2));
|
|
276
|
+
}
|
|
277
|
+
// --- Import command ---
|
|
278
|
+
export async function handleImport(file) {
|
|
279
|
+
const dbPath = getDefaultDbPath();
|
|
280
|
+
const db = await initDatabase(dbPath);
|
|
281
|
+
let data;
|
|
282
|
+
try {
|
|
283
|
+
const raw = readFileSync(file, "utf-8");
|
|
284
|
+
data = JSON.parse(raw);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
288
|
+
console.error(`Failed to read import file: ${msg}`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
let imported = 0;
|
|
292
|
+
if (Array.isArray(data.checkpoints)) {
|
|
293
|
+
for (const cp of data.checkpoints) {
|
|
294
|
+
insertCheckpoint(db, cp.project_path, cp.session_id, cp.current_state, cp.what_was_built, cp.next_steps, {
|
|
295
|
+
branch: cp.branch,
|
|
296
|
+
decisionsMade: cp.decisions_made,
|
|
297
|
+
blockers: cp.blockers,
|
|
298
|
+
uncommittedFiles: cp.uncommitted_files,
|
|
299
|
+
gitSnapshot: cp.git_snapshot,
|
|
300
|
+
});
|
|
301
|
+
imported++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
saveDatabase(db, dbPath);
|
|
305
|
+
console.error(`Imported ${imported} items.`);
|
|
306
|
+
}
|
|
307
|
+
// --- Reset command ---
|
|
308
|
+
export async function handleReset(opts) {
|
|
309
|
+
const dbPath = getDefaultDbPath();
|
|
310
|
+
if (!existsSync(dbPath)) {
|
|
311
|
+
console.error("No database found. Nothing to reset.");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (!opts.force) {
|
|
315
|
+
const target = opts.project ?? "ALL projects";
|
|
316
|
+
const answer = await askConfirmation(`Reset data for ${target}? This cannot be undone. [y/N] `);
|
|
317
|
+
if (answer.toLowerCase() !== "y") {
|
|
318
|
+
console.error("Aborted.");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const db = await initDatabase(dbPath);
|
|
323
|
+
if (opts.project) {
|
|
324
|
+
deleteProjectData(db, opts.project, dbPath);
|
|
325
|
+
console.error(`Reset data for project: ${opts.project}`);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
deleteAllData(db, dbPath);
|
|
329
|
+
console.error("Reset all data.");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function askConfirmation(prompt) {
|
|
333
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
334
|
+
return new Promise((resolve) => {
|
|
335
|
+
rl.question(prompt, (answer) => {
|
|
336
|
+
rl.close();
|
|
337
|
+
resolve(answer);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
// --- CLI program ---
|
|
342
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"));
|
|
343
|
+
const program = new Command();
|
|
344
|
+
program
|
|
345
|
+
.name("claude-baton")
|
|
346
|
+
.description("Session lifecycle management for Claude Code")
|
|
347
|
+
.version(pkg.version);
|
|
348
|
+
program
|
|
349
|
+
.command("serve")
|
|
350
|
+
.description("Start the MCP server (stdio transport)")
|
|
351
|
+
.action(async () => {
|
|
352
|
+
await import("./index.js");
|
|
353
|
+
});
|
|
354
|
+
program
|
|
355
|
+
.command("setup")
|
|
356
|
+
.description("Register MCP server and initialize database")
|
|
357
|
+
.action(() => handleSetup());
|
|
358
|
+
program
|
|
359
|
+
.command("auto-checkpoint")
|
|
360
|
+
.description("Auto-save checkpoint (called by PreCompact hook)")
|
|
361
|
+
.action(() => handleAutoCheckpoint());
|
|
362
|
+
program
|
|
363
|
+
.command("uninstall")
|
|
364
|
+
.description("Remove MCP server, slash commands, and optionally the database")
|
|
365
|
+
.option("--keep-data", "Keep the database (~/.claude-baton)")
|
|
366
|
+
.option("--force", "Skip confirmation for database deletion")
|
|
367
|
+
.action((opts) => handleUninstall(opts));
|
|
368
|
+
program
|
|
369
|
+
.command("status")
|
|
370
|
+
.description("Show checkpoint counts and status")
|
|
371
|
+
.option("--project <path>", "Project path (default: cwd)")
|
|
372
|
+
.action((opts) => handleStatus(opts));
|
|
373
|
+
program
|
|
374
|
+
.command("projects")
|
|
375
|
+
.description("List projects with checkpoints")
|
|
376
|
+
.action(() => handleProjects());
|
|
377
|
+
program
|
|
378
|
+
.command("export")
|
|
379
|
+
.description("Export all data as JSON to stdout")
|
|
380
|
+
.option("--project <path>", "Filter by project path")
|
|
381
|
+
.action((opts) => handleExport(opts));
|
|
382
|
+
program
|
|
383
|
+
.command("import <file>")
|
|
384
|
+
.description("Import data from JSON file")
|
|
385
|
+
.action((file) => handleImport(file));
|
|
386
|
+
program
|
|
387
|
+
.command("reset")
|
|
388
|
+
.description("Delete all data (or data for a project)")
|
|
389
|
+
.option("--project <path>", "Only reset this project")
|
|
390
|
+
.option("--force", "Skip confirmation")
|
|
391
|
+
.action((opts) => handleReset(opts));
|
|
392
|
+
program.parse();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { initDatabase, getDefaultDbPath, insertCheckpoint, getLatestCheckpoint, getCheckpoint, getCheckpointsByDate, insertDailySummary, } from "./store.js";
|
|
5
|
+
import { callClaudeJson } from "./llm.js";
|
|
6
|
+
import { readFileSync, statSync } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
let db;
|
|
11
|
+
let dbPath;
|
|
12
|
+
let lastDbMtime = 0;
|
|
13
|
+
/** Reload the database from disk if the file has been modified externally. */
|
|
14
|
+
async function reloadDbIfChanged() {
|
|
15
|
+
try {
|
|
16
|
+
const mtimeMs = statSync(dbPath).mtimeMs;
|
|
17
|
+
if (mtimeMs > lastDbMtime) {
|
|
18
|
+
db = await initDatabase(dbPath);
|
|
19
|
+
lastDbMtime = mtimeMs;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// File doesn't exist or stat failed — keep current db as-is
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Stable fallback session ID — generated once at module load, not per call. */
|
|
27
|
+
const fallbackSessionId = `session-${Date.now()}`;
|
|
28
|
+
/** Build an MCP error response with a descriptive message. */
|
|
29
|
+
function toolError(message) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
32
|
+
isError: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate that a required string argument is present and is a string.
|
|
37
|
+
* Returns the validated string, or throws with a descriptive message.
|
|
38
|
+
*/
|
|
39
|
+
function requireString(args, field, toolName) {
|
|
40
|
+
const value = args?.[field];
|
|
41
|
+
if (!value || typeof value !== "string") {
|
|
42
|
+
throw new ValidationError(`${toolName} requires a "${field}" string argument`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/** Sentinel error class for argument validation — caught in the handler to return toolError. */
|
|
47
|
+
class ValidationError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "ValidationError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const server = new Server({ name: "claude-baton", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
54
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
55
|
+
tools: [
|
|
56
|
+
{
|
|
57
|
+
name: "save_checkpoint",
|
|
58
|
+
description: "Save session state before context loss",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
what_was_built: { type: "string" },
|
|
63
|
+
current_state: { type: "string" },
|
|
64
|
+
next_steps: { type: "string" },
|
|
65
|
+
decisions: { type: "string" },
|
|
66
|
+
blockers: { type: "string" },
|
|
67
|
+
branch: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Current git branch name",
|
|
70
|
+
},
|
|
71
|
+
uncommitted_files: {
|
|
72
|
+
type: "array",
|
|
73
|
+
items: { type: "string" },
|
|
74
|
+
description: "Output of git status --short",
|
|
75
|
+
},
|
|
76
|
+
git_snapshot: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Recent commits, e.g. output of git log --oneline -10",
|
|
79
|
+
},
|
|
80
|
+
plan_reference: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Reference to active plan document and section, e.g. 'docs/plan.md Phase 2 Step 3'",
|
|
83
|
+
},
|
|
84
|
+
project: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Project path (defaults to cwd)",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ["what_was_built", "current_state", "next_steps"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "get_checkpoint",
|
|
94
|
+
description: "Retrieve a checkpoint by ID, or the latest for the project",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
id: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "Checkpoint ID to fetch; if omitted, returns the latest",
|
|
101
|
+
},
|
|
102
|
+
project: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Project path (defaults to cwd)",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "list_checkpoints",
|
|
111
|
+
description: "List all checkpoints for a date",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
date: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "YYYY-MM-DD (defaults to today)",
|
|
118
|
+
},
|
|
119
|
+
project: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "Project path (defaults to cwd)",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "daily_summary",
|
|
128
|
+
description: "Generate EOD summary from the day's activity",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
date: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Date (YYYY-MM-DD, defaults to today)",
|
|
135
|
+
},
|
|
136
|
+
project: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Project path (defaults to cwd)",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
}));
|
|
145
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
146
|
+
const { name, arguments: args } = request.params;
|
|
147
|
+
const projectPath = args?.project ?? process.cwd();
|
|
148
|
+
await reloadDbIfChanged();
|
|
149
|
+
try {
|
|
150
|
+
switch (name) {
|
|
151
|
+
case "save_checkpoint": {
|
|
152
|
+
const a = args;
|
|
153
|
+
const whatWasBuilt = requireString(a, "what_was_built", "save_checkpoint");
|
|
154
|
+
const currentState = requireString(a, "current_state", "save_checkpoint");
|
|
155
|
+
const nextSteps = requireString(a, "next_steps", "save_checkpoint");
|
|
156
|
+
const sessionId = process.env.CLAUDE_SESSION_ID ?? fallbackSessionId;
|
|
157
|
+
const id = insertCheckpoint(db, projectPath, sessionId, currentState, whatWasBuilt, nextSteps, {
|
|
158
|
+
branch: a?.branch,
|
|
159
|
+
decisionsMade: a?.decisions,
|
|
160
|
+
blockers: a?.blockers,
|
|
161
|
+
uncommittedFiles: a?.uncommitted_files,
|
|
162
|
+
gitSnapshot: a?.git_snapshot,
|
|
163
|
+
planReference: a?.plan_reference,
|
|
164
|
+
}, dbPath);
|
|
165
|
+
return { content: [{ type: "text", text: `Checkpoint saved: ${id}` }] };
|
|
166
|
+
}
|
|
167
|
+
case "get_checkpoint": {
|
|
168
|
+
const cpId = args?.id;
|
|
169
|
+
const cp = cpId
|
|
170
|
+
? getCheckpoint(db, cpId)
|
|
171
|
+
: getLatestCheckpoint(db, projectPath);
|
|
172
|
+
if (!cp)
|
|
173
|
+
return { content: [{ type: "text", text: "No checkpoint found" }] };
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: JSON.stringify(cp, null, 2) }],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
case "list_checkpoints": {
|
|
179
|
+
const cpDate = args?.date ?? new Date().toISOString().slice(0, 10);
|
|
180
|
+
const cps = getCheckpointsByDate(db, projectPath, cpDate);
|
|
181
|
+
const summary = cps.map((cp) => ({
|
|
182
|
+
id: cp.id,
|
|
183
|
+
created_at: cp.created_at,
|
|
184
|
+
what_was_built: cp.what_was_built,
|
|
185
|
+
branch: cp.branch,
|
|
186
|
+
current_state: cp.current_state,
|
|
187
|
+
}));
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
case "daily_summary": {
|
|
193
|
+
const date = args?.date ?? new Date().toISOString().slice(0, 10);
|
|
194
|
+
const checkpoints = getCheckpointsByDate(db, projectPath, date);
|
|
195
|
+
if (checkpoints.length === 0) {
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text", text: `No activity found for ${date}` }],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const activityParts = [];
|
|
201
|
+
activityParts.push("CHECKPOINTS:\n" +
|
|
202
|
+
checkpoints
|
|
203
|
+
.map((cp) => `- [${cp.created_at}] Built: ${cp.what_was_built} | State: ${cp.current_state} | Next: ${cp.next_steps}`)
|
|
204
|
+
.join("\n"));
|
|
205
|
+
const summaryTemplate = readFileSync(path.join(__dirname, "..", "prompts", "daily_summary.txt"), "utf-8");
|
|
206
|
+
const summaryPrompt = summaryTemplate
|
|
207
|
+
.replace("{{DATE}}", date)
|
|
208
|
+
.replace("{{ACTIVITY}}", activityParts.join("\n\n"));
|
|
209
|
+
const summaryResult = await callClaudeJson(summaryPrompt, "haiku", 30000);
|
|
210
|
+
insertDailySummary(db, projectPath, date, summaryResult, dbPath);
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: JSON.stringify(summaryResult, null, 2),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
default:
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
223
|
+
isError: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
229
|
+
console.error(`Tool error (${name}):`, msg);
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
232
|
+
isError: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
async function main() {
|
|
237
|
+
dbPath = getDefaultDbPath();
|
|
238
|
+
db = await initDatabase(dbPath);
|
|
239
|
+
try {
|
|
240
|
+
lastDbMtime = statSync(dbPath).mtimeMs;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// File may not exist yet if initDatabase created it in-memory only
|
|
244
|
+
}
|
|
245
|
+
const transport = new StdioServerTransport();
|
|
246
|
+
await server.connect(transport);
|
|
247
|
+
}
|
|
248
|
+
main().catch((error) => {
|
|
249
|
+
console.error("Server error:", error);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
});
|
package/dist/llm.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip markdown code fences from LLM output.
|
|
3
|
+
* LLMs frequently wrap JSON in ```json ... ``` blocks.
|
|
4
|
+
*/
|
|
5
|
+
export declare function stripCodeFences(text: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Call claude -p and return the result as a string.
|
|
8
|
+
* If the raw output is JSON with a `result` field, extracts it.
|
|
9
|
+
* Always returns a string — non-string result values are JSON.stringified.
|
|
10
|
+
*/
|
|
11
|
+
export declare function callClaude(prompt: string, model?: string, timeout?: number): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Call claude -p and parse the response as JSON of type T.
|
|
14
|
+
* Uses the raw stdout (not the string-extracted callClaude) to avoid double-parse.
|
|
15
|
+
*/
|
|
16
|
+
export declare function callClaudeJson<T>(prompt: string, model?: string, timeout?: number): Promise<T>;
|