@askalf/claude-sync 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `~/.claude-sync/config.json` — top-level CLI configuration.
3
+ *
4
+ * One required field: `syncDir`. Everything else is optional and
5
+ * defaulted. The user runs `claude-sync init` to scaffold this file
6
+ * pointing at their Dropbox / iCloud Drive / Syncthing folder.
7
+ *
8
+ * {
9
+ * "_schemaVersion": 1,
10
+ * "syncDir": "/Users/thomas/Dropbox/claude-sync",
11
+ * "machineName": "thomas-desktop"
12
+ * }
13
+ *
14
+ * The transport layer (filesystem only in v0.0.1) reads/writes
15
+ * `<syncDir>/<projectKey>/<sessionId>-<machineName>.ccsync`. That
16
+ * naming gives us:
17
+ * - One subdir per project, easy to inspect
18
+ * - Multiple machines can push into the same project without
19
+ * overwriting each other (machineName disambiguates)
20
+ * - `pull` can list project subdirs and import the freshest file
21
+ */
22
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
23
+ import { dirname, join } from 'node:path';
24
+ import { hostname } from 'node:os';
25
+ import { syncRoot } from './project.js';
26
+ const CONFIG_PATH = () => join(syncRoot(), 'config.json');
27
+ export function configExists() {
28
+ return existsSync(CONFIG_PATH());
29
+ }
30
+ export function loadConfig() {
31
+ const path = CONFIG_PATH();
32
+ if (!existsSync(path)) {
33
+ throw new Error(`No config found at ${path}. Run \`claude-sync init <syncDir>\` first.`);
34
+ }
35
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
36
+ if (parsed._schemaVersion !== 1) {
37
+ throw new Error(`Unsupported config schema version: ${parsed._schemaVersion}`);
38
+ }
39
+ return parsed;
40
+ }
41
+ export function saveConfig(cfg) {
42
+ const path = CONFIG_PATH();
43
+ if (!existsSync(dirname(path)))
44
+ mkdirSync(dirname(path), { recursive: true });
45
+ writeFileSync(path, JSON.stringify(cfg, null, 2), { mode: 0o600 });
46
+ }
47
+ export function defaultMachineName() {
48
+ // hostname() can include domain on some platforms — strip it so the
49
+ // default name is short and human-friendly.
50
+ return hostname().split('.')[0] ?? 'unknown';
51
+ }
52
+ export function buildDefaultConfig(syncDir, machineName) {
53
+ return {
54
+ _schemaVersion: 1,
55
+ syncDir,
56
+ machineName: machineName ?? defaultMachineName(),
57
+ };
58
+ }
59
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAaxC,MAAM,WAAW,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa,CAAC,CAAC;AAElE,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAC3B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,sBAAsB,IAAI,6CAA6C,CACxE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAW,CAAC;IACjE,IAAI,MAAM,CAAC,cAAc,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,sCAAsC,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAC3B,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAAE,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9E,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,oEAAoE;IACpE,4CAA4C;IAC5C,OAAO,QAAQ,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,WAAoB;IACtE,OAAO;QACL,cAAc,EAAE,CAAC;QACjB,OAAO;QACP,WAAW,EAAE,WAAW,IAAI,kBAAkB,EAAE;KACjD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `.ccsync` file format — a portable bundle representing one Claude
3
+ * Code session, machine-agnostic.
4
+ *
5
+ * {
6
+ * "_schemaVersion": 1,
7
+ * "sessionId": "...", // CC session id (UUID-shaped in practice)
8
+ * "projectKey": "git:https://github.com/owner/repo.git",
9
+ * "originalCwd": "C:\\Users\\...\\repo",
10
+ * "machineName": "thomas-desktop",
11
+ * "exportedAt": 1715380000000, // Date.now() at export time
12
+ * "lineCount": 142,
13
+ * "jsonl": "<entire JSONL content as a string>"
14
+ * }
15
+ *
16
+ * Stored as plain JSON (not gzipped). For very large sessions a future
17
+ * version may switch to a wrapper that streams; v0.0.1 keeps it simple
18
+ * and lets the underlying transport (Dropbox / Syncthing) handle
19
+ * compression if it cares to.
20
+ *
21
+ * The schema is versioned so we can extend it without breaking older
22
+ * importers. Importers MUST refuse unknown schema versions explicitly
23
+ * rather than silently dropping fields.
24
+ */
25
+ export interface CcsyncFile {
26
+ _schemaVersion: 1;
27
+ sessionId: string;
28
+ projectKey: string;
29
+ /** The cwd as it was on the source machine. Useful diagnostic only —
30
+ * importers don't trust it for path resolution (that's projectKey's
31
+ * job). */
32
+ originalCwd: string;
33
+ /** Free-form machine name from the registry; just for "this came from
34
+ * X" surfaces. */
35
+ machineName: string;
36
+ /** Wall-clock millis when the export was made. Used by `pull` to skip
37
+ * files that are older than what's already imported. */
38
+ exportedAt: number;
39
+ /** Line count at export time. Sanity-check against the receiver's
40
+ * count after import to catch transport corruption. */
41
+ lineCount: number;
42
+ /** The session's full JSONL content. */
43
+ jsonl: string;
44
+ }
45
+ export declare const CURRENT_SCHEMA_VERSION: 1;
46
+ export declare function buildCcsync(opts: Omit<CcsyncFile, '_schemaVersion'>): CcsyncFile;
47
+ export declare function serializeCcsync(file: CcsyncFile): string;
48
+ export declare function parseCcsync(raw: string): CcsyncFile;
49
+ export declare function readCcsyncFile(path: string): CcsyncFile;
50
+ export declare function writeCcsyncFile(path: string, file: CcsyncFile): void;
51
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,CAAC,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB;;gBAEY;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB;uBACmB;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;6DACyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB;4DACwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,sBAAsB,EAAG,CAAU,CAAC;AAEjD,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,gBAAgB,CAAC,GAAG,UAAU,CAKhF;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAKxD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CA0BnD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAEvD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAEpE"}
package/dist/format.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `.ccsync` file format — a portable bundle representing one Claude
3
+ * Code session, machine-agnostic.
4
+ *
5
+ * {
6
+ * "_schemaVersion": 1,
7
+ * "sessionId": "...", // CC session id (UUID-shaped in practice)
8
+ * "projectKey": "git:https://github.com/owner/repo.git",
9
+ * "originalCwd": "C:\\Users\\...\\repo",
10
+ * "machineName": "thomas-desktop",
11
+ * "exportedAt": 1715380000000, // Date.now() at export time
12
+ * "lineCount": 142,
13
+ * "jsonl": "<entire JSONL content as a string>"
14
+ * }
15
+ *
16
+ * Stored as plain JSON (not gzipped). For very large sessions a future
17
+ * version may switch to a wrapper that streams; v0.0.1 keeps it simple
18
+ * and lets the underlying transport (Dropbox / Syncthing) handle
19
+ * compression if it cares to.
20
+ *
21
+ * The schema is versioned so we can extend it without breaking older
22
+ * importers. Importers MUST refuse unknown schema versions explicitly
23
+ * rather than silently dropping fields.
24
+ */
25
+ import { readFileSync, writeFileSync } from 'node:fs';
26
+ export const CURRENT_SCHEMA_VERSION = 1;
27
+ export function buildCcsync(opts) {
28
+ return {
29
+ _schemaVersion: CURRENT_SCHEMA_VERSION,
30
+ ...opts,
31
+ };
32
+ }
33
+ export function serializeCcsync(file) {
34
+ // Pretty-print so a human grepping a .ccsync file sees something
35
+ // readable. The file size penalty is trivial compared to the JSONL
36
+ // payload itself.
37
+ return JSON.stringify(file, null, 2);
38
+ }
39
+ export function parseCcsync(raw) {
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(raw);
43
+ }
44
+ catch (err) {
45
+ throw new Error(`Not a valid .ccsync file (JSON parse failed): ${err instanceof Error ? err.message : err}`);
46
+ }
47
+ if (!parsed || typeof parsed !== 'object') {
48
+ throw new Error('Not a valid .ccsync file (expected a JSON object)');
49
+ }
50
+ const obj = parsed;
51
+ const ver = obj['_schemaVersion'];
52
+ if (ver !== CURRENT_SCHEMA_VERSION) {
53
+ throw new Error(`Unsupported .ccsync schema version: ${ver}. ` +
54
+ `This claude-sync supports schema version ${CURRENT_SCHEMA_VERSION}. ` +
55
+ `Upgrade claude-sync to import this file.`);
56
+ }
57
+ const required = [
58
+ 'sessionId', 'projectKey', 'originalCwd', 'machineName', 'exportedAt', 'lineCount', 'jsonl',
59
+ ];
60
+ for (const k of required) {
61
+ if (!(k in obj))
62
+ throw new Error(`Malformed .ccsync file: missing required field "${k}"`);
63
+ }
64
+ return obj;
65
+ }
66
+ export function readCcsyncFile(path) {
67
+ return parseCcsync(readFileSync(path, 'utf-8'));
68
+ }
69
+ export function writeCcsyncFile(path, file) {
70
+ writeFileSync(path, serializeCcsync(file), { mode: 0o600 });
71
+ }
72
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAuBtD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAU,CAAC;AAEjD,MAAM,UAAU,WAAW,CAAC,IAAwC;IAClE,OAAO;QACL,cAAc,EAAE,sBAAsB;QACtC,GAAG,IAAI;KACR,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAgB;IAC9C,iEAAiE;IACjE,mEAAmE;IACnE,kBAAkB;IAClB,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,iDAAiD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/G,CAAC;IACD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,GAAG,GAAG,MAAiC,CAAC;IAC9C,MAAM,GAAG,GAAG,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC,IAAI,GAAG,KAAK,sBAAsB,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,uCAAuC,GAAG,IAAI;YAC9C,4CAA4C,sBAAsB,IAAI;YACtE,0CAA0C,CAC3C,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAoC;QAChD,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO;KAC5F,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,GAAG,CAAC,CAAC;IAC5F,CAAC;IACD,OAAO,GAA4B,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,WAAW,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,IAAgB;IAC5D,aAAa,CAAC,IAAI,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @askalf/claude-sync — programmatic API.
3
+ *
4
+ * Most users will only touch the CLI (`claude-sync push` / `pull`).
5
+ * This module re-exports the building blocks for anyone wanting to
6
+ * embed sync into their own tooling — e.g. a watch-mode daemon, a
7
+ * GitHub Action that snapshots sessions, or an alternative transport
8
+ * (S3, gist, custom relay).
9
+ */
10
+ export { encodeProjectDir, claudeProjectsRoot, syncRoot, gitRemoteUrl, projectKey, loadRegistry, saveRegistry, registerProject, lookupCwd, type ProjectRegistry, } from './project.js';
11
+ export { listSessions, readSession, writeSession, assignFreshId, type SessionMetadata, } from './session.js';
12
+ export { buildCcsync, serializeCcsync, parseCcsync, readCcsyncFile, writeCcsyncFile, CURRENT_SCHEMA_VERSION, type CcsyncFile, } from './format.js';
13
+ export { loadConfig, saveConfig, configExists, defaultMachineName, buildDefaultConfig, type Config, } from './config.js';
14
+ export { projectSubdir, pushToTransport, listTransport, listProjectKeys, type TransportEntry, } from './transport.js';
15
+ export declare const VERSION = "0.0.1";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,SAAS,EACT,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,WAAW,EACX,eAAe,EACf,WAAW,EACX,cAAc,EACd,eAAe,EACf,sBAAsB,EACtB,KAAK,UAAU,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,UAAU,EACV,UAAU,EACV,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,MAAM,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,aAAa,EACb,eAAe,EACf,aAAa,EACb,eAAe,EACf,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AAExB,eAAO,MAAM,OAAO,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @askalf/claude-sync — programmatic API.
3
+ *
4
+ * Most users will only touch the CLI (`claude-sync push` / `pull`).
5
+ * This module re-exports the building blocks for anyone wanting to
6
+ * embed sync into their own tooling — e.g. a watch-mode daemon, a
7
+ * GitHub Action that snapshots sessions, or an alternative transport
8
+ * (S3, gist, custom relay).
9
+ */
10
+ export { encodeProjectDir, claudeProjectsRoot, syncRoot, gitRemoteUrl, projectKey, loadRegistry, saveRegistry, registerProject, lookupCwd, } from './project.js';
11
+ export { listSessions, readSession, writeSession, assignFreshId, } from './session.js';
12
+ export { buildCcsync, serializeCcsync, parseCcsync, readCcsyncFile, writeCcsyncFile, CURRENT_SCHEMA_VERSION, } from './format.js';
13
+ export { loadConfig, saveConfig, configExists, defaultMachineName, buildDefaultConfig, } from './config.js';
14
+ export { projectSubdir, pushToTransport, listTransport, listProjectKeys, } from './transport.js';
15
+ export const VERSION = '0.0.1';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,SAAS,GAEV,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,GAEd,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,WAAW,EACX,eAAe,EACf,WAAW,EACX,cAAc,EACd,eAAe,EACf,sBAAsB,GAEvB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,UAAU,EACV,UAAU,EACV,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,GAEnB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,aAAa,EACb,eAAe,EACf,aAAa,EACb,eAAe,GAEhB,MAAM,gBAAgB,CAAC;AAExB,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Project identity + path-hash utilities.
3
+ *
4
+ * Claude Code stores sessions at:
5
+ * ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl
6
+ *
7
+ * `<encoded-cwd>` is the absolute path with `[/\\:.\s]+` collapsed to `-`.
8
+ * That's the path-hash mismatch problem when syncing across machines:
9
+ * the same git repo at `/Users/alice/code/myapp` (mac) and
10
+ * `D:\code\myapp` (windows) hashes to different directory names, so a
11
+ * naive rsync of `~/.claude/projects/` produces orphaned sessions.
12
+ *
13
+ * This module gives us:
14
+ * - The path-hash function (matches CC's algorithm).
15
+ * - A canonical "project key" derived from the git remote URL of the
16
+ * working directory. The remote is identical across machines, so it
17
+ * deterministically maps the same logical project on both sides.
18
+ * - Reverse lookup: given a project key + a registry of local repo
19
+ * locations, pick the right local cwd to install a session into.
20
+ */
21
+ /** Encode a cwd into Claude Code's project-directory format. Reversible
22
+ * in spirit but lossy in practice (a cwd containing literal `-` chars
23
+ * decodes ambiguously). CC doesn't reverse it; it's a one-way hash. */
24
+ export declare function encodeProjectDir(cwd: string): string;
25
+ /** Where Claude Code stores its session JSONL files. */
26
+ export declare function claudeProjectsRoot(): string;
27
+ /** `~/.claude-sync/` lives next to `~/.claude/`. We keep our config and
28
+ * the local-project registry here. */
29
+ export declare function syncRoot(): string;
30
+ /** Try to read the git remote URL of the cwd. Returns null when:
31
+ * - cwd isn't a git repo
32
+ * - git isn't installed
33
+ * - the repo has no remote configured (greenfield, not yet pushed)
34
+ *
35
+ * Trims trailing whitespace + newlines but otherwise leaves the URL
36
+ * exactly as `git` reported. We use it as a key, not a URL — so
37
+ * `git@github.com:owner/repo.git` and `https://github.com/owner/repo.git`
38
+ * for the same logical repo will NOT match. The user can fix that by
39
+ * normalizing one of the two via `git remote set-url`, or by manually
40
+ * registering both in the project registry under one canonical key. */
41
+ export declare function gitRemoteUrl(cwd: string): string | null;
42
+ /** Compute a stable, machine-portable project key for a cwd. Prefers the
43
+ * git remote URL; falls back to the directory basename when no remote
44
+ * is configured. The basename fallback is best-effort — collisions
45
+ * between unrelated projects with the same name are possible. */
46
+ export declare function projectKey(cwd: string): string;
47
+ /** Local-project registry. Maps a project key to the local cwd on this
48
+ * machine. Built up as the user runs `claude-sync export` (which
49
+ * registers the cwd) and consulted by `claude-sync import` to find
50
+ * where to install a peer's session. */
51
+ export interface ProjectRegistry {
52
+ _schemaVersion: 1;
53
+ /** machine-friendly identifier — used in .ccsync metadata so the
54
+ * receiving side can label "this came from <name>'s laptop" */
55
+ machineName: string;
56
+ /** Map from canonical project key → list of local cwds that resolve
57
+ * to that key on this machine. Most users will have exactly one
58
+ * per key; a list is forgiving when someone clones the same repo
59
+ * twice (e.g. `myapp/` and `myapp-fork/`). */
60
+ projects: Record<string, string[]>;
61
+ }
62
+ export declare function loadRegistry(): ProjectRegistry;
63
+ export declare function saveRegistry(reg: ProjectRegistry): void;
64
+ /** Register a cwd under its computed project key. Idempotent; pushing
65
+ * the same cwd twice doesn't create duplicates. */
66
+ export declare function registerProject(cwd: string): {
67
+ key: string;
68
+ registry: ProjectRegistry;
69
+ };
70
+ /** Look up a local cwd for a given project key. Returns the first
71
+ * registered match (registries are user-edited; they can reorder if
72
+ * they want a specific clone preferred). Returns null when the key
73
+ * isn't in the registry — the caller should ask the user where to
74
+ * install. */
75
+ export declare function lookupCwd(key: string, reg?: ProjectRegistry): string | null;
76
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH;;wEAEwE;AACxE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wDAAwD;AACxD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;uCACuC;AACvC,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;;;;;;;;;;uEAUuE;AACvE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAavD;AAED;;;kEAGkE;AAClE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI9C;AAED;;;yCAGyC;AACzC,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,CAAC,CAAC;IAClB;oEACgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB;;;mDAG+C;IAC/C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACpC;AAID,wBAAgB,YAAY,IAAI,eAAe,CAmB9C;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAUvD;AAED;oDACoD;AACpD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,eAAe,CAAA;CAAE,CASvF;AAED;;;;eAIe;AACf,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAQ3E"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Project identity + path-hash utilities.
3
+ *
4
+ * Claude Code stores sessions at:
5
+ * ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl
6
+ *
7
+ * `<encoded-cwd>` is the absolute path with `[/\\:.\s]+` collapsed to `-`.
8
+ * That's the path-hash mismatch problem when syncing across machines:
9
+ * the same git repo at `/Users/alice/code/myapp` (mac) and
10
+ * `D:\code\myapp` (windows) hashes to different directory names, so a
11
+ * naive rsync of `~/.claude/projects/` produces orphaned sessions.
12
+ *
13
+ * This module gives us:
14
+ * - The path-hash function (matches CC's algorithm).
15
+ * - A canonical "project key" derived from the git remote URL of the
16
+ * working directory. The remote is identical across machines, so it
17
+ * deterministically maps the same logical project on both sides.
18
+ * - Reverse lookup: given a project key + a registry of local repo
19
+ * locations, pick the right local cwd to install a session into.
20
+ */
21
+ import { execSync } from 'node:child_process';
22
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
23
+ import { dirname, basename, join } from 'node:path';
24
+ import { homedir } from 'node:os';
25
+ /** Encode a cwd into Claude Code's project-directory format. Reversible
26
+ * in spirit but lossy in practice (a cwd containing literal `-` chars
27
+ * decodes ambiguously). CC doesn't reverse it; it's a one-way hash. */
28
+ export function encodeProjectDir(cwd) {
29
+ return cwd.replace(/[/\\:.\s]+/g, '-');
30
+ }
31
+ /** Where Claude Code stores its session JSONL files. */
32
+ export function claudeProjectsRoot() {
33
+ return join(homedir(), '.claude', 'projects');
34
+ }
35
+ /** `~/.claude-sync/` lives next to `~/.claude/`. We keep our config and
36
+ * the local-project registry here. */
37
+ export function syncRoot() {
38
+ return join(homedir(), '.claude-sync');
39
+ }
40
+ /** Try to read the git remote URL of the cwd. Returns null when:
41
+ * - cwd isn't a git repo
42
+ * - git isn't installed
43
+ * - the repo has no remote configured (greenfield, not yet pushed)
44
+ *
45
+ * Trims trailing whitespace + newlines but otherwise leaves the URL
46
+ * exactly as `git` reported. We use it as a key, not a URL — so
47
+ * `git@github.com:owner/repo.git` and `https://github.com/owner/repo.git`
48
+ * for the same logical repo will NOT match. The user can fix that by
49
+ * normalizing one of the two via `git remote set-url`, or by manually
50
+ * registering both in the project registry under one canonical key. */
51
+ export function gitRemoteUrl(cwd) {
52
+ if (!existsSync(join(cwd, '.git')))
53
+ return null;
54
+ try {
55
+ const out = execSync('git remote get-url origin', {
56
+ cwd,
57
+ stdio: ['ignore', 'pipe', 'ignore'],
58
+ encoding: 'utf-8',
59
+ timeout: 5000,
60
+ });
61
+ return out.trim() || null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ /** Compute a stable, machine-portable project key for a cwd. Prefers the
68
+ * git remote URL; falls back to the directory basename when no remote
69
+ * is configured. The basename fallback is best-effort — collisions
70
+ * between unrelated projects with the same name are possible. */
71
+ export function projectKey(cwd) {
72
+ const remote = gitRemoteUrl(cwd);
73
+ if (remote)
74
+ return `git:${remote}`;
75
+ return `name:${basename(cwd)}`;
76
+ }
77
+ const REGISTRY_PATH = () => join(syncRoot(), 'projects.json');
78
+ export function loadRegistry() {
79
+ const path = REGISTRY_PATH();
80
+ if (!existsSync(path)) {
81
+ return {
82
+ _schemaVersion: 1,
83
+ machineName: 'unknown',
84
+ projects: {},
85
+ };
86
+ }
87
+ try {
88
+ const raw = readFileSync(path, 'utf-8');
89
+ const parsed = JSON.parse(raw);
90
+ if (parsed._schemaVersion !== 1) {
91
+ throw new Error(`Unsupported registry schema version: ${parsed._schemaVersion}`);
92
+ }
93
+ return parsed;
94
+ }
95
+ catch (err) {
96
+ throw new Error(`Failed to load registry at ${path}: ${err instanceof Error ? err.message : err}`);
97
+ }
98
+ }
99
+ export function saveRegistry(reg) {
100
+ const path = REGISTRY_PATH();
101
+ if (!existsSync(dirname(path)))
102
+ mkdirSync(dirname(path), { recursive: true });
103
+ // Atomic-ish write: tmp + rename. Prevents a half-written registry
104
+ // if the process is killed mid-save.
105
+ const tmp = `${path}.tmp`;
106
+ writeFileSync(tmp, JSON.stringify(reg, null, 2), { mode: 0o600 });
107
+ // renameSync is atomic on POSIX; on Windows it's atomic-ish but
108
+ // close enough for our uses (no concurrent registry writers).
109
+ renameSync(tmp, path);
110
+ }
111
+ /** Register a cwd under its computed project key. Idempotent; pushing
112
+ * the same cwd twice doesn't create duplicates. */
113
+ export function registerProject(cwd) {
114
+ const reg = loadRegistry();
115
+ const key = projectKey(cwd);
116
+ const existing = reg.projects[key] ?? [];
117
+ if (!existing.includes(cwd)) {
118
+ reg.projects[key] = [...existing, cwd];
119
+ saveRegistry(reg);
120
+ }
121
+ return { key, registry: reg };
122
+ }
123
+ /** Look up a local cwd for a given project key. Returns the first
124
+ * registered match (registries are user-edited; they can reorder if
125
+ * they want a specific clone preferred). Returns null when the key
126
+ * isn't in the registry — the caller should ask the user where to
127
+ * install. */
128
+ export function lookupCwd(key, reg) {
129
+ const r = reg ?? loadRegistry();
130
+ const matches = r.projects[key];
131
+ if (!matches || matches.length === 0)
132
+ return null;
133
+ // Filter to ones that still exist on disk; stale entries from deleted
134
+ // clones shouldn't trip up resolution.
135
+ const live = matches.filter((p) => existsSync(p));
136
+ return live[0] ?? null;
137
+ }
138
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Session-file utilities for Claude Code's `~/.claude/projects/` layout.
3
+ *
4
+ * Sessions are append-only JSONL files. Each line is one event (user
5
+ * message, assistant message, tool call, tool result, system event).
6
+ * We don't parse the events — we just shuffle JSONL lines around. The
7
+ * format is opaque to us by design: CC's schema can evolve without
8
+ * needing claude-sync changes.
9
+ */
10
+ import { basename } from 'node:path';
11
+ export interface SessionMetadata {
12
+ id: string;
13
+ /** Local file path. Useful for the CLI to print where it found / put
14
+ * a session, but not part of the wire format — paths don't transfer
15
+ * across machines. */
16
+ path: string;
17
+ /** Bytes on disk. */
18
+ size: number;
19
+ /** Wall-clock mtime. Used to break ties when picking "the most recent
20
+ * session" for a project. */
21
+ modifiedAt: number;
22
+ /** Number of newline-terminated JSONL records. Cheap proxy for
23
+ * message count — the CC schema means roughly one event per line. */
24
+ lineCount: number;
25
+ }
26
+ /** List sessions for a given local cwd. Returns [] if the project dir
27
+ * doesn't exist (no sessions yet). Sorted by mtime descending so the
28
+ * caller can grab `[0]` for "the most recent session". */
29
+ export declare function listSessions(cwd: string): SessionMetadata[];
30
+ /** Read a session's JSONL content. Returned as the raw bytes (no parse)
31
+ * so we don't have to know about CC's evolving message schema. */
32
+ export declare function readSession(path: string): string;
33
+ /** Write a session's JSONL into a target cwd's project dir. Creates the
34
+ * project dir if it doesn't exist; overwrites the session file if a
35
+ * file with the same id already exists.
36
+ *
37
+ * If `overwrite` is `false` (default) and a session with the same id
38
+ * exists at the target, throws. The caller should reconcile before
39
+ * importing — typically by renaming the incoming session to a fresh id
40
+ * via `assignFreshId`. */
41
+ export declare function writeSession(targetCwd: string, sessionId: string, jsonl: string, opts?: {
42
+ overwrite?: boolean;
43
+ }): string;
44
+ /** Generate a session id that doesn't collide with any existing session
45
+ * in the target cwd's project dir. CC session ids are UUIDs in
46
+ * practice; we mint a new one in the same shape so resume-by-id works
47
+ * consistently. */
48
+ export declare function assignFreshId(targetCwd: string, baseId: string): string;
49
+ export { basename as fileBasename };
50
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,EAAQ,QAAQ,EAAE,MAAM,WAAW,CAAC;AAG3C,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX;;2BAEuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb;kCAC8B;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB;0EACsE;IACtE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;2DAE2D;AAC3D,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CAsB3D;AAED;mEACmE;AACnE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;;;;0BAO0B;AAC1B,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,MAAM,CAYR;AAED;;;oBAGoB;AACpB,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CASvE;AAsBD,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,CAAC"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Session-file utilities for Claude Code's `~/.claude/projects/` layout.
3
+ *
4
+ * Sessions are append-only JSONL files. Each line is one event (user
5
+ * message, assistant message, tool call, tool result, system event).
6
+ * We don't parse the events — we just shuffle JSONL lines around. The
7
+ * format is opaque to us by design: CC's schema can evolve without
8
+ * needing claude-sync changes.
9
+ */
10
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync, openSync, readSync, closeSync, } from 'node:fs';
11
+ import { join, basename } from 'node:path';
12
+ import { claudeProjectsRoot, encodeProjectDir } from './project.js';
13
+ /** List sessions for a given local cwd. Returns [] if the project dir
14
+ * doesn't exist (no sessions yet). Sorted by mtime descending so the
15
+ * caller can grab `[0]` for "the most recent session". */
16
+ export function listSessions(cwd) {
17
+ const projDir = join(claudeProjectsRoot(), encodeProjectDir(cwd));
18
+ if (!existsSync(projDir))
19
+ return [];
20
+ const entries = readdirSync(projDir, { withFileTypes: true });
21
+ const sessions = [];
22
+ for (const entry of entries) {
23
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
24
+ continue;
25
+ const path = join(projDir, entry.name);
26
+ const stats = statSync(path);
27
+ sessions.push({
28
+ id: entry.name.replace(/\.jsonl$/, ''),
29
+ path,
30
+ size: stats.size,
31
+ modifiedAt: stats.mtimeMs,
32
+ lineCount: countLines(path),
33
+ });
34
+ }
35
+ sessions.sort((a, b) => b.modifiedAt - a.modifiedAt);
36
+ return sessions;
37
+ }
38
+ /** Read a session's JSONL content. Returned as the raw bytes (no parse)
39
+ * so we don't have to know about CC's evolving message schema. */
40
+ export function readSession(path) {
41
+ return readFileSync(path, 'utf-8');
42
+ }
43
+ /** Write a session's JSONL into a target cwd's project dir. Creates the
44
+ * project dir if it doesn't exist; overwrites the session file if a
45
+ * file with the same id already exists.
46
+ *
47
+ * If `overwrite` is `false` (default) and a session with the same id
48
+ * exists at the target, throws. The caller should reconcile before
49
+ * importing — typically by renaming the incoming session to a fresh id
50
+ * via `assignFreshId`. */
51
+ export function writeSession(targetCwd, sessionId, jsonl, opts = {}) {
52
+ const projDir = join(claudeProjectsRoot(), encodeProjectDir(targetCwd));
53
+ if (!existsSync(projDir))
54
+ mkdirSync(projDir, { recursive: true });
55
+ const targetPath = join(projDir, `${sessionId}.jsonl`);
56
+ if (existsSync(targetPath) && !opts.overwrite) {
57
+ throw new Error(`Session ${sessionId} already exists at ${targetPath}. ` +
58
+ `Pass { overwrite: true } to replace it, or assign a fresh id first.`);
59
+ }
60
+ writeFileSync(targetPath, jsonl);
61
+ return targetPath;
62
+ }
63
+ /** Generate a session id that doesn't collide with any existing session
64
+ * in the target cwd's project dir. CC session ids are UUIDs in
65
+ * practice; we mint a new one in the same shape so resume-by-id works
66
+ * consistently. */
67
+ export function assignFreshId(targetCwd, baseId) {
68
+ const existing = new Set(listSessions(targetCwd).map((s) => s.id));
69
+ if (!existing.has(baseId))
70
+ return baseId;
71
+ // Simplest unique-ifier: append a -copy / -copy-2 / ... suffix.
72
+ // This isn't UUID-shaped anymore, but `claude --resume` accepts any
73
+ // session id and CC re-keys on file presence rather than format.
74
+ let n = 1;
75
+ while (existing.has(`${baseId}-copy${n === 1 ? '' : `-${n}`}`))
76
+ n++;
77
+ return `${baseId}-copy${n === 1 ? '' : `-${n}`}`;
78
+ }
79
+ function countLines(path) {
80
+ // Fast newline count without buffering the whole file as a string —
81
+ // sessions can be large.
82
+ const fd = openSync(path, 'r');
83
+ try {
84
+ const buf = Buffer.alloc(64 * 1024);
85
+ let count = 0;
86
+ let bytesRead = 0;
87
+ do {
88
+ bytesRead = readSync(fd, buf, 0, buf.length, null);
89
+ for (let i = 0; i < bytesRead; i++) {
90
+ if (buf[i] === 0x0a /* \n */)
91
+ count++;
92
+ }
93
+ } while (bytesRead > 0);
94
+ return count;
95
+ }
96
+ finally {
97
+ closeSync(fd);
98
+ }
99
+ }
100
+ export { basename as fileBasename };
101
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}