@askalf/claude-sync 0.0.2 → 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/dist/session.js CHANGED
@@ -7,39 +7,70 @@
7
7
  * format is opaque to us by design: CC's schema can evolve without
8
8
  * needing claude-sync changes.
9
9
  */
10
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync, openSync, readSync, closeSync, } from 'node:fs';
10
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync, openSync, readSync, closeSync, renameSync, } from 'node:fs';
11
11
  import { join, basename } from 'node:path';
12
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) {
13
+ /** Stat-only listing for a cwd id, path, size, mtime, no line count.
14
+ * Returns [] if the project dir doesn't exist. Sorted by mtime
15
+ * descending. This is the cheap path: watch mode polls it every tick to
16
+ * spot changed sessions WITHOUT reading every file's contents to count
17
+ * lines (which `listSessions` does). */
18
+ export function listSessionStats(cwd) {
17
19
  const projDir = join(claudeProjectsRoot(), encodeProjectDir(cwd));
18
20
  if (!existsSync(projDir))
19
21
  return [];
20
22
  const entries = readdirSync(projDir, { withFileTypes: true });
21
- const sessions = [];
23
+ const stats = [];
22
24
  for (const entry of entries) {
23
25
  if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
24
26
  continue;
25
27
  const path = join(projDir, entry.name);
26
- const stats = statSync(path);
27
- sessions.push({
28
+ const st = statSync(path);
29
+ stats.push({
28
30
  id: entry.name.replace(/\.jsonl$/, ''),
29
31
  path,
30
- size: stats.size,
31
- modifiedAt: stats.mtimeMs,
32
- lineCount: countLines(path),
32
+ size: st.size,
33
+ modifiedAt: st.mtimeMs,
33
34
  });
34
35
  }
35
- sessions.sort((a, b) => b.modifiedAt - a.modifiedAt);
36
- return sessions;
36
+ stats.sort((a, b) => b.modifiedAt - a.modifiedAt);
37
+ return stats;
38
+ }
39
+ /** List sessions for a given local cwd, including line counts. Returns []
40
+ * if the project dir doesn't exist (no sessions yet). Sorted by mtime
41
+ * descending so the caller can grab `[0]` for "the most recent session". */
42
+ export function listSessions(cwd) {
43
+ return listSessionStats(cwd).map((s) => ({ ...s, lineCount: countLines(s.path) }));
37
44
  }
38
45
  /** Read a session's JSONL content. Returned as the raw bytes (no parse)
39
46
  * so we don't have to know about CC's evolving message schema. */
40
47
  export function readSession(path) {
41
48
  return readFileSync(path, 'utf-8');
42
49
  }
50
+ /** A session id is used verbatim as a filename segment (`<id>.jsonl`).
51
+ * Since the id can originate from a synced, attacker-influenceable
52
+ * `.ccsync` file (see `installCcsync` in cli.ts), it MUST be a single,
53
+ * benign path segment — never a path that could escape the project dir
54
+ * via `..`, an absolute path, or an embedded separator. CC session ids
55
+ * are UUIDs in practice; `assignFreshId` may append `-copy` suffixes.
56
+ * Both fit comfortably inside this allowlist. */
57
+ const SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
58
+ /** Throw unless `sessionId` is a safe single path segment. Rejects
59
+ * empty ids, anything containing `/`, `\`, or `..`, and anything with
60
+ * characters outside the conservative allowlist. */
61
+ export function assertSafeSessionId(sessionId) {
62
+ if (!sessionId ||
63
+ sessionId === '.' ||
64
+ sessionId === '..' ||
65
+ sessionId.includes('/') ||
66
+ sessionId.includes('\\') ||
67
+ sessionId.includes('..') ||
68
+ !SAFE_SESSION_ID.test(sessionId)) {
69
+ throw new Error(`Refusing to write session: unsafe session id ${JSON.stringify(sessionId)}. ` +
70
+ `Session ids must be a single path segment matching ${SAFE_SESSION_ID} ` +
71
+ `(no \`/\`, \`\\\`, or \`..\`).`);
72
+ }
73
+ }
43
74
  /** Write a session's JSONL into a target cwd's project dir. Creates the
44
75
  * project dir if it doesn't exist; overwrites the session file if a
45
76
  * file with the same id already exists.
@@ -49,6 +80,9 @@ export function readSession(path) {
49
80
  * importing — typically by renaming the incoming session to a fresh id
50
81
  * via `assignFreshId`. */
51
82
  export function writeSession(targetCwd, sessionId, jsonl, opts = {}) {
83
+ // Validate BEFORE building any path: the id is interpolated into a
84
+ // filename and a malicious `../../x` would escape projDir otherwise.
85
+ assertSafeSessionId(sessionId);
52
86
  const projDir = join(claudeProjectsRoot(), encodeProjectDir(targetCwd));
53
87
  if (!existsSync(projDir))
54
88
  mkdirSync(projDir, { recursive: true });
@@ -57,7 +91,12 @@ export function writeSession(targetCwd, sessionId, jsonl, opts = {}) {
57
91
  throw new Error(`Session ${sessionId} already exists at ${targetPath}. ` +
58
92
  `Pass { overwrite: true } to replace it, or assign a fresh id first.`);
59
93
  }
60
- writeFileSync(targetPath, jsonl);
94
+ // Atomic write: tmp + rename, with 0o600 perms (session JSONL holds
95
+ // conversation data). Mirrors transport.ts's pushToTransport so a
96
+ // mid-snapshot sync provider never sees a half-written file.
97
+ const tmp = `${targetPath}.tmp`;
98
+ writeFileSync(tmp, jsonl, { mode: 0o600 });
99
+ renameSync(tmp, targetPath);
61
100
  return targetPath;
62
101
  }
63
102
  /** Generate a session id that doesn't collide with any existing session
@@ -76,6 +115,12 @@ export function assignFreshId(targetCwd, baseId) {
76
115
  n++;
77
116
  return `${baseId}-copy${n === 1 ? '' : `-${n}`}`;
78
117
  }
118
+ /** Count newline-terminated records in a session file. Exported for watch
119
+ * mode, which counts lines only for the handful of sessions that changed
120
+ * since the last tick (the bulk listing stays on `listSessionStats`). */
121
+ export function countSessionLines(path) {
122
+ return countLines(path);
123
+ }
79
124
  function countLines(path) {
80
125
  // Fast newline count without buffering the whole file as a string —
81
126
  // sessions can be large.
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
@@ -7,10 +7,11 @@
7
7
  * <encoded-project-key>/
8
8
  * <sessionId>-<machineName>.ccsync
9
9
  *
10
- * Project key is encoded the same way CC encodes cwds — `[/\\:.\s]+`
11
- * collapsed to `-` so `git:https://github.com/owner/repo.git` becomes
12
- * `git-https---github-com-owner-repo-git`. Reversible enough for human
13
- * inspection.
10
+ * Project key is encoded the same way CC encodes cwds — each
11
+ * non-alphanumeric character becomes a single `-` (see
12
+ * `encodeProjectDir`) — so `git:https://github.com/owner/repo.git`
13
+ * becomes `git-https---github-com-owner-repo-git`. Reversible enough
14
+ * for human inspection.
14
15
  *
15
16
  * Multiple machines can push into the same project subdir without
16
17
  * collision: each writes a file named with its own machineName, and
@@ -33,6 +34,13 @@ export interface TransportEntry {
33
34
  * `selfMachineName`) are omitted — pull is for receiving OTHER
34
35
  * machines' work, not echoing your own. */
35
36
  export declare function listTransport(syncDir: string, projectKey: string, selfMachineName: string): TransportEntry[];
37
+ /** Tally `machineName` → file count across every `.ccsync` in the
38
+ * transport (all projects). Used by `doctor` to catch the
39
+ * duplicate-machineName footgun: `pull` self-filters files whose
40
+ * machineName matches this machine's, so if two machines share a name —
41
+ * e.g. both defaulted to the same hostname — pull silently imports
42
+ * nothing. Malformed files are skipped quietly. */
43
+ export declare function listTransportMachines(syncDir: string): Map<string, number>;
36
44
  /** List all project keys present in the transport. Used by the CLI's
37
45
  * `pull` (no args) form to fan out across every known project. */
38
46
  export declare function listProjectKeys(syncDir: string): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAQH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED;;;gBAGgB;AAChB,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAWzE;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;4CAG4C;AAC5C,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,MAAM,GACtB,cAAc,EAAE,CA0BlB;AAED;mEACmE;AACnE,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAWzD"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAQH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED;;;gBAGgB;AAChB,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAWzE;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;4CAG4C;AAC5C,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,MAAM,GACtB,cAAc,EAAE,CA0BlB;AAED;;;;;oDAKoD;AACpD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAkB1E;AAED;mEACmE;AACnE,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAWzD"}
package/dist/transport.js CHANGED
@@ -7,10 +7,11 @@
7
7
  * <encoded-project-key>/
8
8
  * <sessionId>-<machineName>.ccsync
9
9
  *
10
- * Project key is encoded the same way CC encodes cwds — `[/\\:.\s]+`
11
- * collapsed to `-` so `git:https://github.com/owner/repo.git` becomes
12
- * `git-https---github-com-owner-repo-git`. Reversible enough for human
13
- * inspection.
10
+ * Project key is encoded the same way CC encodes cwds — each
11
+ * non-alphanumeric character becomes a single `-` (see
12
+ * `encodeProjectDir`) — so `git:https://github.com/owner/repo.git`
13
+ * becomes `git-https---github-com-owner-repo-git`. Reversible enough
14
+ * for human inspection.
14
15
  *
15
16
  * Multiple machines can push into the same project subdir without
16
17
  * collision: each writes a file named with its own machineName, and
@@ -72,6 +73,34 @@ export function listTransport(syncDir, projectKey, selfMachineName) {
72
73
  result.sort((a, b) => b.mtime - a.mtime);
73
74
  return result;
74
75
  }
76
+ /** Tally `machineName` → file count across every `.ccsync` in the
77
+ * transport (all projects). Used by `doctor` to catch the
78
+ * duplicate-machineName footgun: `pull` self-filters files whose
79
+ * machineName matches this machine's, so if two machines share a name —
80
+ * e.g. both defaulted to the same hostname — pull silently imports
81
+ * nothing. Malformed files are skipped quietly. */
82
+ export function listTransportMachines(syncDir) {
83
+ const counts = new Map();
84
+ if (!existsSync(syncDir))
85
+ return counts;
86
+ for (const sub of readdirSync(syncDir, { withFileTypes: true })) {
87
+ if (!sub.isDirectory())
88
+ continue;
89
+ const dir = join(syncDir, sub.name);
90
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
91
+ if (!entry.isFile() || !entry.name.endsWith('.ccsync'))
92
+ continue;
93
+ try {
94
+ const file = parseCcsync(readFileSync(join(dir, entry.name), 'utf-8'));
95
+ counts.set(file.machineName, (counts.get(file.machineName) ?? 0) + 1);
96
+ }
97
+ catch {
98
+ // Shared dir may hold in-progress writes / stale tmp files.
99
+ }
100
+ }
101
+ }
102
+ return counts;
103
+ }
75
104
  /** List all project keys present in the transport. Used by the CLI's
76
105
  * `pull` (no args) form to fan out across every known project. */
77
106
  export function listProjectKeys(syncDir) {
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@askalf/claude-sync",
3
- "version": "0.0.2",
4
- "description": "Sync Claude Code sessions across machines. Pack a local CC session into a portable .ccsync file, ship it via Dropbox / iCloud / Syncthing / a USB stick, unpack on the other side. Path-hash mismatches solved via git-remote-url as canonical project key.",
3
+ "version": "0.1.0",
4
+ "description": "own your sessions — move Claude Code sessions across machines. Part of Own Your Stack.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "claude-sync": "./dist/cli.js"
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc",
24
- "test": "node --test test/project.test.mjs test/format.test.mjs test/session.test.mjs test/cli.test.mjs",
24
+ "test": "node --test test/project.test.mjs test/format.test.mjs test/session.test.mjs test/transport.test.mjs test/cli.test.mjs test/watch.test.mjs test/version.test.mjs",
25
25
  "typecheck": "tsc --noEmit",
26
26
  "audit": "npm audit --production --audit-level=high",
27
27
  "prepublishOnly": "npm run build",