@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/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +338 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +59 -0
- package/dist/config.js.map +1 -0
- package/dist/format.d.ts +51 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +72 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/project.d.ts +76 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +138 -0
- package/dist/project.js.map +1 -0
- package/dist/session.d.ts +50 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +101 -0
- package/dist/session.js.map +1 -0
- package/dist/transport.d.ts +39 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +90 -0
- package/dist/transport.js.map +1 -0
- package/package.json +57 -0
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"}
|
package/dist/format.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
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, 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"}
|
package/dist/project.js
ADDED
|
@@ -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"}
|
package/dist/session.js
ADDED
|
@@ -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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"}
|