baton-cli 0.1.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 +137 -0
- package/dist/cli.js +693 -0
- package/dist/cli.js.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Baton
|
|
2
|
+
|
|
3
|
+
**Baton** is a session handoff tool for Claude Code across machines.
|
|
4
|
+
|
|
5
|
+
It solves one problem:
|
|
6
|
+
|
|
7
|
+
> Can I continue the same Claude Code session on another machine without losing context?
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
baton push # on machine A
|
|
11
|
+
baton pull # on machine B
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why it exists
|
|
17
|
+
|
|
18
|
+
Claude Code sessions are trapped on one machine:
|
|
19
|
+
|
|
20
|
+
- session context doesn't travel between devices
|
|
21
|
+
- macOS, Linux, and Windows use different paths, usernames, and home directories
|
|
22
|
+
- the same repo often lives at different absolute paths on different machines
|
|
23
|
+
- existing tools sync config, not coding sessions
|
|
24
|
+
|
|
25
|
+
Baton does not try to replace the agent itself.
|
|
26
|
+
|
|
27
|
+
It focuses on one job:
|
|
28
|
+
|
|
29
|
+
**push a session here, pull it there, keep working.**
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## One-line positioning
|
|
34
|
+
|
|
35
|
+
**Git-backed session handoff for Claude Code.**
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
1. You run `baton push` from a project directory
|
|
42
|
+
2. Baton auto-detects the project from the git remote
|
|
43
|
+
3. All Claude Code sessions for that project are captured
|
|
44
|
+
4. Absolute paths are replaced with portable placeholders
|
|
45
|
+
5. The checkpoint is pushed to a private GitHub repo
|
|
46
|
+
6. On another machine, `baton pull` restores the sessions
|
|
47
|
+
7. Placeholders are expanded to machine-local paths
|
|
48
|
+
8. Claude Code can continue where you left off
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
Same repo, different machines:
|
|
55
|
+
|
|
56
|
+
- macOS: `/Users/dr_who/work/foo`
|
|
57
|
+
- Linux: `/root/projects/foo`
|
|
58
|
+
- Windows: `C:\Users\dr_who\work\foo`
|
|
59
|
+
|
|
60
|
+
Baton identifies them as the same project from the git remote:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# on macOS
|
|
64
|
+
cd /Users/dr_who/work/foo
|
|
65
|
+
baton push
|
|
66
|
+
|
|
67
|
+
# on Linux
|
|
68
|
+
cd /root/projects/foo
|
|
69
|
+
baton pull
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Path placeholders handle the differences automatically:
|
|
73
|
+
|
|
74
|
+
| Placeholder | macOS | Linux | Windows |
|
|
75
|
+
|-------------|-------|-------|---------|
|
|
76
|
+
| `${PROJECT_ROOT}` | `/Users/dr_who/work/foo` | `/root/projects/foo` | `C:\Users\dr_who\work\foo` |
|
|
77
|
+
| `${HOME}` | `/Users/dr_who` | `/root` | `C:\Users\dr_who` |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## What gets synced
|
|
82
|
+
|
|
83
|
+
| Component | Synced? | Why |
|
|
84
|
+
|-----------|---------|-----|
|
|
85
|
+
| Session conversation logs (all) | Yes | The sessions themselves |
|
|
86
|
+
| Tool results | Yes | Small, needed for reference integrity |
|
|
87
|
+
| Project memory | Yes | Tiny, valuable for continuity |
|
|
88
|
+
| Subagent logs | No | Too large, results already in main conversation |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## CLI
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
baton push # push all sessions for this project
|
|
96
|
+
baton push --force # overwrite remote even if ahead
|
|
97
|
+
baton pull # restore sessions locally
|
|
98
|
+
baton status # show current project and sync state
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## What Baton is not
|
|
104
|
+
|
|
105
|
+
Baton is **not**:
|
|
106
|
+
|
|
107
|
+
- a real-time sync engine
|
|
108
|
+
- a multi-user collaboration platform
|
|
109
|
+
- a vector database or semantic memory system
|
|
110
|
+
- a config sync tool
|
|
111
|
+
- a full native state restoration guarantee
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Design principles
|
|
116
|
+
|
|
117
|
+
* **Project-aware**: project identity comes from git remote, not local paths
|
|
118
|
+
* **Checkpoint-first**: restore from snapshots, not fragile live mirroring
|
|
119
|
+
* **Portable before native**: prioritize continuity over perfect internal restoration
|
|
120
|
+
* **Git-backed**: use GitHub for durable history and recovery
|
|
121
|
+
* **Simple**: two commands, no daemon, no config ceremony
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Status
|
|
126
|
+
|
|
127
|
+
Early design / MVP planning.
|
|
128
|
+
|
|
129
|
+
v0.1 target:
|
|
130
|
+
|
|
131
|
+
> Push/pull Claude Code sessions across macOS, Linux, and Windows for the same logical project.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/pull.ts
|
|
7
|
+
import { join as join5 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/adapters/claude-code/writer.ts
|
|
10
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
11
|
+
import { dirname, join as join2 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/adapters/claude-code/paths.ts
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
function encodeProjectDir(projectPath) {
|
|
17
|
+
const normalized = projectPath.replace(/\\/g, "/");
|
|
18
|
+
return normalized.replace(/[/.:]/g, "-");
|
|
19
|
+
}
|
|
20
|
+
function getClaudeProjectsDir() {
|
|
21
|
+
return join(homedir(), ".claude", "projects");
|
|
22
|
+
}
|
|
23
|
+
function getProjectConfigPath() {
|
|
24
|
+
return join(homedir(), ".claude", "project-config.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/adapters/claude-code/writer.ts
|
|
28
|
+
async function restoreProjectData(projectPath, data) {
|
|
29
|
+
const projectDirName = encodeProjectDir(projectPath);
|
|
30
|
+
const projectDir = join2(getClaudeProjectsDir(), projectDirName);
|
|
31
|
+
await mkdir(projectDir, { recursive: true });
|
|
32
|
+
for (const session of data.sessions) {
|
|
33
|
+
await writeFile(
|
|
34
|
+
join2(projectDir, `${session.sessionId}.jsonl`),
|
|
35
|
+
session.jsonl,
|
|
36
|
+
"utf-8"
|
|
37
|
+
);
|
|
38
|
+
if (session.toolResults.size > 0) {
|
|
39
|
+
const toolResultsDir = join2(
|
|
40
|
+
projectDir,
|
|
41
|
+
session.sessionId,
|
|
42
|
+
"tool-results"
|
|
43
|
+
);
|
|
44
|
+
await mkdir(toolResultsDir, { recursive: true });
|
|
45
|
+
for (const [filename, content] of session.toolResults) {
|
|
46
|
+
await writeFile(join2(toolResultsDir, filename), content, "utf-8");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (data.memory.size > 0) {
|
|
51
|
+
const memoryDir = join2(projectDir, "memory");
|
|
52
|
+
await mkdir(memoryDir, { recursive: true });
|
|
53
|
+
for (const [filename, content] of data.memory) {
|
|
54
|
+
await writeFile(join2(memoryDir, filename), content, "utf-8");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
await ensureProjectConfig(projectPath, projectDirName);
|
|
58
|
+
}
|
|
59
|
+
async function ensureProjectConfig(projectPath, projectDirName) {
|
|
60
|
+
const configPath = getProjectConfigPath();
|
|
61
|
+
let config = {};
|
|
62
|
+
try {
|
|
63
|
+
const raw = await readFile(configPath, "utf-8");
|
|
64
|
+
config = JSON.parse(raw);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
if (!config[projectDirName]) {
|
|
68
|
+
config[projectDirName] = {
|
|
69
|
+
originalPath: projectPath
|
|
70
|
+
};
|
|
71
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
72
|
+
await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/core/config.ts
|
|
77
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
78
|
+
import { homedir as homedir2 } from "os";
|
|
79
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
80
|
+
function getBatonDir() {
|
|
81
|
+
return join3(homedir2(), ".baton");
|
|
82
|
+
}
|
|
83
|
+
function getConfigPath() {
|
|
84
|
+
return join3(getBatonDir(), "config.json");
|
|
85
|
+
}
|
|
86
|
+
function getRepoDir() {
|
|
87
|
+
return join3(getBatonDir(), "repo");
|
|
88
|
+
}
|
|
89
|
+
async function loadConfig() {
|
|
90
|
+
try {
|
|
91
|
+
const raw = await readFile2(getConfigPath(), "utf-8");
|
|
92
|
+
const config = JSON.parse(raw);
|
|
93
|
+
if (!config.repo || typeof config.repo !== "string") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return config;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function saveConfig(config) {
|
|
102
|
+
const configPath = getConfigPath();
|
|
103
|
+
await mkdir2(dirname2(configPath), { recursive: true });
|
|
104
|
+
await writeFile2(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/core/git.ts
|
|
108
|
+
import { execFile } from "child_process";
|
|
109
|
+
import { access } from "fs/promises";
|
|
110
|
+
import { promisify } from "util";
|
|
111
|
+
|
|
112
|
+
// src/errors.ts
|
|
113
|
+
var BatonError = class extends Error {
|
|
114
|
+
constructor(message) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = this.constructor.name;
|
|
117
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var ProjectNotFoundError = class extends BatonError {
|
|
121
|
+
};
|
|
122
|
+
var NoSessionsError = class extends BatonError {
|
|
123
|
+
};
|
|
124
|
+
var ConflictError = class extends BatonError {
|
|
125
|
+
};
|
|
126
|
+
var GitNotFoundError = class extends BatonError {
|
|
127
|
+
};
|
|
128
|
+
var GhNotFoundError = class extends BatonError {
|
|
129
|
+
};
|
|
130
|
+
var ConfigError = class extends BatonError {
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/core/git.ts
|
|
134
|
+
var execFileAsync = promisify(execFile);
|
|
135
|
+
var GIT_TIMEOUT_MS = 3e4;
|
|
136
|
+
var GH_TIMEOUT_MS = 6e4;
|
|
137
|
+
async function git(args, cwd) {
|
|
138
|
+
try {
|
|
139
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
140
|
+
cwd,
|
|
141
|
+
timeout: GIT_TIMEOUT_MS
|
|
142
|
+
});
|
|
143
|
+
return stdout.trim();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (isNotFound(error)) {
|
|
146
|
+
throw new GitNotFoundError("git is not installed or not found in PATH.");
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function gh(args) {
|
|
152
|
+
try {
|
|
153
|
+
const { stdout } = await execFileAsync("gh", args, {
|
|
154
|
+
timeout: GH_TIMEOUT_MS
|
|
155
|
+
});
|
|
156
|
+
return stdout.trim();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (isNotFound(error)) {
|
|
159
|
+
throw new GhNotFoundError(
|
|
160
|
+
"gh CLI is not installed. Install it from https://cli.github.com/"
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function repoExists(repoDir) {
|
|
167
|
+
try {
|
|
168
|
+
await access(repoDir);
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function cloneRepo(repoUrl, targetDir) {
|
|
175
|
+
await execFileAsync("git", ["clone", repoUrl, targetDir], {
|
|
176
|
+
timeout: GH_TIMEOUT_MS
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function fetchRepo(repoDir) {
|
|
180
|
+
await git(["fetch", "origin"], repoDir);
|
|
181
|
+
}
|
|
182
|
+
async function pullRepo(repoDir) {
|
|
183
|
+
await git(["pull", "--ff-only"], repoDir);
|
|
184
|
+
}
|
|
185
|
+
async function isRemoteAhead(repoDir) {
|
|
186
|
+
await fetchRepo(repoDir);
|
|
187
|
+
const localHead = await git(["rev-parse", "HEAD"], repoDir);
|
|
188
|
+
const remoteHead = await git(["rev-parse", "origin/main"], repoDir);
|
|
189
|
+
return localHead !== remoteHead;
|
|
190
|
+
}
|
|
191
|
+
async function commitAndPush(repoDir, message, force) {
|
|
192
|
+
await git(["add", "-A"], repoDir);
|
|
193
|
+
const status2 = await git(["status", "--porcelain"], repoDir);
|
|
194
|
+
if (!status2) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await git(["commit", "-m", message], repoDir);
|
|
198
|
+
const pushArgs = force ? ["push", "--force", "origin", "main"] : ["push", "origin", "main"];
|
|
199
|
+
await git(pushArgs, repoDir);
|
|
200
|
+
}
|
|
201
|
+
async function createGhRepo(repoName) {
|
|
202
|
+
const output = await gh([
|
|
203
|
+
"repo",
|
|
204
|
+
"create",
|
|
205
|
+
repoName,
|
|
206
|
+
"--private",
|
|
207
|
+
"--confirm"
|
|
208
|
+
]);
|
|
209
|
+
const match = output.match(/https:\/\/github\.com\/[\w.-]+\/[\w.-]+/);
|
|
210
|
+
if (match) {
|
|
211
|
+
return match[0];
|
|
212
|
+
}
|
|
213
|
+
const username = await gh(["api", "user", "--jq", ".login"]);
|
|
214
|
+
return `https://github.com/${username}/${repoName}`;
|
|
215
|
+
}
|
|
216
|
+
function isNotFound(error) {
|
|
217
|
+
if (!(error instanceof Error)) return false;
|
|
218
|
+
return error.code === "ENOENT";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/core/paths.ts
|
|
222
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
223
|
+
import { sep } from "path";
|
|
224
|
+
var PLACEHOLDER_PROJECT_ROOT = "${PROJECT_ROOT}";
|
|
225
|
+
var PLACEHOLDER_HOME = "${HOME}";
|
|
226
|
+
var PLACEHOLDER_TMP = "${TMP}";
|
|
227
|
+
var PLACEHOLDERS = {
|
|
228
|
+
PROJECT_ROOT: PLACEHOLDER_PROJECT_ROOT,
|
|
229
|
+
HOME: PLACEHOLDER_HOME,
|
|
230
|
+
TMP: PLACEHOLDER_TMP
|
|
231
|
+
};
|
|
232
|
+
function getLocalPathContext(projectRoot) {
|
|
233
|
+
return {
|
|
234
|
+
projectRoot,
|
|
235
|
+
home: homedir3(),
|
|
236
|
+
tmp: tmpdir()
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function virtualizePaths(content, ctx) {
|
|
240
|
+
const normalizedContent = sep === "\\" ? content.replace(/\\/g, "/") : content;
|
|
241
|
+
const replacements = [
|
|
242
|
+
[normalizeSeparators(ctx.projectRoot), PLACEHOLDERS.PROJECT_ROOT],
|
|
243
|
+
[normalizeSeparators(ctx.home), PLACEHOLDERS.HOME],
|
|
244
|
+
[normalizeSeparators(ctx.tmp), PLACEHOLDERS.TMP]
|
|
245
|
+
].sort((a, b) => b[0].length - a[0].length);
|
|
246
|
+
let result = normalizedContent;
|
|
247
|
+
for (const [path, placeholder] of replacements) {
|
|
248
|
+
if (path) {
|
|
249
|
+
result = replacePathWithBoundary(result, path, placeholder);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
function expandPaths(content, ctx) {
|
|
255
|
+
let result = content;
|
|
256
|
+
result = replaceAll(
|
|
257
|
+
result,
|
|
258
|
+
PLACEHOLDERS.PROJECT_ROOT,
|
|
259
|
+
toNativePath(ctx.projectRoot)
|
|
260
|
+
);
|
|
261
|
+
result = replaceAll(result, PLACEHOLDERS.HOME, toNativePath(ctx.home));
|
|
262
|
+
result = replaceAll(result, PLACEHOLDERS.TMP, toNativePath(ctx.tmp));
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
function normalizeSeparators(path) {
|
|
266
|
+
return path.replace(/\\/g, "/");
|
|
267
|
+
}
|
|
268
|
+
function toNativePath(path) {
|
|
269
|
+
if (sep === "\\") {
|
|
270
|
+
return path.replace(/\//g, "\\");
|
|
271
|
+
}
|
|
272
|
+
return path;
|
|
273
|
+
}
|
|
274
|
+
function replacePathWithBoundary(str, path, replacement) {
|
|
275
|
+
const escaped = escapeRegex(path);
|
|
276
|
+
const regex = new RegExp(`${escaped}(?=[/\\\\"\\s,}\\]]|$)`, "g");
|
|
277
|
+
return str.replace(regex, replacement);
|
|
278
|
+
}
|
|
279
|
+
function replaceAll(str, search, replacement) {
|
|
280
|
+
return str.split(search).join(replacement);
|
|
281
|
+
}
|
|
282
|
+
function escapeRegex(str) {
|
|
283
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/core/project.ts
|
|
287
|
+
import { execFile as execFile2 } from "child_process";
|
|
288
|
+
import { createHash } from "crypto";
|
|
289
|
+
import { promisify as promisify2 } from "util";
|
|
290
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
291
|
+
var GIT_TIMEOUT_MS2 = 1e4;
|
|
292
|
+
async function getGitRemote(cwd) {
|
|
293
|
+
try {
|
|
294
|
+
const { stdout } = await execFileAsync2(
|
|
295
|
+
"git",
|
|
296
|
+
["remote", "get-url", "origin"],
|
|
297
|
+
{ cwd, timeout: GIT_TIMEOUT_MS2 }
|
|
298
|
+
);
|
|
299
|
+
const remote = stdout.trim();
|
|
300
|
+
if (!remote) {
|
|
301
|
+
throw new ProjectNotFoundError(
|
|
302
|
+
"No git remote URL found. Run this command from a git repository with an 'origin' remote."
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return remote;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error instanceof ProjectNotFoundError) throw error;
|
|
308
|
+
if (isGitNotInstalled(error)) {
|
|
309
|
+
throw new GitNotFoundError(
|
|
310
|
+
"git is not installed or not found in PATH. Please install git first."
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
throw new ProjectNotFoundError(
|
|
314
|
+
"Not a git repository or no 'origin' remote configured. Run this command from a git repository."
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function isGitNotInstalled(error) {
|
|
319
|
+
if (!(error instanceof Error)) return false;
|
|
320
|
+
const err = error;
|
|
321
|
+
return err.code === "ENOENT";
|
|
322
|
+
}
|
|
323
|
+
function normalizeGitRemote(remote) {
|
|
324
|
+
const normalized = remote.trim();
|
|
325
|
+
const sshMatch = normalized.match(/^[\w.-]+@([\w.-]+):(.*?)(?:\.git)?$/);
|
|
326
|
+
if (sshMatch) {
|
|
327
|
+
return `${sshMatch[1]}/${sshMatch[2]}`;
|
|
328
|
+
}
|
|
329
|
+
const protoMatch = normalized.match(
|
|
330
|
+
/^(?:ssh|git):\/\/(?:[\w.-]+@)?([\w.-]+)\/(.*?)(?:\.git)?$/
|
|
331
|
+
);
|
|
332
|
+
if (protoMatch) {
|
|
333
|
+
return `${protoMatch[1]}/${protoMatch[2]}`;
|
|
334
|
+
}
|
|
335
|
+
const httpsMatch = normalized.match(
|
|
336
|
+
/^https?:\/\/([\w.-]+)\/(.*?)(?:\.git)?$/
|
|
337
|
+
);
|
|
338
|
+
if (httpsMatch) {
|
|
339
|
+
return `${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
340
|
+
}
|
|
341
|
+
return normalized;
|
|
342
|
+
}
|
|
343
|
+
function hashProjectId(normalizedRemote) {
|
|
344
|
+
return createHash("sha256").update(normalizedRemote).digest("hex").slice(0, 16);
|
|
345
|
+
}
|
|
346
|
+
async function detectProject(cwd) {
|
|
347
|
+
const gitRemote = await getGitRemote(cwd);
|
|
348
|
+
const normalizedRemote = normalizeGitRemote(gitRemote);
|
|
349
|
+
const projectId = hashProjectId(normalizedRemote);
|
|
350
|
+
return { projectId, gitRemote, normalizedRemote };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/commands/checkpoint.ts
|
|
354
|
+
import { mkdir as mkdir3, readdir, readFile as readFile3, rm, writeFile as writeFile3 } from "fs/promises";
|
|
355
|
+
import { join as join4 } from "path";
|
|
356
|
+
async function writeCheckpoint(projectDir, project, data) {
|
|
357
|
+
const sessionsDir = join4(projectDir, "sessions");
|
|
358
|
+
await rm(sessionsDir, { recursive: true, force: true });
|
|
359
|
+
await mkdir3(sessionsDir, { recursive: true });
|
|
360
|
+
const meta = {
|
|
361
|
+
project_id: project.projectId,
|
|
362
|
+
git_remote: project.gitRemote,
|
|
363
|
+
pushed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
364
|
+
};
|
|
365
|
+
await writeFile3(
|
|
366
|
+
join4(projectDir, "meta.json"),
|
|
367
|
+
JSON.stringify(meta, null, 2),
|
|
368
|
+
"utf-8"
|
|
369
|
+
);
|
|
370
|
+
for (const session of data.sessions) {
|
|
371
|
+
await writeFile3(
|
|
372
|
+
join4(sessionsDir, `${session.sessionId}.jsonl`),
|
|
373
|
+
session.jsonl,
|
|
374
|
+
"utf-8"
|
|
375
|
+
);
|
|
376
|
+
if (session.toolResults.size > 0) {
|
|
377
|
+
const toolResultsDir = join4(
|
|
378
|
+
sessionsDir,
|
|
379
|
+
session.sessionId,
|
|
380
|
+
"tool-results"
|
|
381
|
+
);
|
|
382
|
+
await mkdir3(toolResultsDir, { recursive: true });
|
|
383
|
+
for (const [filename, content] of session.toolResults) {
|
|
384
|
+
await writeFile3(join4(toolResultsDir, filename), content, "utf-8");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (data.memory.size > 0) {
|
|
389
|
+
const memoryDir = join4(projectDir, "memory");
|
|
390
|
+
await mkdir3(memoryDir, { recursive: true });
|
|
391
|
+
for (const [filename, content] of data.memory) {
|
|
392
|
+
await writeFile3(join4(memoryDir, filename), content, "utf-8");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async function readCheckpoint(projectDir) {
|
|
397
|
+
const sessionsDir = join4(projectDir, "sessions");
|
|
398
|
+
let sessionFiles;
|
|
399
|
+
try {
|
|
400
|
+
const entries = await readdir(sessionsDir);
|
|
401
|
+
sessionFiles = entries.filter((e) => e.endsWith(".jsonl"));
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
if (sessionFiles.length === 0) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const sessions = [];
|
|
409
|
+
for (const file of sessionFiles) {
|
|
410
|
+
const sessionId = file.replace(".jsonl", "");
|
|
411
|
+
const jsonl = await readFile3(join4(sessionsDir, file), "utf-8");
|
|
412
|
+
const toolResults = await readToolResults(sessionsDir, sessionId);
|
|
413
|
+
sessions.push({ sessionId, jsonl, toolResults });
|
|
414
|
+
}
|
|
415
|
+
const memory = await readMemory(projectDir);
|
|
416
|
+
return {
|
|
417
|
+
sessions,
|
|
418
|
+
memory,
|
|
419
|
+
projectDirName: ""
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
async function readToolResults(sessionsDir, sessionId) {
|
|
423
|
+
const results = /* @__PURE__ */ new Map();
|
|
424
|
+
const toolResultsDir = join4(sessionsDir, sessionId, "tool-results");
|
|
425
|
+
let entries;
|
|
426
|
+
try {
|
|
427
|
+
entries = await readdir(toolResultsDir);
|
|
428
|
+
} catch {
|
|
429
|
+
return results;
|
|
430
|
+
}
|
|
431
|
+
for (const entry of entries) {
|
|
432
|
+
const content = await readFile3(join4(toolResultsDir, entry), "utf-8");
|
|
433
|
+
results.set(entry, content);
|
|
434
|
+
}
|
|
435
|
+
return results;
|
|
436
|
+
}
|
|
437
|
+
async function readMemory(projectDir) {
|
|
438
|
+
const memory = /* @__PURE__ */ new Map();
|
|
439
|
+
const memoryDir = join4(projectDir, "memory");
|
|
440
|
+
let entries;
|
|
441
|
+
try {
|
|
442
|
+
entries = await readdir(memoryDir);
|
|
443
|
+
} catch {
|
|
444
|
+
return memory;
|
|
445
|
+
}
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
const content = await readFile3(join4(memoryDir, entry), "utf-8");
|
|
448
|
+
memory.set(entry, content);
|
|
449
|
+
}
|
|
450
|
+
return memory;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/commands/pull.ts
|
|
454
|
+
async function pull() {
|
|
455
|
+
const cwd = process.cwd();
|
|
456
|
+
const project = await detectProject(cwd);
|
|
457
|
+
console.log(`Project: ${project.normalizedRemote} (${project.projectId})`);
|
|
458
|
+
const config = await loadConfig();
|
|
459
|
+
if (!config) {
|
|
460
|
+
throw new ConfigError(
|
|
461
|
+
"Baton is not configured. Run 'baton push' first to set up the sync repo."
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
const repoDir = getRepoDir();
|
|
465
|
+
if (!await repoExists(repoDir)) {
|
|
466
|
+
console.log("Cloning baton repo...");
|
|
467
|
+
await cloneRepo(config.repo, repoDir);
|
|
468
|
+
} else {
|
|
469
|
+
console.log("Pulling latest...");
|
|
470
|
+
await pullRepo(repoDir);
|
|
471
|
+
}
|
|
472
|
+
const projectDir = join5(repoDir, "projects", project.projectId);
|
|
473
|
+
const data = await readCheckpoint(projectDir);
|
|
474
|
+
if (!data) {
|
|
475
|
+
throw new NoSessionsError(
|
|
476
|
+
"No checkpoint found for this project. Run 'baton push' on another machine first."
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
console.log(
|
|
480
|
+
`Found ${data.sessions.length} session(s), ${data.memory.size} memory file(s)`
|
|
481
|
+
);
|
|
482
|
+
const pathCtx = getLocalPathContext(cwd);
|
|
483
|
+
for (const session of data.sessions) {
|
|
484
|
+
session.jsonl = expandPaths(session.jsonl, pathCtx);
|
|
485
|
+
}
|
|
486
|
+
await restoreProjectData(cwd, data);
|
|
487
|
+
console.log("Pulled successfully. Sessions restored to Claude Code.");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/commands/push.ts
|
|
491
|
+
import { join as join7 } from "path";
|
|
492
|
+
import { createInterface } from "readline/promises";
|
|
493
|
+
|
|
494
|
+
// src/adapters/claude-code/reader.ts
|
|
495
|
+
import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
|
|
496
|
+
import { basename, join as join6 } from "path";
|
|
497
|
+
async function collectProjectData(projectPath) {
|
|
498
|
+
const projectDirName = encodeProjectDir(projectPath);
|
|
499
|
+
const projectDir = join6(getClaudeProjectsDir(), projectDirName);
|
|
500
|
+
const sessions = await collectSessions(projectDir);
|
|
501
|
+
if (sessions.length === 0) {
|
|
502
|
+
throw new NoSessionsError(
|
|
503
|
+
`No Claude Code sessions found for this project. Start a Claude Code session first.`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
const memory = await collectMemory(projectDir);
|
|
507
|
+
return { sessions, memory, projectDirName };
|
|
508
|
+
}
|
|
509
|
+
async function collectSessions(projectDir) {
|
|
510
|
+
let entries;
|
|
511
|
+
try {
|
|
512
|
+
entries = await readdir2(projectDir);
|
|
513
|
+
} catch {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
const jsonlFiles = entries.filter((e) => e.endsWith(".jsonl"));
|
|
517
|
+
const sessions = [];
|
|
518
|
+
for (const jsonlFile of jsonlFiles) {
|
|
519
|
+
const sessionId = basename(jsonlFile, ".jsonl");
|
|
520
|
+
const jsonl = await readFile4(join6(projectDir, jsonlFile), "utf-8");
|
|
521
|
+
const toolResults = await collectToolResults(projectDir, sessionId);
|
|
522
|
+
sessions.push({ sessionId, jsonl, toolResults });
|
|
523
|
+
}
|
|
524
|
+
return sessions;
|
|
525
|
+
}
|
|
526
|
+
async function collectToolResults(projectDir, sessionId) {
|
|
527
|
+
const results = /* @__PURE__ */ new Map();
|
|
528
|
+
const toolResultsDir = join6(projectDir, sessionId, "tool-results");
|
|
529
|
+
let entries;
|
|
530
|
+
try {
|
|
531
|
+
entries = await readdir2(toolResultsDir);
|
|
532
|
+
} catch {
|
|
533
|
+
return results;
|
|
534
|
+
}
|
|
535
|
+
for (const entry of entries) {
|
|
536
|
+
const filePath = join6(toolResultsDir, entry);
|
|
537
|
+
const fileStat = await stat(filePath);
|
|
538
|
+
if (fileStat.isFile()) {
|
|
539
|
+
const content = await readFile4(filePath, "utf-8");
|
|
540
|
+
results.set(entry, content);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return results;
|
|
544
|
+
}
|
|
545
|
+
async function collectMemory(projectDir) {
|
|
546
|
+
const memory = /* @__PURE__ */ new Map();
|
|
547
|
+
const memoryDir = join6(projectDir, "memory");
|
|
548
|
+
let entries;
|
|
549
|
+
try {
|
|
550
|
+
entries = await readdir2(memoryDir);
|
|
551
|
+
} catch {
|
|
552
|
+
return memory;
|
|
553
|
+
}
|
|
554
|
+
for (const entry of entries) {
|
|
555
|
+
const filePath = join6(memoryDir, entry);
|
|
556
|
+
const fileStat = await stat(filePath);
|
|
557
|
+
if (fileStat.isFile()) {
|
|
558
|
+
const content = await readFile4(filePath, "utf-8");
|
|
559
|
+
memory.set(entry, content);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return memory;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/commands/push.ts
|
|
566
|
+
async function push(options) {
|
|
567
|
+
const cwd = process.cwd();
|
|
568
|
+
const project = await detectProject(cwd);
|
|
569
|
+
console.log(`Project: ${project.normalizedRemote} (${project.projectId})`);
|
|
570
|
+
const data = await collectProjectData(cwd);
|
|
571
|
+
console.log(
|
|
572
|
+
`Found ${data.sessions.length} session(s), ${data.memory.size} memory file(s)`
|
|
573
|
+
);
|
|
574
|
+
const pathCtx = getLocalPathContext(cwd);
|
|
575
|
+
for (const session of data.sessions) {
|
|
576
|
+
session.jsonl = virtualizePaths(session.jsonl, pathCtx);
|
|
577
|
+
}
|
|
578
|
+
const repoDir = getRepoDir();
|
|
579
|
+
await ensureBatonRepo(repoDir);
|
|
580
|
+
if (!options.force && await repoExists(repoDir)) {
|
|
581
|
+
try {
|
|
582
|
+
const ahead = await isRemoteAhead(repoDir);
|
|
583
|
+
if (ahead) {
|
|
584
|
+
throw new ConflictError(
|
|
585
|
+
"Remote has changes you haven't pulled. Run 'baton pull' first, or use 'baton push --force' to overwrite."
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (error instanceof ConflictError) throw error;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const projectDir = join7(repoDir, "projects", project.projectId);
|
|
593
|
+
await writeCheckpoint(projectDir, project, data);
|
|
594
|
+
await commitAndPush(
|
|
595
|
+
repoDir,
|
|
596
|
+
`push: ${project.normalizedRemote}`,
|
|
597
|
+
options.force ?? false
|
|
598
|
+
);
|
|
599
|
+
console.log("Pushed successfully.");
|
|
600
|
+
}
|
|
601
|
+
async function ensureBatonRepo(repoDir) {
|
|
602
|
+
let config = await loadConfig();
|
|
603
|
+
if (!config) {
|
|
604
|
+
const rl = createInterface({
|
|
605
|
+
input: process.stdin,
|
|
606
|
+
output: process.stdout
|
|
607
|
+
});
|
|
608
|
+
try {
|
|
609
|
+
const rawName = await rl.question(
|
|
610
|
+
"Enter a name for your baton sync repo (will be created as private on GitHub): "
|
|
611
|
+
);
|
|
612
|
+
const repoName = rawName.trim();
|
|
613
|
+
if (!repoName) {
|
|
614
|
+
throw new Error("Repo name cannot be empty.");
|
|
615
|
+
}
|
|
616
|
+
console.log(`Creating private repo '${repoName}'...`);
|
|
617
|
+
const repoUrl = await createGhRepo(repoName);
|
|
618
|
+
config = { repo: repoUrl };
|
|
619
|
+
await saveConfig(config);
|
|
620
|
+
console.log(`Repo created: ${repoUrl}`);
|
|
621
|
+
} finally {
|
|
622
|
+
rl.close();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (!await repoExists(repoDir)) {
|
|
626
|
+
console.log("Cloning baton repo...");
|
|
627
|
+
await cloneRepo(config.repo, repoDir);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/commands/status.ts
|
|
632
|
+
import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
|
|
633
|
+
import { join as join8 } from "path";
|
|
634
|
+
async function status() {
|
|
635
|
+
const cwd = process.cwd();
|
|
636
|
+
const project = await detectProject(cwd);
|
|
637
|
+
console.log(`Project: ${project.normalizedRemote}`);
|
|
638
|
+
console.log(`Project ID: ${project.projectId}`);
|
|
639
|
+
const projectDirName = encodeProjectDir(cwd);
|
|
640
|
+
const localProjectDir = join8(getClaudeProjectsDir(), projectDirName);
|
|
641
|
+
try {
|
|
642
|
+
const entries = await readdir3(localProjectDir);
|
|
643
|
+
const sessionCount = entries.filter((e) => e.endsWith(".jsonl")).length;
|
|
644
|
+
console.log(`Local sessions: ${sessionCount}`);
|
|
645
|
+
} catch {
|
|
646
|
+
console.log("Local sessions: 0");
|
|
647
|
+
}
|
|
648
|
+
const config = await loadConfig();
|
|
649
|
+
if (!config) {
|
|
650
|
+
console.log("Baton repo: not configured (run 'baton push' to set up)");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
console.log(`Baton repo: ${config.repo}`);
|
|
654
|
+
const repoDir = getRepoDir();
|
|
655
|
+
if (!await repoExists(repoDir)) {
|
|
656
|
+
console.log("Remote checkpoint: not cloned yet");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const metaPath = join8(repoDir, "projects", project.projectId, "meta.json");
|
|
660
|
+
try {
|
|
661
|
+
const raw = await readFile5(metaPath, "utf-8");
|
|
662
|
+
const meta = JSON.parse(raw);
|
|
663
|
+
console.log(`Last pushed: ${meta.pushed_at ?? "unknown"}`);
|
|
664
|
+
} catch {
|
|
665
|
+
console.log("Remote checkpoint: none for this project");
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/cli.ts
|
|
670
|
+
var program = new Command();
|
|
671
|
+
program.name("baton").description("Git-backed session handoff for Claude Code").version("0.1.0");
|
|
672
|
+
program.command("push").description("Push all sessions for the current project to GitHub").option("-f, --force", "Overwrite remote even if ahead").action(async (options) => {
|
|
673
|
+
await push({ force: options.force });
|
|
674
|
+
});
|
|
675
|
+
program.command("pull").description("Restore sessions for the current project from GitHub").action(async () => {
|
|
676
|
+
await pull();
|
|
677
|
+
});
|
|
678
|
+
program.command("status").description("Show current project and sync state").action(async () => {
|
|
679
|
+
await status();
|
|
680
|
+
});
|
|
681
|
+
try {
|
|
682
|
+
await program.parseAsync(process.argv);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (error instanceof BatonError) {
|
|
685
|
+
console.error(`Error: ${error.message}`);
|
|
686
|
+
} else {
|
|
687
|
+
console.error(
|
|
688
|
+
`Unexpected error: ${error instanceof Error ? error.message : String(error)}`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/commands/pull.ts","../src/adapters/claude-code/writer.ts","../src/adapters/claude-code/paths.ts","../src/core/config.ts","../src/core/git.ts","../src/errors.ts","../src/core/paths.ts","../src/core/project.ts","../src/commands/checkpoint.ts","../src/commands/push.ts","../src/adapters/claude-code/reader.ts","../src/commands/status.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { pull } from \"./commands/pull.js\";\nimport { push } from \"./commands/push.js\";\nimport { status } from \"./commands/status.js\";\nimport { BatonError } from \"./errors.js\";\n\nconst program = new Command();\n\nprogram\n\t.name(\"baton\")\n\t.description(\"Git-backed session handoff for Claude Code\")\n\t.version(\"0.1.0\");\n\nprogram\n\t.command(\"push\")\n\t.description(\"Push all sessions for the current project to GitHub\")\n\t.option(\"-f, --force\", \"Overwrite remote even if ahead\")\n\t.action(async (options) => {\n\t\tawait push({ force: options.force });\n\t});\n\nprogram\n\t.command(\"pull\")\n\t.description(\"Restore sessions for the current project from GitHub\")\n\t.action(async () => {\n\t\tawait pull();\n\t});\n\nprogram\n\t.command(\"status\")\n\t.description(\"Show current project and sync state\")\n\t.action(async () => {\n\t\tawait status();\n\t});\n\ntry {\n\tawait program.parseAsync(process.argv);\n} catch (error) {\n\tif (error instanceof BatonError) {\n\t\tconsole.error(`Error: ${error.message}`);\n\t} else {\n\t\tconsole.error(\n\t\t\t`Unexpected error: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n\tprocess.exit(1);\n}\n","import { join } from \"node:path\";\nimport { restoreProjectData } from \"../adapters/claude-code/writer.js\";\nimport { getRepoDir, loadConfig } from \"../core/config.js\";\nimport { cloneRepo, pullRepo, repoExists } from \"../core/git.js\";\nimport { expandPaths, getLocalPathContext } from \"../core/paths.js\";\nimport { detectProject } from \"../core/project.js\";\nimport { ConfigError, NoSessionsError } from \"../errors.js\";\nimport { readCheckpoint } from \"./checkpoint.js\";\n\nexport async function pull(): Promise<void> {\n\tconst cwd = process.cwd();\n\n\t// 1. Detect project\n\tconst project = await detectProject(cwd);\n\tconsole.log(`Project: ${project.normalizedRemote} (${project.projectId})`);\n\n\t// 2. Ensure baton repo is available\n\tconst config = await loadConfig();\n\tif (!config) {\n\t\tthrow new ConfigError(\n\t\t\t\"Baton is not configured. Run 'baton push' first to set up the sync repo.\",\n\t\t);\n\t}\n\n\tconst repoDir = getRepoDir();\n\tif (!(await repoExists(repoDir))) {\n\t\tconsole.log(\"Cloning baton repo...\");\n\t\tawait cloneRepo(config.repo, repoDir);\n\t} else {\n\t\tconsole.log(\"Pulling latest...\");\n\t\tawait pullRepo(repoDir);\n\t}\n\n\t// 3. Read checkpoint from baton repo\n\tconst projectDir = join(repoDir, \"projects\", project.projectId);\n\tconst data = await readCheckpoint(projectDir);\n\n\tif (!data) {\n\t\tthrow new NoSessionsError(\n\t\t\t\"No checkpoint found for this project. Run 'baton push' on another machine first.\",\n\t\t);\n\t}\n\n\tconsole.log(\n\t\t`Found ${data.sessions.length} session(s), ${data.memory.size} memory file(s)`,\n\t);\n\n\t// 4. Expand paths\n\tconst pathCtx = getLocalPathContext(cwd);\n\tfor (const session of data.sessions) {\n\t\tsession.jsonl = expandPaths(session.jsonl, pathCtx);\n\t}\n\n\t// 5. Restore to Claude Code's local storage\n\tawait restoreProjectData(cwd, data);\n\n\tconsole.log(\"Pulled successfully. Sessions restored to Claude Code.\");\n}\n","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport {\n\tencodeProjectDir,\n\tgetClaudeProjectsDir,\n\tgetProjectConfigPath,\n} from \"./paths.js\";\nimport type { ProjectData } from \"./reader.js\";\n\n/**\n * Restore project data to Claude Code's local storage.\n */\nexport async function restoreProjectData(\n\tprojectPath: string,\n\tdata: ProjectData,\n): Promise<void> {\n\tconst projectDirName = encodeProjectDir(projectPath);\n\tconst projectDir = join(getClaudeProjectsDir(), projectDirName);\n\n\tawait mkdir(projectDir, { recursive: true });\n\n\tfor (const session of data.sessions) {\n\t\t// Write session JSONL\n\t\tawait writeFile(\n\t\t\tjoin(projectDir, `${session.sessionId}.jsonl`),\n\t\t\tsession.jsonl,\n\t\t\t\"utf-8\",\n\t\t);\n\n\t\t// Write tool results\n\t\tif (session.toolResults.size > 0) {\n\t\t\tconst toolResultsDir = join(\n\t\t\t\tprojectDir,\n\t\t\t\tsession.sessionId,\n\t\t\t\t\"tool-results\",\n\t\t\t);\n\t\t\tawait mkdir(toolResultsDir, { recursive: true });\n\t\t\tfor (const [filename, content] of session.toolResults) {\n\t\t\t\tawait writeFile(join(toolResultsDir, filename), content, \"utf-8\");\n\t\t\t}\n\t\t}\n\t}\n\n\t// Write memory files\n\tif (data.memory.size > 0) {\n\t\tconst memoryDir = join(projectDir, \"memory\");\n\t\tawait mkdir(memoryDir, { recursive: true });\n\t\tfor (const [filename, content] of data.memory) {\n\t\t\tawait writeFile(join(memoryDir, filename), content, \"utf-8\");\n\t\t}\n\t}\n\n\t// Ensure project-config.json has a mapping\n\tawait ensureProjectConfig(projectPath, projectDirName);\n}\n\nasync function ensureProjectConfig(\n\tprojectPath: string,\n\tprojectDirName: string,\n): Promise<void> {\n\tconst configPath = getProjectConfigPath();\n\n\tlet config: Record<\n\t\tstring,\n\t\t{ manuallyAdded?: boolean; originalPath: string }\n\t> = {};\n\ttry {\n\t\tconst raw = await readFile(configPath, \"utf-8\");\n\t\tconfig = JSON.parse(raw);\n\t} catch {\n\t\t// File doesn't exist or is invalid, start fresh\n\t}\n\n\tif (!config[projectDirName]) {\n\t\tconfig[projectDirName] = {\n\t\t\toriginalPath: projectPath,\n\t\t};\n\t\tawait mkdir(dirname(configPath), { recursive: true });\n\t\tawait writeFile(configPath, JSON.stringify(config, null, 2), \"utf-8\");\n\t}\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Encode a local project path into a Claude Code project directory name.\n * Claude Code replaces `/`, `.`, and `:` with `-`.\n *\n * Examples:\n * /home/dr_who/baton → -home-dr_who-baton\n * /Users/dr_who/work/baton → -Users-dr_who-work-baton\n * C:\\Users\\dr_who\\baton → -C-Users-dr_who-baton\n */\nexport function encodeProjectDir(projectPath: string): string {\n\t// Normalize backslashes to forward slashes (Windows support)\n\tconst normalized = projectPath.replace(/\\\\/g, \"/\");\n\treturn normalized.replace(/[/.:]/g, \"-\");\n}\n\n/**\n * Get the Claude Code projects base directory.\n */\nexport function getClaudeProjectsDir(): string {\n\treturn join(homedir(), \".claude\", \"projects\");\n}\n\n/**\n * Get the full path to a Claude Code project directory.\n */\nexport function getClaudeProjectPath(projectPath: string): string {\n\treturn join(getClaudeProjectsDir(), encodeProjectDir(projectPath));\n}\n\n/**\n * Get the path to Claude Code's project-config.json.\n */\nexport function getProjectConfigPath(): string {\n\treturn join(homedir(), \".claude\", \"project-config.json\");\n}\n","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nexport interface BatonConfig {\n\trepo: string;\n}\n\n/**\n * Get the path to the baton config directory.\n */\nexport function getBatonDir(): string {\n\treturn join(homedir(), \".baton\");\n}\n\n/**\n * Get the path to the baton config file.\n */\nexport function getConfigPath(): string {\n\treturn join(getBatonDir(), \"config.json\");\n}\n\n/**\n * Get the path to the local repo clone.\n */\nexport function getRepoDir(): string {\n\treturn join(getBatonDir(), \"repo\");\n}\n\n/**\n * Load baton config. Returns null if not configured.\n */\nexport async function loadConfig(): Promise<BatonConfig | null> {\n\ttry {\n\t\tconst raw = await readFile(getConfigPath(), \"utf-8\");\n\t\tconst config = JSON.parse(raw);\n\t\tif (!config.repo || typeof config.repo !== \"string\") {\n\t\t\treturn null;\n\t\t}\n\t\treturn config as BatonConfig;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Save baton config.\n */\nexport async function saveConfig(config: BatonConfig): Promise<void> {\n\tconst configPath = getConfigPath();\n\tawait mkdir(dirname(configPath), { recursive: true });\n\tawait writeFile(configPath, JSON.stringify(config, null, 2), \"utf-8\");\n}\n","import { execFile } from \"node:child_process\";\nimport { access } from \"node:fs/promises\";\nimport { promisify } from \"node:util\";\nimport { GhNotFoundError, GitNotFoundError } from \"../errors.js\";\n\nconst execFileAsync = promisify(execFile);\n\nconst GIT_TIMEOUT_MS = 30_000;\nconst GH_TIMEOUT_MS = 60_000;\n\n/**\n * Run a git command in the given directory.\n */\nexport async function git(args: string[], cwd: string): Promise<string> {\n\ttry {\n\t\tconst { stdout } = await execFileAsync(\"git\", args, {\n\t\t\tcwd,\n\t\t\ttimeout: GIT_TIMEOUT_MS,\n\t\t});\n\t\treturn stdout.trim();\n\t} catch (error) {\n\t\tif (isNotFound(error)) {\n\t\t\tthrow new GitNotFoundError(\"git is not installed or not found in PATH.\");\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Run a gh CLI command.\n */\nexport async function gh(args: string[]): Promise<string> {\n\ttry {\n\t\tconst { stdout } = await execFileAsync(\"gh\", args, {\n\t\t\ttimeout: GH_TIMEOUT_MS,\n\t\t});\n\t\treturn stdout.trim();\n\t} catch (error) {\n\t\tif (isNotFound(error)) {\n\t\t\tthrow new GhNotFoundError(\n\t\t\t\t\"gh CLI is not installed. Install it from https://cli.github.com/\",\n\t\t\t);\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Check if the local repo clone exists.\n */\nexport async function repoExists(repoDir: string): Promise<boolean> {\n\ttry {\n\t\tawait access(repoDir);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Clone a repo to the local cache directory.\n */\nexport async function cloneRepo(\n\trepoUrl: string,\n\ttargetDir: string,\n): Promise<void> {\n\tawait execFileAsync(\"git\", [\"clone\", repoUrl, targetDir], {\n\t\ttimeout: GH_TIMEOUT_MS,\n\t});\n}\n\n/**\n * Fetch latest from remote without merging.\n */\nexport async function fetchRepo(repoDir: string): Promise<void> {\n\tawait git([\"fetch\", \"origin\"], repoDir);\n}\n\n/**\n * Pull latest from remote (fast-forward).\n */\nexport async function pullRepo(repoDir: string): Promise<void> {\n\tawait git([\"pull\", \"--ff-only\"], repoDir);\n}\n\n/**\n * Check if remote is ahead of local (has commits we haven't pulled).\n */\nexport async function isRemoteAhead(repoDir: string): Promise<boolean> {\n\tawait fetchRepo(repoDir);\n\tconst localHead = await git([\"rev-parse\", \"HEAD\"], repoDir);\n\tconst remoteHead = await git([\"rev-parse\", \"origin/main\"], repoDir);\n\treturn localHead !== remoteHead;\n}\n\n/**\n * Stage, commit, and push changes.\n */\nexport async function commitAndPush(\n\trepoDir: string,\n\tmessage: string,\n\tforce: boolean,\n): Promise<void> {\n\tawait git([\"add\", \"-A\"], repoDir);\n\n\t// Check if there are changes to commit\n\tconst status = await git([\"status\", \"--porcelain\"], repoDir);\n\tif (!status) {\n\t\treturn; // Nothing to commit\n\t}\n\n\tawait git([\"commit\", \"-m\", message], repoDir);\n\n\tconst pushArgs = force\n\t\t? [\"push\", \"--force\", \"origin\", \"main\"]\n\t\t: [\"push\", \"origin\", \"main\"];\n\tawait git(pushArgs, repoDir);\n}\n\n/**\n * Create a private GitHub repo via gh CLI.\n */\nexport async function createGhRepo(repoName: string): Promise<string> {\n\tconst output = await gh([\n\t\t\"repo\",\n\t\t\"create\",\n\t\trepoName,\n\t\t\"--private\",\n\t\t\"--confirm\",\n\t]);\n\t// gh repo create outputs the URL\n\tconst match = output.match(/https:\\/\\/github\\.com\\/[\\w.-]+\\/[\\w.-]+/);\n\tif (match) {\n\t\treturn match[0];\n\t}\n\t// Fallback: construct from gh whoami\n\tconst username = await gh([\"api\", \"user\", \"--jq\", \".login\"]);\n\treturn `https://github.com/${username}/${repoName}`;\n}\n\n/**\n * Initialize a new git repo with an initial commit.\n */\nexport async function initRepo(repoDir: string): Promise<void> {\n\tawait git([\"init\", \"-b\", \"main\"], repoDir);\n\tawait git([\"commit\", \"--allow-empty\", \"-m\", \"init baton repo\"], repoDir);\n}\n\nfunction isNotFound(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\treturn (error as NodeJS.ErrnoException).code === \"ENOENT\";\n}\n","export class BatonError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = this.constructor.name;\n\t\tError.captureStackTrace?.(this, this.constructor);\n\t}\n}\n\nexport class ProjectNotFoundError extends BatonError {}\n\nexport class NoSessionsError extends BatonError {}\n\nexport class ConflictError extends BatonError {}\n\nexport class GitNotFoundError extends BatonError {}\n\nexport class GhNotFoundError extends BatonError {}\n\nexport class ConfigError extends BatonError {}\n","import { homedir, tmpdir } from \"node:os\";\nimport { sep } from \"node:path\";\n\nconst PLACEHOLDER_PROJECT_ROOT = \"$\" + \"{PROJECT_ROOT}\";\nconst PLACEHOLDER_HOME = \"$\" + \"{HOME}\";\nconst PLACEHOLDER_TMP = \"$\" + \"{TMP}\";\n\nconst PLACEHOLDERS = {\n\tPROJECT_ROOT: PLACEHOLDER_PROJECT_ROOT,\n\tHOME: PLACEHOLDER_HOME,\n\tTMP: PLACEHOLDER_TMP,\n} as const;\n\ninterface PathContext {\n\tprojectRoot: string;\n\thome: string;\n\ttmp: string;\n}\n\n/**\n * Get the current machine's path context.\n */\nexport function getLocalPathContext(projectRoot: string): PathContext {\n\treturn {\n\t\tprojectRoot,\n\t\thome: homedir(),\n\t\ttmp: tmpdir(),\n\t};\n}\n\n/**\n * Virtualize paths in content: replace machine-local absolute paths\n * with portable placeholders.\n *\n * Replaces longest paths first to prevent partial matches.\n * Normalizes path separators to `/` in stored content.\n */\nexport function virtualizePaths(content: string, ctx: PathContext): string {\n\t// Normalize backslashes to forward slashes for consistent matching\n\tconst normalizedContent =\n\t\tsep === \"\\\\\" ? content.replace(/\\\\/g, \"/\") : content;\n\n\t// Build replacement pairs, sorted by path length (longest first)\n\tconst replacements: [string, string][] = (\n\t\t[\n\t\t\t[normalizeSeparators(ctx.projectRoot), PLACEHOLDERS.PROJECT_ROOT],\n\t\t\t[normalizeSeparators(ctx.home), PLACEHOLDERS.HOME],\n\t\t\t[normalizeSeparators(ctx.tmp), PLACEHOLDERS.TMP],\n\t\t] as [string, string][]\n\t).sort((a, b) => b[0].length - a[0].length);\n\n\tlet result = normalizedContent;\n\tfor (const [path, placeholder] of replacements) {\n\t\tif (path) {\n\t\t\tresult = replacePathWithBoundary(result, path, placeholder);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Expand placeholders in content: replace portable placeholders\n * with machine-local absolute paths.\n *\n * Expands to OS-native path separators.\n */\nexport function expandPaths(content: string, ctx: PathContext): string {\n\tlet result = content;\n\n\tresult = replaceAll(\n\t\tresult,\n\t\tPLACEHOLDERS.PROJECT_ROOT,\n\t\ttoNativePath(ctx.projectRoot),\n\t);\n\tresult = replaceAll(result, PLACEHOLDERS.HOME, toNativePath(ctx.home));\n\tresult = replaceAll(result, PLACEHOLDERS.TMP, toNativePath(ctx.tmp));\n\n\treturn result;\n}\n\n/**\n * Normalize path separators to forward slashes.\n */\nfunction normalizeSeparators(path: string): string {\n\treturn path.replace(/\\\\/g, \"/\");\n}\n\n/**\n * Convert a path to use the OS-native separator.\n */\nfunction toNativePath(path: string): string {\n\tif (sep === \"\\\\\") {\n\t\treturn path.replace(/\\//g, \"\\\\\");\n\t}\n\treturn path;\n}\n\n/**\n * Replace all occurrences of a path, but only when followed by a path\n * boundary character (/, \\, \", whitespace, end of string, etc.).\n * Prevents matching /home/dr_who inside /home/dr_who_backup.\n */\nfunction replacePathWithBoundary(\n\tstr: string,\n\tpath: string,\n\treplacement: string,\n): string {\n\tconst escaped = escapeRegex(path);\n\tconst regex = new RegExp(`${escaped}(?=[/\\\\\\\\\"\\\\s,}\\\\]]|$)`, \"g\");\n\treturn str.replace(regex, replacement);\n}\n\n/**\n * Replace all occurrences of a substring.\n */\nfunction replaceAll(str: string, search: string, replacement: string): string {\n\treturn str.split(search).join(replacement);\n}\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n\treturn str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","import { execFile } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { promisify } from \"node:util\";\nimport { GitNotFoundError, ProjectNotFoundError } from \"../errors.js\";\n\nconst execFileAsync = promisify(execFile);\n\nconst GIT_TIMEOUT_MS = 10_000;\n\n/**\n * Detect the git remote URL from the current working directory.\n * Uses the \"origin\" remote by default.\n */\nexport async function getGitRemote(cwd: string): Promise<string> {\n\ttry {\n\t\tconst { stdout } = await execFileAsync(\n\t\t\t\"git\",\n\t\t\t[\"remote\", \"get-url\", \"origin\"],\n\t\t\t{ cwd, timeout: GIT_TIMEOUT_MS },\n\t\t);\n\t\tconst remote = stdout.trim();\n\t\tif (!remote) {\n\t\t\tthrow new ProjectNotFoundError(\n\t\t\t\t\"No git remote URL found. Run this command from a git repository with an 'origin' remote.\",\n\t\t\t);\n\t\t}\n\t\treturn remote;\n\t} catch (error) {\n\t\tif (error instanceof ProjectNotFoundError) throw error;\n\t\tif (isGitNotInstalled(error)) {\n\t\t\tthrow new GitNotFoundError(\n\t\t\t\t\"git is not installed or not found in PATH. Please install git first.\",\n\t\t\t);\n\t\t}\n\t\tthrow new ProjectNotFoundError(\n\t\t\t\"Not a git repository or no 'origin' remote configured. Run this command from a git repository.\",\n\t\t);\n\t}\n}\n\nfunction isGitNotInstalled(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\tconst err = error as NodeJS.ErrnoException;\n\treturn err.code === \"ENOENT\";\n}\n\n/**\n * Normalize a git remote URL to a canonical form.\n *\n * Handles:\n * - git@github.com:user/repo.git → github.com/user/repo\n * - https://github.com/user/repo.git → github.com/user/repo\n * - https://github.com/user/repo → github.com/user/repo\n * - ssh://git@github.com/user/repo.git → github.com/user/repo\n * - git://github.com/user/repo.git → github.com/user/repo\n */\nexport function normalizeGitRemote(remote: string): string {\n\tconst normalized = remote.trim();\n\n\t// SSH shorthand: git@github.com:user/repo.git\n\tconst sshMatch = normalized.match(/^[\\w.-]+@([\\w.-]+):(.*?)(?:\\.git)?$/);\n\tif (sshMatch) {\n\t\treturn `${sshMatch[1]}/${sshMatch[2]}`;\n\t}\n\n\t// ssh:// or git:// protocol: ssh://git@github.com/user/repo.git\n\tconst protoMatch = normalized.match(\n\t\t/^(?:ssh|git):\\/\\/(?:[\\w.-]+@)?([\\w.-]+)\\/(.*?)(?:\\.git)?$/,\n\t);\n\tif (protoMatch) {\n\t\treturn `${protoMatch[1]}/${protoMatch[2]}`;\n\t}\n\n\t// HTTPS: https://github.com/user/repo.git\n\tconst httpsMatch = normalized.match(\n\t\t/^https?:\\/\\/([\\w.-]+)\\/(.*?)(?:\\.git)?$/,\n\t);\n\tif (httpsMatch) {\n\t\treturn `${httpsMatch[1]}/${httpsMatch[2]}`;\n\t}\n\n\t// Fallback: return as-is (stripped)\n\treturn normalized;\n}\n\n/**\n * Hash a normalized git remote URL to produce a stable project ID.\n */\nexport function hashProjectId(normalizedRemote: string): string {\n\treturn createHash(\"sha256\")\n\t\t.update(normalizedRemote)\n\t\t.digest(\"hex\")\n\t\t.slice(0, 16);\n}\n\n/**\n * Detect the project from the current working directory.\n * Returns the project hash and normalized git remote.\n */\nexport async function detectProject(\n\tcwd: string,\n): Promise<{ projectId: string; gitRemote: string; normalizedRemote: string }> {\n\tconst gitRemote = await getGitRemote(cwd);\n\tconst normalizedRemote = normalizeGitRemote(gitRemote);\n\tconst projectId = hashProjectId(normalizedRemote);\n\treturn { projectId, gitRemote, normalizedRemote };\n}\n","import { mkdir, readdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type {\n\tProjectData,\n\tSessionData,\n} from \"../adapters/claude-code/reader.js\";\n\ninterface ProjectMeta {\n\tproject_id: string;\n\tgit_remote: string;\n\tpushed_at: string;\n}\n\n/**\n * Write project data as a checkpoint to the baton repo.\n */\nexport async function writeCheckpoint(\n\tprojectDir: string,\n\tproject: { projectId: string; gitRemote: string },\n\tdata: ProjectData,\n): Promise<void> {\n\t// Clean existing sessions directory to remove stale sessions\n\tconst sessionsDir = join(projectDir, \"sessions\");\n\tawait rm(sessionsDir, { recursive: true, force: true });\n\tawait mkdir(sessionsDir, { recursive: true });\n\n\t// Write meta\n\tconst meta: ProjectMeta = {\n\t\tproject_id: project.projectId,\n\t\tgit_remote: project.gitRemote,\n\t\tpushed_at: new Date().toISOString(),\n\t};\n\tawait writeFile(\n\t\tjoin(projectDir, \"meta.json\"),\n\t\tJSON.stringify(meta, null, 2),\n\t\t\"utf-8\",\n\t);\n\n\t// Write sessions\n\tfor (const session of data.sessions) {\n\t\tawait writeFile(\n\t\t\tjoin(sessionsDir, `${session.sessionId}.jsonl`),\n\t\t\tsession.jsonl,\n\t\t\t\"utf-8\",\n\t\t);\n\n\t\t// Write tool results\n\t\tif (session.toolResults.size > 0) {\n\t\t\tconst toolResultsDir = join(\n\t\t\t\tsessionsDir,\n\t\t\t\tsession.sessionId,\n\t\t\t\t\"tool-results\",\n\t\t\t);\n\t\t\tawait mkdir(toolResultsDir, { recursive: true });\n\t\t\tfor (const [filename, content] of session.toolResults) {\n\t\t\t\tawait writeFile(join(toolResultsDir, filename), content, \"utf-8\");\n\t\t\t}\n\t\t}\n\t}\n\n\t// Write memory\n\tif (data.memory.size > 0) {\n\t\tconst memoryDir = join(projectDir, \"memory\");\n\t\tawait mkdir(memoryDir, { recursive: true });\n\t\tfor (const [filename, content] of data.memory) {\n\t\t\tawait writeFile(join(memoryDir, filename), content, \"utf-8\");\n\t\t}\n\t}\n}\n\n/**\n * Read a checkpoint from the baton repo. Returns null if not found.\n */\nexport async function readCheckpoint(\n\tprojectDir: string,\n): Promise<ProjectData | null> {\n\tconst sessionsDir = join(projectDir, \"sessions\");\n\n\tlet sessionFiles: string[];\n\ttry {\n\t\tconst entries = await readdir(sessionsDir);\n\t\tsessionFiles = entries.filter((e) => e.endsWith(\".jsonl\"));\n\t} catch {\n\t\treturn null;\n\t}\n\n\tif (sessionFiles.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst sessions: SessionData[] = [];\n\tfor (const file of sessionFiles) {\n\t\tconst sessionId = file.replace(\".jsonl\", \"\");\n\t\tconst jsonl = await readFile(join(sessionsDir, file), \"utf-8\");\n\t\tconst toolResults = await readToolResults(sessionsDir, sessionId);\n\t\tsessions.push({ sessionId, jsonl, toolResults });\n\t}\n\n\tconst memory = await readMemory(projectDir);\n\n\treturn {\n\t\tsessions,\n\t\tmemory,\n\t\tprojectDirName: \"\",\n\t};\n}\n\nasync function readToolResults(\n\tsessionsDir: string,\n\tsessionId: string,\n): Promise<Map<string, string>> {\n\tconst results = new Map<string, string>();\n\tconst toolResultsDir = join(sessionsDir, sessionId, \"tool-results\");\n\n\tlet entries: string[];\n\ttry {\n\t\tentries = await readdir(toolResultsDir);\n\t} catch {\n\t\treturn results;\n\t}\n\n\tfor (const entry of entries) {\n\t\tconst content = await readFile(join(toolResultsDir, entry), \"utf-8\");\n\t\tresults.set(entry, content);\n\t}\n\n\treturn results;\n}\n\nasync function readMemory(projectDir: string): Promise<Map<string, string>> {\n\tconst memory = new Map<string, string>();\n\tconst memoryDir = join(projectDir, \"memory\");\n\n\tlet entries: string[];\n\ttry {\n\t\tentries = await readdir(memoryDir);\n\t} catch {\n\t\treturn memory;\n\t}\n\n\tfor (const entry of entries) {\n\t\tconst content = await readFile(join(memoryDir, entry), \"utf-8\");\n\t\tmemory.set(entry, content);\n\t}\n\n\treturn memory;\n}\n","import { join } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\nimport { collectProjectData } from \"../adapters/claude-code/reader.js\";\nimport { getRepoDir, loadConfig, saveConfig } from \"../core/config.js\";\nimport {\n\tcloneRepo,\n\tcommitAndPush,\n\tcreateGhRepo,\n\tisRemoteAhead,\n\trepoExists,\n} from \"../core/git.js\";\nimport { getLocalPathContext, virtualizePaths } from \"../core/paths.js\";\nimport { detectProject } from \"../core/project.js\";\nimport { ConflictError } from \"../errors.js\";\nimport { writeCheckpoint } from \"./checkpoint.js\";\n\nexport async function push(options: { force?: boolean }): Promise<void> {\n\tconst cwd = process.cwd();\n\n\t// 1. Detect project\n\tconst project = await detectProject(cwd);\n\tconsole.log(`Project: ${project.normalizedRemote} (${project.projectId})`);\n\n\t// 2. Collect session data\n\tconst data = await collectProjectData(cwd);\n\tconsole.log(\n\t\t`Found ${data.sessions.length} session(s), ${data.memory.size} memory file(s)`,\n\t);\n\n\t// 3. Virtualize paths\n\tconst pathCtx = getLocalPathContext(cwd);\n\tfor (const session of data.sessions) {\n\t\tsession.jsonl = virtualizePaths(session.jsonl, pathCtx);\n\t}\n\n\t// 4. Ensure baton repo exists\n\tconst repoDir = getRepoDir();\n\tawait ensureBatonRepo(repoDir);\n\n\t// 5. Conflict check\n\tif (!options.force && (await repoExists(repoDir))) {\n\t\ttry {\n\t\t\tconst ahead = await isRemoteAhead(repoDir);\n\t\t\tif (ahead) {\n\t\t\t\tthrow new ConflictError(\n\t\t\t\t\t\"Remote has changes you haven't pulled. Run 'baton pull' first, or use 'baton push --force' to overwrite.\",\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (error instanceof ConflictError) throw error;\n\t\t\t// Ignore fetch errors (e.g., empty repo with no commits on remote yet)\n\t\t}\n\t}\n\n\t// 6. Write checkpoint to baton repo\n\tconst projectDir = join(repoDir, \"projects\", project.projectId);\n\tawait writeCheckpoint(projectDir, project, data);\n\n\t// 7. Commit and push\n\tawait commitAndPush(\n\t\trepoDir,\n\t\t`push: ${project.normalizedRemote}`,\n\t\toptions.force ?? false,\n\t);\n\n\tconsole.log(\"Pushed successfully.\");\n}\n\nasync function ensureBatonRepo(repoDir: string): Promise<void> {\n\tlet config = await loadConfig();\n\n\tif (!config) {\n\t\t// First-time setup: prompt for repo name\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\ttry {\n\t\t\tconst rawName = await rl.question(\n\t\t\t\t\"Enter a name for your baton sync repo (will be created as private on GitHub): \",\n\t\t\t);\n\t\t\tconst repoName = rawName.trim();\n\t\t\tif (!repoName) {\n\t\t\t\tthrow new Error(\"Repo name cannot be empty.\");\n\t\t\t}\n\n\t\t\tconsole.log(`Creating private repo '${repoName}'...`);\n\t\t\tconst repoUrl = await createGhRepo(repoName);\n\t\t\tconfig = { repo: repoUrl };\n\t\t\tawait saveConfig(config);\n\t\t\tconsole.log(`Repo created: ${repoUrl}`);\n\t\t} finally {\n\t\t\trl.close();\n\t\t}\n\t}\n\n\tif (!(await repoExists(repoDir))) {\n\t\tconsole.log(\"Cloning baton repo...\");\n\t\tawait cloneRepo(config.repo, repoDir);\n\t}\n}\n","import { readdir, readFile, stat } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport { NoSessionsError } from \"../../errors.js\";\nimport { encodeProjectDir, getClaudeProjectsDir } from \"./paths.js\";\n\nexport interface SessionData {\n\tsessionId: string;\n\tjsonl: string;\n\ttoolResults: Map<string, string>;\n}\n\nexport interface ProjectData {\n\tsessions: SessionData[];\n\tmemory: Map<string, string>;\n\tprojectDirName: string;\n}\n\n/**\n * Collect all session data for a project from Claude Code's local storage.\n */\nexport async function collectProjectData(\n\tprojectPath: string,\n): Promise<ProjectData> {\n\tconst projectDirName = encodeProjectDir(projectPath);\n\tconst projectDir = join(getClaudeProjectsDir(), projectDirName);\n\n\tconst sessions = await collectSessions(projectDir);\n\tif (sessions.length === 0) {\n\t\tthrow new NoSessionsError(\n\t\t\t`No Claude Code sessions found for this project. Start a Claude Code session first.`,\n\t\t);\n\t}\n\n\tconst memory = await collectMemory(projectDir);\n\n\treturn { sessions, memory, projectDirName };\n}\n\nasync function collectSessions(projectDir: string): Promise<SessionData[]> {\n\tlet entries: string[];\n\ttry {\n\t\tentries = await readdir(projectDir);\n\t} catch {\n\t\treturn [];\n\t}\n\n\tconst jsonlFiles = entries.filter((e) => e.endsWith(\".jsonl\"));\n\tconst sessions: SessionData[] = [];\n\n\tfor (const jsonlFile of jsonlFiles) {\n\t\tconst sessionId = basename(jsonlFile, \".jsonl\");\n\t\tconst jsonl = await readFile(join(projectDir, jsonlFile), \"utf-8\");\n\t\tconst toolResults = await collectToolResults(projectDir, sessionId);\n\t\tsessions.push({ sessionId, jsonl, toolResults });\n\t}\n\n\treturn sessions;\n}\n\nasync function collectToolResults(\n\tprojectDir: string,\n\tsessionId: string,\n): Promise<Map<string, string>> {\n\tconst results = new Map<string, string>();\n\tconst toolResultsDir = join(projectDir, sessionId, \"tool-results\");\n\n\tlet entries: string[];\n\ttry {\n\t\tentries = await readdir(toolResultsDir);\n\t} catch {\n\t\treturn results;\n\t}\n\n\tfor (const entry of entries) {\n\t\tconst filePath = join(toolResultsDir, entry);\n\t\tconst fileStat = await stat(filePath);\n\t\tif (fileStat.isFile()) {\n\t\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\t\tresults.set(entry, content);\n\t\t}\n\t}\n\n\treturn results;\n}\n\nasync function collectMemory(projectDir: string): Promise<Map<string, string>> {\n\tconst memory = new Map<string, string>();\n\tconst memoryDir = join(projectDir, \"memory\");\n\n\tlet entries: string[];\n\ttry {\n\t\tentries = await readdir(memoryDir);\n\t} catch {\n\t\treturn memory;\n\t}\n\n\tfor (const entry of entries) {\n\t\tconst filePath = join(memoryDir, entry);\n\t\tconst fileStat = await stat(filePath);\n\t\tif (fileStat.isFile()) {\n\t\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\t\tmemory.set(entry, content);\n\t\t}\n\t}\n\n\treturn memory;\n}\n","import { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n\tencodeProjectDir,\n\tgetClaudeProjectsDir,\n} from \"../adapters/claude-code/paths.js\";\nimport { getRepoDir, loadConfig } from \"../core/config.js\";\nimport { repoExists } from \"../core/git.js\";\nimport { detectProject } from \"../core/project.js\";\n\nexport async function status(): Promise<void> {\n\tconst cwd = process.cwd();\n\n\t// Project info\n\tconst project = await detectProject(cwd);\n\tconsole.log(`Project: ${project.normalizedRemote}`);\n\tconsole.log(`Project ID: ${project.projectId}`);\n\n\t// Local sessions\n\tconst projectDirName = encodeProjectDir(cwd);\n\tconst localProjectDir = join(getClaudeProjectsDir(), projectDirName);\n\ttry {\n\t\tconst entries = await readdir(localProjectDir);\n\t\tconst sessionCount = entries.filter((e) => e.endsWith(\".jsonl\")).length;\n\t\tconsole.log(`Local sessions: ${sessionCount}`);\n\t} catch {\n\t\tconsole.log(\"Local sessions: 0\");\n\t}\n\n\t// Baton config\n\tconst config = await loadConfig();\n\tif (!config) {\n\t\tconsole.log(\"Baton repo: not configured (run 'baton push' to set up)\");\n\t\treturn;\n\t}\n\tconsole.log(`Baton repo: ${config.repo}`);\n\n\t// Remote checkpoint\n\tconst repoDir = getRepoDir();\n\tif (!(await repoExists(repoDir))) {\n\t\tconsole.log(\"Remote checkpoint: not cloned yet\");\n\t\treturn;\n\t}\n\n\tconst metaPath = join(repoDir, \"projects\", project.projectId, \"meta.json\");\n\ttry {\n\t\tconst raw = await readFile(metaPath, \"utf-8\");\n\t\tconst meta = JSON.parse(raw);\n\t\tconsole.log(`Last pushed: ${meta.pushed_at ?? \"unknown\"}`);\n\t} catch {\n\t\tconsole.log(\"Remote checkpoint: none for this project\");\n\t}\n}\n"],"mappings":"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","names":["join","join","join","mkdir","readFile","writeFile","homedir","dirname","join","status","homedir","execFile","promisify","execFileAsync","promisify","execFile","GIT_TIMEOUT_MS","mkdir","readFile","writeFile","join","join","join","readdir","readFile","join","join","readdir","readFile","join","readdir","readFile","join","join","readdir","readFile"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "baton-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Git-backed session handoff for Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"baton": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "tsx src/cli.ts",
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"test:coverage": "vitest run --coverage",
|
|
18
|
+
"lint": "biome check src/",
|
|
19
|
+
"lint:fix": "biome check --write src/",
|
|
20
|
+
"format": "biome format --write src/",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"claude-code",
|
|
25
|
+
"session",
|
|
26
|
+
"handoff",
|
|
27
|
+
"sync"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/quabug/baton.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"packageManager": "pnpm@10.32.1",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"commander": "^14.0.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@biomejs/biome": "^2.4.7",
|
|
41
|
+
"@types/node": "^25.5.0",
|
|
42
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
43
|
+
"tsup": "^8.5.1",
|
|
44
|
+
"tsx": "^4.21.0",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^4.1.0"
|
|
47
|
+
}
|
|
48
|
+
}
|