@zzusp/ccsm 1.0.1 → 1.0.2

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.
Files changed (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +236 -236
  3. package/bin/cli.mjs +52 -52
  4. package/dist/assets/{DiskUsage-CKhggLs5.js → DiskUsage-BY6XwffG.js} +2 -2
  5. package/dist/assets/DiskUsage-BY6XwffG.js.map +1 -0
  6. package/dist/assets/{ImportPage-wge4VhZ-.js → ImportPage-Cwq5bx7G.js} +2 -2
  7. package/dist/assets/ImportPage-Cwq5bx7G.js.map +1 -0
  8. package/dist/assets/MarkdownContent-BFu7Nkk_.js +2 -0
  9. package/dist/assets/MarkdownContent-BFu7Nkk_.js.map +1 -0
  10. package/dist/assets/{ProjectMemory-Q4XX40j_.js → ProjectMemory-CcE3KbUK.js} +2 -2
  11. package/dist/assets/ProjectMemory-CcE3KbUK.js.map +1 -0
  12. package/dist/assets/index-CrWxV6sb.css +1 -0
  13. package/dist/assets/index-DTbWl1jb.js +11 -0
  14. package/dist/assets/index-DTbWl1jb.js.map +1 -0
  15. package/dist/assets/markdown-Bag5rX3T.js +30 -0
  16. package/dist/assets/markdown-Bag5rX3T.js.map +1 -0
  17. package/dist/index.html +26 -26
  18. package/package.json +85 -83
  19. package/server/index.ts +130 -130
  20. package/server/lib/active-sessions.test.ts +119 -119
  21. package/server/lib/active-sessions.ts +95 -95
  22. package/server/lib/bundle.test.ts +182 -182
  23. package/server/lib/bundle.ts +86 -86
  24. package/server/lib/claude-paths.test.ts +126 -126
  25. package/server/lib/claude-paths.ts +43 -43
  26. package/server/lib/cleanup-suggestions.ts +131 -131
  27. package/server/lib/constants.ts +8 -8
  28. package/server/lib/delete-project.ts +100 -100
  29. package/server/lib/delete.test.ts +244 -244
  30. package/server/lib/delete.ts +192 -192
  31. package/server/lib/disk-usage.ts +81 -81
  32. package/server/lib/encode-cwd.ts +24 -24
  33. package/server/lib/export-bundle.ts +236 -236
  34. package/server/lib/export-import-bundle.test.ts +337 -337
  35. package/server/lib/fs-size.ts +38 -38
  36. package/server/lib/import-bundle.ts +488 -488
  37. package/server/lib/load-memory.ts +120 -120
  38. package/server/lib/load-session.ts +209 -209
  39. package/server/lib/modified-files.test.ts +280 -280
  40. package/server/lib/modified-files.ts +228 -228
  41. package/server/lib/open-folder.ts +47 -47
  42. package/server/lib/parse-jsonl.ts +160 -139
  43. package/server/lib/port.ts +23 -23
  44. package/server/lib/safe-id.test.ts +41 -41
  45. package/server/lib/safe-id.ts +6 -6
  46. package/server/lib/safe-remove.test.ts +73 -73
  47. package/server/lib/safe-remove.ts +25 -25
  48. package/server/lib/scan.ts +289 -286
  49. package/server/lib/search-all.ts +130 -130
  50. package/server/lib/search-session.ts +203 -203
  51. package/server/lib/system-tags.ts +20 -20
  52. package/server/lib/update.ts +67 -67
  53. package/server/lib/version.test.ts +39 -39
  54. package/server/lib/version.ts +117 -117
  55. package/server/routes/disk-cleanup.ts +54 -54
  56. package/server/routes/disk.ts +9 -9
  57. package/server/routes/import.ts +87 -87
  58. package/server/routes/projects.ts +104 -104
  59. package/server/routes/search.ts +79 -79
  60. package/server/routes/sessions.ts +130 -130
  61. package/server/routes/version.ts +34 -34
  62. package/server/types.ts +1 -1
  63. package/shared/constants.ts +7 -7
  64. package/shared/types.ts +513 -511
  65. package/dist/assets/DiskUsage-CKhggLs5.js.map +0 -1
  66. package/dist/assets/ImportPage-wge4VhZ-.js.map +0 -1
  67. package/dist/assets/ProjectMemory-Q4XX40j_.js.map +0 -1
  68. package/dist/assets/index-7aMrnHJG.js +0 -7
  69. package/dist/assets/index-7aMrnHJG.js.map +0 -1
  70. package/dist/assets/index-BOeI_J4B.css +0 -1
@@ -1,236 +1,236 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import readline from 'node:readline';
4
- import { isUnderClaudeRoot, PATHS } from './claude-paths.ts';
5
- import { parseJsonlMeta } from './parse-jsonl.ts';
6
- import { isSafeId } from './safe-id.ts';
7
- import { resolveProjectCwd } from './scan.ts';
8
- import { rewriteLineField, sha256, sha256File, SENTINEL, transformFile } from './bundle.ts';
9
- import {
10
- BUNDLE_KIND,
11
- BUNDLE_SCHEMA_VERSION,
12
- type BundleManifest,
13
- type BundleMemoryFileMeta,
14
- type BundleMemoryInventory,
15
- type BundleSessionMeta,
16
- type ExportResult,
17
- } from '../types.ts';
18
-
19
- const JSONL_EXT = '.jsonl';
20
-
21
- export class ExportError extends Error {}
22
-
23
- /**
24
- * Write a path-independent bundle of a project's memory + conversations to
25
- * `destDir`. The structural project root is replaced with {@link SENTINEL} so the
26
- * bundle can be moved to a device where the project lives at a different path.
27
- */
28
- export async function exportBundle(
29
- projectId: string,
30
- sessionIds: string[] | 'all',
31
- destDir: string,
32
- ): Promise<ExportResult> {
33
- if (!isSafeId(projectId)) throw new ExportError('invalid project id');
34
-
35
- const projectDir = path.join(PATHS.projects, projectId);
36
- if (!fs.existsSync(projectDir)) throw new ExportError('project not found');
37
-
38
- // The real source cwd — the golden record we replace with the sentinel.
39
- const resolved = await resolveProjectCwd(projectId);
40
- if (!resolved) throw new ExportError('project not found');
41
- const sourceCwd = resolved.decoded;
42
-
43
- // Never write the bundle inside ~/.claude — that would corrupt the store.
44
- const dest = path.resolve(destDir);
45
- if (!path.isAbsolute(dest)) throw new ExportError('destination must be an absolute path');
46
- if (isUnderClaudeRoot(dest)) throw new ExportError('destination must be outside ~/.claude');
47
- prepareDestDir(dest);
48
-
49
- const allIds = listSessionIds(projectDir);
50
- const ids =
51
- sessionIds === 'all'
52
- ? allIds
53
- : sessionIds.filter((id) => isSafeId(id) && allIds.includes(id));
54
- if (ids.length === 0) throw new ExportError('no sessions to export');
55
- const idSet = new Set(ids);
56
-
57
- // Scan history.jsonl once, bucketing matching lines (project -> sentinel) by sid.
58
- const historyBuckets = await bucketHistoryLines(idSet, sourceCwd);
59
-
60
- const sessionsDir = path.join(dest, 'sessions');
61
- fs.mkdirSync(sessionsDir, { recursive: true });
62
-
63
- let totalBytes = 0;
64
- let historyLinesExported = 0;
65
- const sessionMetas: BundleSessionMeta[] = [];
66
-
67
- for (const id of ids) {
68
- const jsonlPath = path.join(projectDir, `${id}${JSONL_EXT}`);
69
- if (!fs.existsSync(jsonlPath)) continue;
70
-
71
- const sessDir = path.join(sessionsDir, id);
72
- fs.mkdirSync(sessDir, { recursive: true });
73
-
74
- const convPath = path.join(sessDir, 'conversation.jsonl');
75
- const conv = await transformFile(jsonlPath, convPath, 'cwd', sourceCwd, SENTINEL);
76
- const convBytes = fs.statSync(convPath).size;
77
- totalBytes += convBytes;
78
-
79
- let history: BundleSessionMeta['history'] = null;
80
- const bucket = historyBuckets.get(id);
81
- if (bucket && bucket.length > 0) {
82
- const content = bucket.join('\n') + '\n';
83
- fs.writeFileSync(path.join(sessDir, 'history.ndjson'), content, 'utf8');
84
- const histBytes = Buffer.byteLength(content, 'utf8');
85
- totalBytes += histBytes;
86
- historyLinesExported += bucket.length;
87
- history = { sha256: sha256(content), lines: bucket.length, bytes: histBytes };
88
- }
89
-
90
- const meta = await parseJsonlMeta(jsonlPath);
91
- sessionMetas.push({
92
- sessionId: id,
93
- title: meta.title,
94
- customTitle: meta.customTitle,
95
- firstAt: meta.firstAt,
96
- lastAt: meta.lastAt,
97
- messageCount: meta.messageCount,
98
- cwdRewritten: meta.cwdFromMessages === sourceCwd,
99
- conversation: { sha256: conv.sha256, lines: conv.lines, bytes: convBytes },
100
- history,
101
- });
102
- }
103
-
104
- const memory = copyMemory(projectId, dest);
105
- totalBytes += memory.bytes;
106
-
107
- const manifest: BundleManifest = {
108
- schemaVersion: BUNDLE_SCHEMA_VERSION,
109
- kind: BUNDLE_KIND,
110
- exportedAt: new Date().toISOString(),
111
- placeholder: SENTINEL,
112
- source: {
113
- platform: process.platform,
114
- pathSep: path.sep,
115
- projectId,
116
- cwd: sourceCwd,
117
- cwdResolvedAtExport: resolved.resolved,
118
- },
119
- memory: memory.inventory,
120
- sessions: sessionMetas,
121
- };
122
- const manifestStr = JSON.stringify(manifest, null, 2);
123
- fs.writeFileSync(path.join(dest, 'manifest.json'), manifestStr, 'utf8');
124
- totalBytes += Buffer.byteLength(manifestStr, 'utf8');
125
-
126
- return {
127
- destDir: dest,
128
- sessionsExported: sessionMetas.length,
129
- memoryFilesExported: memory.inventory.entries.length,
130
- historyLinesExported,
131
- totalBytes,
132
- };
133
- }
134
-
135
- function listSessionIds(projectDir: string): string[] {
136
- const ids: string[] = [];
137
- for (const ent of fs.readdirSync(projectDir, { withFileTypes: true })) {
138
- if (ent.isFile() && ent.name.endsWith(JSONL_EXT)) {
139
- ids.push(ent.name.slice(0, -JSONL_EXT.length));
140
- }
141
- }
142
- return ids;
143
- }
144
-
145
- /** Create dest if absent; allow an empty dir or a prior bundle (re-export). */
146
- function prepareDestDir(dest: string): void {
147
- if (!fs.existsSync(dest)) {
148
- fs.mkdirSync(dest, { recursive: true });
149
- return;
150
- }
151
- if (!fs.statSync(dest).isDirectory()) throw new ExportError('destination is not a directory');
152
- if (fs.readdirSync(dest).length === 0) return;
153
- if (!isPriorBundle(path.join(dest, 'manifest.json'))) {
154
- throw new ExportError('destination is not empty (and not a prior bundle)');
155
- }
156
- for (const name of ['manifest.json', 'sessions', 'memory']) {
157
- fs.rmSync(path.join(dest, name), { recursive: true, force: true });
158
- }
159
- }
160
-
161
- function isPriorBundle(manifestPath: string): boolean {
162
- try {
163
- const obj = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as { kind?: unknown };
164
- return obj.kind === BUNDLE_KIND;
165
- } catch {
166
- return false;
167
- }
168
- }
169
-
170
- async function bucketHistoryLines(
171
- idSet: Set<string>,
172
- sourceCwd: string,
173
- ): Promise<Map<string, string[]>> {
174
- const buckets = new Map<string, string[]>();
175
- if (!fs.existsSync(PATHS.history)) return buckets;
176
-
177
- const rl = readline.createInterface({
178
- input: fs.createReadStream(PATHS.history, { encoding: 'utf8' }),
179
- crlfDelay: Infinity,
180
- });
181
- for await (const raw of rl) {
182
- if (!raw.trim()) continue;
183
- let sid: unknown;
184
- try {
185
- sid = (JSON.parse(raw) as { sessionId?: unknown }).sessionId;
186
- } catch {
187
- continue;
188
- }
189
- if (typeof sid !== 'string' || !idSet.has(sid)) continue;
190
- let bucket = buckets.get(sid);
191
- if (!bucket) buckets.set(sid, (bucket = []));
192
- bucket.push(rewriteLineField(raw, 'project', sourceCwd, SENTINEL));
193
- }
194
- return buckets;
195
- }
196
-
197
- function copyMemory(
198
- projectId: string,
199
- dest: string,
200
- ): { inventory: BundleMemoryInventory; bytes: number } {
201
- const memDir = path.join(PATHS.projects, projectId, 'memory');
202
- const inventory: BundleMemoryInventory = { hasIndex: false, entries: [] };
203
- let bytes = 0;
204
- if (!fs.existsSync(memDir)) return { inventory, bytes };
205
-
206
- const outDir = path.join(dest, 'memory');
207
- let made = false;
208
- for (const filename of fs.readdirSync(memDir)) {
209
- if (!filename.toLowerCase().endsWith('.md')) continue;
210
- const src = path.join(memDir, filename);
211
- let stat;
212
- try {
213
- stat = fs.statSync(src);
214
- } catch {
215
- continue;
216
- }
217
- if (!stat.isFile()) continue;
218
-
219
- if (!made) {
220
- fs.mkdirSync(outDir, { recursive: true });
221
- made = true;
222
- }
223
- fs.copyFileSync(src, path.join(outDir, filename));
224
- const isIndex = filename.toLowerCase() === 'memory.md';
225
- const entry: BundleMemoryFileMeta = {
226
- filename,
227
- isIndex,
228
- sha256: sha256File(src),
229
- bytes: stat.size,
230
- };
231
- bytes += stat.size;
232
- if (isIndex) inventory.hasIndex = true;
233
- inventory.entries.push(entry);
234
- }
235
- return { inventory, bytes };
236
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { isUnderClaudeRoot, PATHS } from './claude-paths.ts';
5
+ import { parseJsonlMeta } from './parse-jsonl.ts';
6
+ import { isSafeId } from './safe-id.ts';
7
+ import { resolveProjectCwd } from './scan.ts';
8
+ import { rewriteLineField, sha256, sha256File, SENTINEL, transformFile } from './bundle.ts';
9
+ import {
10
+ BUNDLE_KIND,
11
+ BUNDLE_SCHEMA_VERSION,
12
+ type BundleManifest,
13
+ type BundleMemoryFileMeta,
14
+ type BundleMemoryInventory,
15
+ type BundleSessionMeta,
16
+ type ExportResult,
17
+ } from '../types.ts';
18
+
19
+ const JSONL_EXT = '.jsonl';
20
+
21
+ export class ExportError extends Error {}
22
+
23
+ /**
24
+ * Write a path-independent bundle of a project's memory + conversations to
25
+ * `destDir`. The structural project root is replaced with {@link SENTINEL} so the
26
+ * bundle can be moved to a device where the project lives at a different path.
27
+ */
28
+ export async function exportBundle(
29
+ projectId: string,
30
+ sessionIds: string[] | 'all',
31
+ destDir: string,
32
+ ): Promise<ExportResult> {
33
+ if (!isSafeId(projectId)) throw new ExportError('invalid project id');
34
+
35
+ const projectDir = path.join(PATHS.projects, projectId);
36
+ if (!fs.existsSync(projectDir)) throw new ExportError('project not found');
37
+
38
+ // The real source cwd — the golden record we replace with the sentinel.
39
+ const resolved = await resolveProjectCwd(projectId);
40
+ if (!resolved) throw new ExportError('project not found');
41
+ const sourceCwd = resolved.decoded;
42
+
43
+ // Never write the bundle inside ~/.claude — that would corrupt the store.
44
+ const dest = path.resolve(destDir);
45
+ if (!path.isAbsolute(dest)) throw new ExportError('destination must be an absolute path');
46
+ if (isUnderClaudeRoot(dest)) throw new ExportError('destination must be outside ~/.claude');
47
+ prepareDestDir(dest);
48
+
49
+ const allIds = listSessionIds(projectDir);
50
+ const ids =
51
+ sessionIds === 'all'
52
+ ? allIds
53
+ : sessionIds.filter((id) => isSafeId(id) && allIds.includes(id));
54
+ if (ids.length === 0) throw new ExportError('no sessions to export');
55
+ const idSet = new Set(ids);
56
+
57
+ // Scan history.jsonl once, bucketing matching lines (project -> sentinel) by sid.
58
+ const historyBuckets = await bucketHistoryLines(idSet, sourceCwd);
59
+
60
+ const sessionsDir = path.join(dest, 'sessions');
61
+ fs.mkdirSync(sessionsDir, { recursive: true });
62
+
63
+ let totalBytes = 0;
64
+ let historyLinesExported = 0;
65
+ const sessionMetas: BundleSessionMeta[] = [];
66
+
67
+ for (const id of ids) {
68
+ const jsonlPath = path.join(projectDir, `${id}${JSONL_EXT}`);
69
+ if (!fs.existsSync(jsonlPath)) continue;
70
+
71
+ const sessDir = path.join(sessionsDir, id);
72
+ fs.mkdirSync(sessDir, { recursive: true });
73
+
74
+ const convPath = path.join(sessDir, 'conversation.jsonl');
75
+ const conv = await transformFile(jsonlPath, convPath, 'cwd', sourceCwd, SENTINEL);
76
+ const convBytes = fs.statSync(convPath).size;
77
+ totalBytes += convBytes;
78
+
79
+ let history: BundleSessionMeta['history'] = null;
80
+ const bucket = historyBuckets.get(id);
81
+ if (bucket && bucket.length > 0) {
82
+ const content = bucket.join('\n') + '\n';
83
+ fs.writeFileSync(path.join(sessDir, 'history.ndjson'), content, 'utf8');
84
+ const histBytes = Buffer.byteLength(content, 'utf8');
85
+ totalBytes += histBytes;
86
+ historyLinesExported += bucket.length;
87
+ history = { sha256: sha256(content), lines: bucket.length, bytes: histBytes };
88
+ }
89
+
90
+ const meta = await parseJsonlMeta(jsonlPath);
91
+ sessionMetas.push({
92
+ sessionId: id,
93
+ title: meta.title,
94
+ customTitle: meta.customTitle,
95
+ firstAt: meta.firstAt,
96
+ lastAt: meta.lastAt,
97
+ messageCount: meta.messageCount,
98
+ cwdRewritten: meta.cwdFromMessages === sourceCwd,
99
+ conversation: { sha256: conv.sha256, lines: conv.lines, bytes: convBytes },
100
+ history,
101
+ });
102
+ }
103
+
104
+ const memory = copyMemory(projectId, dest);
105
+ totalBytes += memory.bytes;
106
+
107
+ const manifest: BundleManifest = {
108
+ schemaVersion: BUNDLE_SCHEMA_VERSION,
109
+ kind: BUNDLE_KIND,
110
+ exportedAt: new Date().toISOString(),
111
+ placeholder: SENTINEL,
112
+ source: {
113
+ platform: process.platform,
114
+ pathSep: path.sep,
115
+ projectId,
116
+ cwd: sourceCwd,
117
+ cwdResolvedAtExport: resolved.resolved,
118
+ },
119
+ memory: memory.inventory,
120
+ sessions: sessionMetas,
121
+ };
122
+ const manifestStr = JSON.stringify(manifest, null, 2);
123
+ fs.writeFileSync(path.join(dest, 'manifest.json'), manifestStr, 'utf8');
124
+ totalBytes += Buffer.byteLength(manifestStr, 'utf8');
125
+
126
+ return {
127
+ destDir: dest,
128
+ sessionsExported: sessionMetas.length,
129
+ memoryFilesExported: memory.inventory.entries.length,
130
+ historyLinesExported,
131
+ totalBytes,
132
+ };
133
+ }
134
+
135
+ function listSessionIds(projectDir: string): string[] {
136
+ const ids: string[] = [];
137
+ for (const ent of fs.readdirSync(projectDir, { withFileTypes: true })) {
138
+ if (ent.isFile() && ent.name.endsWith(JSONL_EXT)) {
139
+ ids.push(ent.name.slice(0, -JSONL_EXT.length));
140
+ }
141
+ }
142
+ return ids;
143
+ }
144
+
145
+ /** Create dest if absent; allow an empty dir or a prior bundle (re-export). */
146
+ function prepareDestDir(dest: string): void {
147
+ if (!fs.existsSync(dest)) {
148
+ fs.mkdirSync(dest, { recursive: true });
149
+ return;
150
+ }
151
+ if (!fs.statSync(dest).isDirectory()) throw new ExportError('destination is not a directory');
152
+ if (fs.readdirSync(dest).length === 0) return;
153
+ if (!isPriorBundle(path.join(dest, 'manifest.json'))) {
154
+ throw new ExportError('destination is not empty (and not a prior bundle)');
155
+ }
156
+ for (const name of ['manifest.json', 'sessions', 'memory']) {
157
+ fs.rmSync(path.join(dest, name), { recursive: true, force: true });
158
+ }
159
+ }
160
+
161
+ function isPriorBundle(manifestPath: string): boolean {
162
+ try {
163
+ const obj = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as { kind?: unknown };
164
+ return obj.kind === BUNDLE_KIND;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ async function bucketHistoryLines(
171
+ idSet: Set<string>,
172
+ sourceCwd: string,
173
+ ): Promise<Map<string, string[]>> {
174
+ const buckets = new Map<string, string[]>();
175
+ if (!fs.existsSync(PATHS.history)) return buckets;
176
+
177
+ const rl = readline.createInterface({
178
+ input: fs.createReadStream(PATHS.history, { encoding: 'utf8' }),
179
+ crlfDelay: Infinity,
180
+ });
181
+ for await (const raw of rl) {
182
+ if (!raw.trim()) continue;
183
+ let sid: unknown;
184
+ try {
185
+ sid = (JSON.parse(raw) as { sessionId?: unknown }).sessionId;
186
+ } catch {
187
+ continue;
188
+ }
189
+ if (typeof sid !== 'string' || !idSet.has(sid)) continue;
190
+ let bucket = buckets.get(sid);
191
+ if (!bucket) buckets.set(sid, (bucket = []));
192
+ bucket.push(rewriteLineField(raw, 'project', sourceCwd, SENTINEL));
193
+ }
194
+ return buckets;
195
+ }
196
+
197
+ function copyMemory(
198
+ projectId: string,
199
+ dest: string,
200
+ ): { inventory: BundleMemoryInventory; bytes: number } {
201
+ const memDir = path.join(PATHS.projects, projectId, 'memory');
202
+ const inventory: BundleMemoryInventory = { hasIndex: false, entries: [] };
203
+ let bytes = 0;
204
+ if (!fs.existsSync(memDir)) return { inventory, bytes };
205
+
206
+ const outDir = path.join(dest, 'memory');
207
+ let made = false;
208
+ for (const filename of fs.readdirSync(memDir)) {
209
+ if (!filename.toLowerCase().endsWith('.md')) continue;
210
+ const src = path.join(memDir, filename);
211
+ let stat;
212
+ try {
213
+ stat = fs.statSync(src);
214
+ } catch {
215
+ continue;
216
+ }
217
+ if (!stat.isFile()) continue;
218
+
219
+ if (!made) {
220
+ fs.mkdirSync(outDir, { recursive: true });
221
+ made = true;
222
+ }
223
+ fs.copyFileSync(src, path.join(outDir, filename));
224
+ const isIndex = filename.toLowerCase() === 'memory.md';
225
+ const entry: BundleMemoryFileMeta = {
226
+ filename,
227
+ isIndex,
228
+ sha256: sha256File(src),
229
+ bytes: stat.size,
230
+ };
231
+ bytes += stat.size;
232
+ if (isIndex) inventory.hasIndex = true;
233
+ inventory.entries.push(entry);
234
+ }
235
+ return { inventory, bytes };
236
+ }