claude-baton 2.0.1 → 2.1.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/README.md CHANGED
@@ -27,8 +27,9 @@ claude-baton setup
27
27
  ```
28
28
 
29
29
  This:
30
- - Registers the MCP server in `~/.claude/settings.json`
30
+ - Registers the MCP server via `claude mcp add` (user scope, available across all projects)
31
31
  - Registers the PreCompact hook for auto-checkpoint
32
+ - Registers allowed tools so slash commands run without approval prompts
32
33
  - Initializes the SQLite database at `~/.claude-baton/store.db`
33
34
  - Installs slash commands to `~/.claude/commands/`
34
35
 
@@ -97,8 +98,8 @@ All data lives in `~/.claude-baton/store.db`:
97
98
  ## Requirements
98
99
 
99
100
  - Node.js >= 18
100
- - Claude Code with a Claude subscription (for `claude -p` calls)
101
- - No API keys needed
101
+ - Claude Code CLI installed and authenticated (setup uses `claude mcp add`)
102
+ - No API keys needed — LLM calls use `claude -p`
102
103
 
103
104
  ## Development
104
105
 
@@ -8,6 +8,17 @@ Restore context from last checkpoint at session start.
8
8
 
9
9
  3. Call the `get_checkpoint` MCP tool (latest by default, or by ID if the user specified one via $ARGUMENTS).
10
10
 
11
+ 3.5. **Staleness check:** If a checkpoint was found, compute the time delta between the checkpoint's `created_at` and now:
12
+ - More than 7 days old: display `"WARNING: This checkpoint is [N] days old. Project state may have changed significantly."`
13
+ - More than 24 hours but 7 days or less: display `"Note: This checkpoint is [N] hours/days old."`
14
+ - 24 hours or less: no staleness note
15
+
16
+ 3.75. **Cold-start fallback:** If NO checkpoint exists for this project:
17
+ - Run `git log --oneline -20`, `git diff --stat HEAD~5..HEAD`, `git branch --show-current`, `git status --short`
18
+ - Present a "Cold Start Briefing" with recent commits, file changes, and uncommitted work
19
+ - End with: "No checkpoint to resume from, but here is the project state from git. What would you like to work on?"
20
+ - Skip remaining steps (4-8)
21
+
11
22
  4. Capture current git state by running these bash commands:
12
23
  - `git branch --show-current`
13
24
  - `git status --short`
@@ -28,6 +39,8 @@ Restore context from last checkpoint at session start.
28
39
  ## Session Resume -- [Project Name] -- [DATE]
29
40
 
30
41
  Resuming from checkpoint: [timestamp]
42
+ Source: [auto/manual] checkpoint
43
+ [Staleness warning if applicable]
31
44
 
32
45
  ### Branch
33
46
  [checkpoint branch] -- currently on [current branch]
package/dist/cli.js CHANGED
@@ -5,8 +5,8 @@ import { fileURLToPath } from "url";
5
5
  import path from "path";
6
6
  import os from "os";
7
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";
8
+ import { initDatabase, getDefaultDbPath, saveDatabase, countAll, listProjects, insertCheckpoint, getLatestCheckpoint, getAllCheckpoints, getAllDailySummaries, deleteProjectData, deleteAllData, } from "./store.js";
9
+ import { ensureDir, normalizeProjectPath } from "./utils.js";
10
10
  import { callClaudeJson } from "./llm.js";
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
12
  function readPromptTemplate(name) {
@@ -33,8 +33,9 @@ export async function handleAutoCheckpoint() {
33
33
  }
34
34
  let transcriptPath;
35
35
  try {
36
- const metadata = JSON.parse(stdinData);
37
- transcriptPath = metadata?.input?.metadata?.transcript_path;
36
+ const hookInput = JSON.parse(stdinData);
37
+ // Claude Code PreCompact hook sends flat JSON with transcript_path at top level
38
+ transcriptPath = hookInput?.transcript_path;
38
39
  }
39
40
  catch {
40
41
  console.error("[claude-baton] Could not parse hook metadata, skipping auto-checkpoint");
@@ -54,15 +55,44 @@ export async function handleAutoCheckpoint() {
54
55
  const branch = gitCmd("git branch --show-current");
55
56
  const status = gitCmd("git status --short");
56
57
  const log = gitCmd("git log --oneline -10");
57
- // Build prompt
58
+ // Initialize DB and fetch previous checkpoint for chaining
59
+ const dbPath = getDefaultDbPath();
60
+ const db = await initDatabase(dbPath);
61
+ const projectPath = normalizeProjectPath(process.cwd());
62
+ const prevCheckpoint = getLatestCheckpoint(db, projectPath);
63
+ // Compute git diff since last checkpoint
64
+ let gitDiffSinceCheckpoint = "";
65
+ if (prevCheckpoint?.git_snapshot) {
66
+ const topCommitHash = prevCheckpoint.git_snapshot
67
+ .split("\n")[0]
68
+ ?.split(" ")[0];
69
+ if (topCommitHash) {
70
+ gitDiffSinceCheckpoint = gitCmd(`git diff --stat ${topCommitHash}..HEAD`);
71
+ }
72
+ }
73
+ // Build previous checkpoint context
74
+ let prevContext = "No previous checkpoint exists for this project.";
75
+ if (prevCheckpoint) {
76
+ prevContext = [
77
+ `What was built: ${prevCheckpoint.what_was_built}`,
78
+ `Current state: ${prevCheckpoint.current_state}`,
79
+ `Next steps: ${prevCheckpoint.next_steps}`,
80
+ prevCheckpoint.decisions_made
81
+ ? `Decisions: ${prevCheckpoint.decisions_made}`
82
+ : null,
83
+ ]
84
+ .filter(Boolean)
85
+ .join("\n");
86
+ }
87
+ // Build prompt with three sections
58
88
  const template = readPromptTemplate("auto_checkpoint.txt");
59
- const prompt = template.replace("{TRANSCRIPT}", transcript);
89
+ const prompt = template
90
+ .replace("{{PREVIOUS_CHECKPOINT}}", prevContext)
91
+ .replace("{{GIT_DIFF}}", gitDiffSinceCheckpoint || "No file changes since last checkpoint.")
92
+ .replace("{{TRANSCRIPT}}", transcript);
60
93
  // Call LLM
61
- const result = await callClaudeJson(prompt, "haiku", 30000);
94
+ const result = await callClaudeJson(prompt, "sonnet", 60000);
62
95
  // Save checkpoint
63
- const dbPath = getDefaultDbPath();
64
- const db = await initDatabase(dbPath);
65
- const projectPath = process.cwd();
66
96
  const sessionId = new Date().toISOString();
67
97
  const uncommittedFiles = status
68
98
  ? status.split("\n").map((l) => l.trim())
@@ -74,6 +104,7 @@ export async function handleAutoCheckpoint() {
74
104
  uncommittedFiles,
75
105
  gitSnapshot: log || undefined,
76
106
  planReference: result.plan_reference || undefined,
107
+ source: "auto",
77
108
  }, dbPath);
78
109
  console.error("[claude-baton] Auto-checkpoint saved before compaction");
79
110
  }
@@ -94,29 +125,76 @@ export async function handleSetup() {
94
125
  settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
95
126
  }
96
127
  catch {
97
- settings = {};
128
+ console.error("Error: could not parse ~/.claude/settings.json. Fix the file manually and re-run setup.");
129
+ return;
130
+ }
131
+ }
132
+ // Register MCP server via `claude mcp add` (the correct way for Claude Code
133
+ // to discover servers). Uses --scope user for cross-project availability.
134
+ const serverScript = path.resolve(__dirname, "..", "bin", "claude-baton.js");
135
+ try {
136
+ // Remove first (idempotent) then add — avoids "already exists" errors
137
+ execSync("claude mcp remove claude-baton -s user 2>/dev/null || true", {
138
+ encoding: "utf-8",
139
+ timeout: 10000,
140
+ });
141
+ execSync(`claude mcp add -s user claude-baton -- node ${serverScript} serve`, { encoding: "utf-8", timeout: 10000 });
142
+ console.error(" Registered MCP server (user scope)");
143
+ }
144
+ catch {
145
+ console.error(" Warning: could not register MCP server via 'claude mcp add'.");
146
+ console.error(" Ensure Claude Code CLI is installed. You can manually run:");
147
+ console.error(` claude mcp add -s user claude-baton -- node ${serverScript} serve`);
148
+ }
149
+ // Clean up legacy mcpServers from settings.json if present
150
+ if (settings.mcpServers &&
151
+ typeof settings.mcpServers === "object" &&
152
+ settings.mcpServers["claude-baton"]) {
153
+ delete settings.mcpServers["claude-baton"];
154
+ if (Object.keys(settings.mcpServers).length === 0) {
155
+ delete settings.mcpServers;
98
156
  }
99
157
  }
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
158
  // Register PreCompact hook (idempotent — skip if already present)
108
159
  const hooks = (settings.hooks ?? {});
109
160
  const preCompactHooks = (hooks.PreCompact ?? []);
110
- const hasMemoriaHook = preCompactHooks.some((h) => h.command && h.command.includes("claude-baton"));
111
- if (!hasMemoriaHook) {
161
+ const hasBatonHook = preCompactHooks.some((h) => Array.isArray(h.hooks) &&
162
+ h.hooks.some((hook) => hook.command?.includes("claude-baton")));
163
+ if (!hasBatonHook) {
164
+ const autoCheckpointBin = path.resolve(__dirname, "..", "bin", "claude-baton.js");
112
165
  preCompactHooks.push({
113
- type: "command",
114
- command: "npx -y claude-baton auto-checkpoint",
166
+ matcher: "",
167
+ hooks: [
168
+ {
169
+ type: "command",
170
+ command: `node ${autoCheckpointBin} auto-checkpoint`,
171
+ },
172
+ ],
115
173
  });
116
174
  hooks.PreCompact = preCompactHooks;
117
175
  settings.hooks = hooks;
118
176
  console.error(" Registered PreCompact hook");
119
177
  }
178
+ // Register allowed tools for frictionless slash commands (idempotent)
179
+ const BATON_TOOLS = [
180
+ "Bash(git status*)",
181
+ "Bash(git log*)",
182
+ "Bash(git diff*)",
183
+ "Bash(git branch*)",
184
+ "Bash(node *claude-baton*)",
185
+ ];
186
+ const allowedTools = (settings.allowedTools ?? []);
187
+ let toolsAdded = 0;
188
+ for (const tool of BATON_TOOLS) {
189
+ if (!allowedTools.includes(tool)) {
190
+ allowedTools.push(tool);
191
+ toolsAdded++;
192
+ }
193
+ }
194
+ if (toolsAdded > 0) {
195
+ settings.allowedTools = allowedTools;
196
+ console.error(` Registered ${toolsAdded} allowed tools`);
197
+ }
120
198
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
121
199
  const dbPath = getDefaultDbPath();
122
200
  await initDatabase(dbPath);
@@ -152,12 +230,22 @@ export function installCommands() {
152
230
  }
153
231
  // --- Uninstall command ---
154
232
  export async function handleUninstall(opts) {
155
- // 1. Remove MCP server from settings.json
233
+ // 1. Remove MCP server via claude CLI + clean up settings.json
234
+ try {
235
+ execSync("claude mcp remove claude-baton -s user 2>/dev/null || true", {
236
+ encoding: "utf-8",
237
+ timeout: 10000,
238
+ });
239
+ console.error(" Removed MCP server");
240
+ }
241
+ catch {
242
+ console.error(" Warning: could not remove MCP server via CLI");
243
+ }
156
244
  const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
157
245
  if (existsSync(settingsPath)) {
158
246
  try {
159
247
  const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
160
- // Remove MCP server registration
248
+ // Remove legacy MCP server from settings.json if present
161
249
  if (settings.mcpServers &&
162
250
  typeof settings.mcpServers === "object" &&
163
251
  settings.mcpServers["claude-baton"]) {
@@ -165,7 +253,6 @@ export async function handleUninstall(opts) {
165
253
  if (Object.keys(settings.mcpServers).length === 0) {
166
254
  delete settings.mcpServers;
167
255
  }
168
- console.error(" Removed MCP server from settings.json");
169
256
  }
170
257
  // Remove PreCompact hook
171
258
  if (settings.hooks &&
@@ -173,7 +260,8 @@ export async function handleUninstall(opts) {
173
260
  settings.hooks.PreCompact) {
174
261
  const hooksObj = settings.hooks;
175
262
  const preCompact = hooksObj.PreCompact;
176
- const filtered = preCompact.filter((h) => !h.command || !h.command.includes("claude-baton"));
263
+ const filtered = preCompact.filter((h) => !Array.isArray(h.hooks) ||
264
+ !h.hooks.some((hook) => hook.command?.includes("claude-baton")));
177
265
  if (filtered.length === 0) {
178
266
  delete hooksObj.PreCompact;
179
267
  }
@@ -185,6 +273,21 @@ export async function handleUninstall(opts) {
185
273
  }
186
274
  console.error(" Removed PreCompact hook");
187
275
  }
276
+ // Remove allowed tools
277
+ if (Array.isArray(settings.allowedTools)) {
278
+ const BATON_PATTERNS = [
279
+ "Bash(git status*)",
280
+ "Bash(git log*)",
281
+ "Bash(git diff*)",
282
+ "Bash(git branch*)",
283
+ "Bash(node *claude-baton*)",
284
+ ];
285
+ settings.allowedTools = settings.allowedTools.filter((t) => !BATON_PATTERNS.includes(t));
286
+ if (settings.allowedTools.length === 0) {
287
+ delete settings.allowedTools;
288
+ }
289
+ console.error(" Removed allowed tools");
290
+ }
188
291
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
189
292
  }
190
293
  catch {
@@ -229,7 +332,7 @@ export async function handleStatus(opts) {
229
332
  return;
230
333
  }
231
334
  const db = await initDatabase(dbPath);
232
- const projectPath = opts.project ?? process.cwd();
335
+ const projectPath = opts.project ?? normalizeProjectPath(process.cwd());
233
336
  const counts = countAll(db, projectPath);
234
337
  const dbSize = statSync(dbPath).size;
235
338
  console.log(`Project: ${projectPath}`);
@@ -289,8 +392,17 @@ export async function handleImport(file) {
289
392
  return;
290
393
  }
291
394
  let imported = 0;
395
+ let skipped = 0;
292
396
  if (Array.isArray(data.checkpoints)) {
293
397
  for (const cp of data.checkpoints) {
398
+ if (!cp.project_path ||
399
+ !cp.session_id ||
400
+ !cp.current_state ||
401
+ !cp.what_was_built ||
402
+ !cp.next_steps) {
403
+ skipped++;
404
+ continue;
405
+ }
294
406
  insertCheckpoint(db, cp.project_path, cp.session_id, cp.current_state, cp.what_was_built, cp.next_steps, {
295
407
  branch: cp.branch,
296
408
  decisionsMade: cp.decisions_made,
@@ -302,7 +414,7 @@ export async function handleImport(file) {
302
414
  }
303
415
  }
304
416
  saveDatabase(db, dbPath);
305
- console.error(`Imported ${imported} items.`);
417
+ console.error(`Imported ${imported} items.${skipped ? ` Skipped ${skipped} malformed.` : ""}`);
306
418
  }
307
419
  // --- Reset command ---
308
420
  export async function handleReset(opts) {
package/dist/index.js CHANGED
@@ -3,10 +3,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { initDatabase, getDefaultDbPath, insertCheckpoint, getLatestCheckpoint, getCheckpoint, getCheckpointsByDate, insertDailySummary, } from "./store.js";
5
5
  import { callClaudeJson } from "./llm.js";
6
+ import { normalizeProjectPath } from "./utils.js";
6
7
  import { readFileSync, statSync } from "fs";
7
8
  import path from "path";
8
9
  import { fileURLToPath } from "url";
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"));
10
12
  let db;
11
13
  let dbPath;
12
14
  let lastDbMtime = 0;
@@ -25,13 +27,6 @@ async function reloadDbIfChanged() {
25
27
  }
26
28
  /** Stable fallback session ID — generated once at module load, not per call. */
27
29
  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
30
  /**
36
31
  * Validate that a required string argument is present and is a string.
37
32
  * Returns the validated string, or throws with a descriptive message.
@@ -50,7 +45,7 @@ class ValidationError extends Error {
50
45
  this.name = "ValidationError";
51
46
  }
52
47
  }
53
- const server = new Server({ name: "claude-baton", version: "1.0.0" }, { capabilities: { tools: {} } });
48
+ const server = new Server({ name: "claude-baton", version: pkg.version }, { capabilities: { tools: {} } });
54
49
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
55
50
  tools: [
56
51
  {
@@ -81,6 +76,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
81
76
  type: "string",
82
77
  description: "Reference to active plan document and section, e.g. 'docs/plan.md Phase 2 Step 3'",
83
78
  },
79
+ source: {
80
+ type: "string",
81
+ enum: ["manual", "auto"],
82
+ description: "Checkpoint source. Defaults to manual.",
83
+ },
84
84
  project: {
85
85
  type: "string",
86
86
  description: "Project path (defaults to cwd)",
@@ -144,7 +144,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
144
144
  }));
145
145
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
146
146
  const { name, arguments: args } = request.params;
147
- const projectPath = args?.project ?? process.cwd();
147
+ const projectPath = args?.project ?? normalizeProjectPath(process.cwd());
148
148
  await reloadDbIfChanged();
149
149
  try {
150
150
  switch (name) {
@@ -161,6 +161,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
161
161
  uncommittedFiles: a?.uncommitted_files,
162
162
  gitSnapshot: a?.git_snapshot,
163
163
  planReference: a?.plan_reference,
164
+ source: a?.source ?? "manual",
164
165
  }, dbPath);
165
166
  return { content: [{ type: "text", text: `Checkpoint saved: ${id}` }] };
166
167
  }
@@ -206,7 +207,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
206
207
  const summaryPrompt = summaryTemplate
207
208
  .replace("{{DATE}}", date)
208
209
  .replace("{{ACTIVITY}}", activityParts.join("\n\n"));
209
- const summaryResult = await callClaudeJson(summaryPrompt, "haiku", 30000);
210
+ const summaryResult = await callClaudeJson(summaryPrompt, "sonnet", 60000);
210
211
  insertDailySummary(db, projectPath, date, summaryResult, dbPath);
211
212
  return {
212
213
  content: [
package/dist/llm.js CHANGED
@@ -49,7 +49,7 @@ function callClaudeRaw(prompt, model = "haiku", timeout = 30000) {
49
49
  if (settled)
50
50
  return;
51
51
  settled = true;
52
- if (process.env.MEMORIA_DEBUG) {
52
+ if (process.env.CLAUDE_BATON_DEBUG) {
53
53
  console.error(`[DEBUG] claude -p exit=${code} stdout=${stdout.slice(0, 500)} stderr=${stderr.slice(0, 200)}`);
54
54
  }
55
55
  if (code !== 0) {
@@ -65,7 +65,7 @@ function callClaudeRaw(prompt, model = "haiku", timeout = 30000) {
65
65
  settled = true;
66
66
  reject(new Error(`Failed to spawn claude: ${err.message}`));
67
67
  });
68
- if (process.env.MEMORIA_DEBUG) {
68
+ if (process.env.CLAUDE_BATON_DEBUG) {
69
69
  console.error(`[DEBUG] Prompt length: ${prompt.length}, first 300 chars: ${prompt.slice(0, 300)}`);
70
70
  }
71
71
  proc.stdin.write(prompt);
package/dist/store.d.ts CHANGED
@@ -11,6 +11,7 @@ export declare function insertCheckpoint(db: Database, projectPath: string, sess
11
11
  uncommittedFiles?: string[];
12
12
  gitSnapshot?: string;
13
13
  planReference?: string;
14
+ source?: "manual" | "auto";
14
15
  }, dbPath?: string): string;
15
16
  export declare function getCheckpoint(db: Database, id: string): Checkpoint | null;
16
17
  export declare function getLatestCheckpoint(db: Database, projectPath: string): Checkpoint | null;
package/dist/store.js CHANGED
@@ -45,6 +45,7 @@ export function initSchema(db) {
45
45
  uncommitted_files TEXT DEFAULT '[]',
46
46
  git_snapshot TEXT,
47
47
  plan_reference TEXT,
48
+ source TEXT DEFAULT 'manual',
48
49
  created_at TEXT DEFAULT (datetime('now'))
49
50
  );
50
51
  CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project_path);
@@ -64,22 +65,35 @@ export function initSchema(db) {
64
65
  try {
65
66
  db.exec("ALTER TABLE checkpoints ADD COLUMN git_snapshot TEXT");
66
67
  }
67
- catch {
68
- // Column already exists expected for new databases or already-migrated ones
68
+ catch (e) {
69
+ const msg = e instanceof Error ? e.message : "";
70
+ if (!msg.includes("duplicate column"))
71
+ throw e;
69
72
  }
70
73
  // Migration: add plan_reference column for existing databases
71
74
  try {
72
75
  db.exec("ALTER TABLE checkpoints ADD COLUMN plan_reference TEXT");
73
76
  }
74
- catch {
75
- // Column already exists
77
+ catch (e) {
78
+ const msg = e instanceof Error ? e.message : "";
79
+ if (!msg.includes("duplicate column"))
80
+ throw e;
81
+ }
82
+ // Migration: add source column for existing databases
83
+ try {
84
+ db.exec("ALTER TABLE checkpoints ADD COLUMN source TEXT DEFAULT 'manual'");
85
+ }
86
+ catch (e) {
87
+ const msg = e instanceof Error ? e.message : "";
88
+ if (!msg.includes("duplicate column"))
89
+ throw e;
76
90
  }
77
91
  }
78
92
  // --- Checkpoints CRUD ---
79
93
  export function insertCheckpoint(db, projectPath, sessionId, currentState, whatWasBuilt, nextSteps, opts, dbPath) {
80
94
  const id = crypto.randomUUID();
81
- db.run(`INSERT INTO checkpoints (id, project_path, session_id, branch, current_state, what_was_built, next_steps, decisions_made, blockers, uncommitted_files, git_snapshot, plan_reference, created_at)
82
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
95
+ db.run(`INSERT INTO checkpoints (id, project_path, session_id, branch, current_state, what_was_built, next_steps, decisions_made, blockers, uncommitted_files, git_snapshot, plan_reference, source, created_at)
96
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
83
97
  id,
84
98
  projectPath,
85
99
  sessionId,
@@ -92,6 +106,7 @@ export function insertCheckpoint(db, projectPath, sessionId, currentState, whatW
92
106
  JSON.stringify(opts?.uncommittedFiles ?? []),
93
107
  opts?.gitSnapshot ?? null,
94
108
  opts?.planReference ?? null,
109
+ opts?.source ?? "manual",
95
110
  new Date().toISOString(),
96
111
  ]);
97
112
  if (dbPath)
@@ -122,11 +137,16 @@ function parseCheckpointRow(row) {
122
137
  uncommitted_files: JSON.parse(row.uncommitted_files),
123
138
  git_snapshot: row.git_snapshot ?? null,
124
139
  plan_reference: row.plan_reference ?? null,
140
+ source: row.source ?? "manual",
125
141
  };
126
142
  }
127
143
  export function getCheckpointsByDate(db, projectPath, date) {
128
- const stmt = db.prepare("SELECT * FROM checkpoints WHERE project_path = ? AND created_at LIKE ? || '%' ORDER BY created_at ASC");
129
- stmt.bind([projectPath, date]);
144
+ const startLocal = new Date(`${date}T00:00:00`);
145
+ const endLocal = new Date(`${date}T23:59:59.999`);
146
+ const startUtc = startLocal.toISOString();
147
+ const endUtc = endLocal.toISOString();
148
+ const stmt = db.prepare("SELECT * FROM checkpoints WHERE project_path = ? AND created_at >= ? AND created_at <= ? ORDER BY created_at ASC");
149
+ stmt.bind([projectPath, startUtc, endUtc]);
130
150
  const results = [];
131
151
  while (stmt.step()) {
132
152
  results.push(parseCheckpointRow(stmt.getAsObject()));
package/dist/types.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface Checkpoint {
12
12
  uncommitted_files: string[];
13
13
  git_snapshot: string | null;
14
14
  plan_reference: string | null;
15
+ source: "manual" | "auto";
15
16
  created_at: string;
16
17
  }
17
18
  /** An aggregated end-of-day summary of project activity. */
package/dist/utils.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export declare function getProjectPath(): string;
2
+ export declare function normalizeProjectPath(p: string): string;
2
3
  export declare function ensureDir(dirPath: string): void;
package/dist/utils.js CHANGED
@@ -1,10 +1,23 @@
1
- import { mkdirSync, existsSync } from "fs";
1
+ import { mkdirSync, realpathSync } from "fs";
2
+ import path from "path";
2
3
  // --- Path helpers ---
3
4
  export function getProjectPath() {
4
5
  return process.cwd();
5
6
  }
6
- export function ensureDir(dirPath) {
7
- if (!existsSync(dirPath)) {
8
- mkdirSync(dirPath, { recursive: true });
7
+ export function normalizeProjectPath(p) {
8
+ try {
9
+ const resolved = realpathSync(p);
10
+ return resolved.length > 1 && resolved.endsWith(path.sep)
11
+ ? resolved.slice(0, -1)
12
+ : resolved;
13
+ }
14
+ catch {
15
+ const normalized = path.resolve(p);
16
+ return normalized.length > 1 && normalized.endsWith(path.sep)
17
+ ? normalized.slice(0, -1)
18
+ : normalized;
9
19
  }
10
20
  }
21
+ export function ensureDir(dirPath) {
22
+ mkdirSync(dirPath, { recursive: true });
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-baton",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "Session lifecycle management for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,18 +1,35 @@
1
1
  You are extracting a session checkpoint from a Claude Code conversation transcript.
2
2
 
3
- Analyze the transcript and produce a JSON object with these fields:
3
+ You have three sources of context:
4
4
 
5
- - what_was_built: What was accomplished in this session (1-3 sentences)
5
+ PREVIOUS CHECKPOINT:
6
+ {{PREVIOUS_CHECKPOINT}}
7
+
8
+ GIT CHANGES SINCE LAST CHECKPOINT:
9
+ {{GIT_DIFF}}
10
+
11
+ CURRENT TRANSCRIPT:
12
+ {{TRANSCRIPT}}
13
+
14
+ Analyze these inputs and produce a JSON object with these fields:
15
+
16
+ - what_was_built: What was accomplished since the previous checkpoint (1-3 sentences). Focus on what CHANGED — don't repeat already-captured work.
6
17
  - current_state: Current state of the project — what's passing, what's broken, what's uncommitted (1-2 sentences)
7
18
  - next_steps: What should be done next (1-2 sentences, actionable)
8
- - decisions_made: Key decisions made during this session, if any (1-2 sentences, or "None")
19
+ - decisions_made: Key decisions made since the previous checkpoint, if any (1-2 sentences, or "None")
9
20
  - blockers: Anything blocking progress, if any (1 sentence, or "None")
10
21
  - plan_reference: If the session references an active plan document (e.g. PLAN.md, docs/plan.md, a spec file), include the file path and the specific section being worked on (e.g. "docs/v2-plan.md Phase 2 Step 3"). Null if no plan document is referenced.
11
22
 
12
23
  Rules:
13
24
  - Be concise — each field should be 1-3 sentences max
14
- - Focus on what changed, not what was discussed
15
- - next_steps should be specific enough to resume work
25
+ - Focus on what was COMPLETED in THIS session's transcript, not what was discussed or explored
26
+ - Don't repeat work already captured in the previous checkpoint
27
+ - If the transcript is mostly noise (exploration, context-filling, debugging with no resolution), be honest: "Explored X and Y but no code changes yet"
28
+ - Note file changes from git diff that aren't explained in the transcript
29
+ - next_steps should be the logical next action based on THIS session's work, not inherited from the previous checkpoint
30
+ - blockers should only include blockers that were ACTIVELY ENCOUNTERED in this session. Do NOT carry forward blockers from the previous checkpoint unless the session transcript shows they were hit. If nothing blocked progress, say "None"
31
+ - current_state should lead with what IS working (e.g. "All tests passing, build clean") before mentioning what's not done
32
+ - what_was_built should name specific features, not just describe generic "schema changes" — use the same terminology the transcript uses
16
33
  - For plan_reference, look for file reads/references to plan docs, spec files, or roadmap documents in the transcript
17
34
  - Output ONLY valid JSON, no markdown, no explanation
18
35
 
@@ -24,7 +41,4 @@ Example output:
24
41
  "decisions_made": "Using bcrypt over argon2 for password hashing due to simpler deployment. Storing refresh tokens in httpOnly cookies.",
25
42
  "blockers": "None",
26
43
  "plan_reference": "docs/auth-plan.md Phase 2 Step 1"
27
- }
28
-
29
- Transcript:
30
- {TRANSCRIPT}
44
+ }
@@ -15,7 +15,7 @@ Instructions:
15
15
  - Do not fabricate information not present in the activity data
16
16
  - If the activity data is sparse, reflect that honestly rather than padding
17
17
 
18
- Respond with ONLY a valid JSON object matching this shape:
18
+ Respond with ONLY a valid JSON object matching this shape — no code fences, no text before or after. Your entire response must be parseable by JSON.parse().
19
19
 
20
20
  {"what_was_built": "...", "decisions_made": ["..."], "blockers": ["..."], "next_steps": ["..."], "highlights": "..."}
21
21
 
@@ -25,5 +25,3 @@ Field rules:
25
25
  - blockers: array of unresolved issues; use [] if none
26
26
  - next_steps: array of actionable items for the next session
27
27
  - highlights: one-line summary suitable for a changelog or standup
28
-
29
- Respond with ONLY valid JSON. Do not wrap in code fences. Do not include any text before or after the JSON object. Your entire response must be parseable by JSON.parse().