@alexgsdev/claude-code-transcript-export 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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/dist/cli/args.d.ts +23 -0
  4. package/dist/cli/args.d.ts.map +1 -0
  5. package/dist/cli/args.js +85 -0
  6. package/dist/cli/args.js.map +1 -0
  7. package/dist/cli/index.d.ts +10 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +230 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/init.d.ts +2 -0
  12. package/dist/cli/init.d.ts.map +1 -0
  13. package/dist/cli/init.js +35 -0
  14. package/dist/cli/init.js.map +1 -0
  15. package/dist/cli/probe.d.ts +18 -0
  16. package/dist/cli/probe.d.ts.map +1 -0
  17. package/dist/cli/probe.js +83 -0
  18. package/dist/cli/probe.js.map +1 -0
  19. package/dist/commits.d.ts +31 -0
  20. package/dist/commits.d.ts.map +1 -0
  21. package/dist/commits.js +59 -0
  22. package/dist/commits.js.map +1 -0
  23. package/dist/config.d.ts +65 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +189 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/content.d.ts +50 -0
  28. package/dist/content.d.ts.map +1 -0
  29. package/dist/content.js +141 -0
  30. package/dist/content.js.map +1 -0
  31. package/dist/discover.d.ts +55 -0
  32. package/dist/discover.d.ts.map +1 -0
  33. package/dist/discover.js +116 -0
  34. package/dist/discover.js.map +1 -0
  35. package/dist/extract.d.ts +47 -0
  36. package/dist/extract.d.ts.map +1 -0
  37. package/dist/extract.js +127 -0
  38. package/dist/extract.js.map +1 -0
  39. package/dist/index.d.ts +24 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +23 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/paths.d.ts +31 -0
  44. package/dist/paths.d.ts.map +1 -0
  45. package/dist/paths.js +47 -0
  46. package/dist/paths.js.map +1 -0
  47. package/dist/render/index.d.ts +21 -0
  48. package/dist/render/index.d.ts.map +1 -0
  49. package/dist/render/index.js +68 -0
  50. package/dist/render/index.js.map +1 -0
  51. package/dist/render/transcript.d.ts +31 -0
  52. package/dist/render/transcript.d.ts.map +1 -0
  53. package/dist/render/transcript.js +54 -0
  54. package/dist/render/transcript.js.map +1 -0
  55. package/dist/session.d.ts +30 -0
  56. package/dist/session.d.ts.map +1 -0
  57. package/dist/session.js +99 -0
  58. package/dist/session.js.map +1 -0
  59. package/dist/titles.d.ts +26 -0
  60. package/dist/titles.d.ts.map +1 -0
  61. package/dist/titles.js +51 -0
  62. package/dist/titles.js.map +1 -0
  63. package/dist/turns.d.ts +62 -0
  64. package/dist/turns.d.ts.map +1 -0
  65. package/dist/turns.js +108 -0
  66. package/dist/turns.js.map +1 -0
  67. package/dist/types.d.ts +95 -0
  68. package/dist/types.d.ts.map +1 -0
  69. package/dist/types.js +14 -0
  70. package/dist/types.js.map +1 -0
  71. package/dist/write.d.ts +61 -0
  72. package/dist/write.d.ts.map +1 -0
  73. package/dist/write.js +126 -0
  74. package/dist/write.js.map +1 -0
  75. package/package.json +64 -0
@@ -0,0 +1,61 @@
1
+ import type { SessionImage } from './types.js';
2
+ /**
3
+ * Writes `render(date)` to `path` unless the file there already says the same
4
+ * thing. Returns whether it wrote.
5
+ *
6
+ * The date is preserved when the content around it is identical, so
7
+ * `extracted:` comes to mean "the day this transcript last changed" rather than
8
+ * "the day the extractor last ran". Without that, a rerun touches every
9
+ * transcript and the resulting commit is a wall of one-line diffs with nothing
10
+ * in it — the shape of a diff nobody reads, in the one directory where a real
11
+ * change to an old transcript would matter.
12
+ *
13
+ * Takes a render *function* rather than a string so the prior date can be fed
14
+ * back in. The renderer stays pure; the impurity lives here, where the I/O
15
+ * already is.
16
+ */
17
+ export declare function writeIfChanged(path: string, render: (extractedOn: string) => string, today: string): boolean;
18
+ /** Writes a file only when its bytes differ. For output with no date header. */
19
+ export declare function writePlainIfChanged(path: string, contents: string): boolean;
20
+ /**
21
+ * Writes images that are not already on disk.
22
+ *
23
+ * Filenames are content hashes, so an existing file with the right name always
24
+ * has the right bytes and never needs rewriting.
25
+ */
26
+ export declare function writeImages(dir: string, images: SessionImage[]): string[];
27
+ /**
28
+ * Deletes transcripts left behind when a session was renamed.
29
+ *
30
+ * A filename is built from the session's title, and a title is whatever the
31
+ * last `custom-title` or `ai-title` record says. Sessions get renamed as they
32
+ * go, so a session extracted twice across a rename lands under two names.
33
+ * Writing never removes the first, and the stale copy stays: a truncated
34
+ * transcript that reads exactly like evidence, absent from the index, citable
35
+ * by anyone who finds it.
36
+ *
37
+ * **Scoped to renames, never a directory sync.** A file is removed only when
38
+ * this run wrote a different name for the same session id. The logs are
39
+ * ephemeral and the output is the durable record, so a transcript whose JSONL
40
+ * has since been pruned must survive; a sync would delete precisely the
41
+ * transcripts that can no longer be regenerated.
42
+ */
43
+ export declare function pruneRenamed(outDir: string, written: string[]): string[];
44
+ /**
45
+ * Deletes transcripts for sessions the config now excludes.
46
+ *
47
+ * Without this, adding a UUID to `exclude` does nothing to what is already on
48
+ * disk, which is the opposite of what the user asked for.
49
+ */
50
+ export declare function pruneExcluded(outDir: string, excludedIds: string[]): string[];
51
+ /**
52
+ * Images in `imageDir` that no transcript references.
53
+ *
54
+ * **Reported, never deleted.** Content-hash naming makes "unreferenced" an
55
+ * exact fact rather than a guess, so the report is trustworthy — but an image
56
+ * is the one output that cannot be regenerated once its log is pruned, which
57
+ * puts it on the wrong side of the rule that this tool never deletes what it
58
+ * cannot rebuild.
59
+ */
60
+ export declare function findOrphanImages(imageDir: string, referenced: Set<string>): string[];
61
+ //# sourceMappingURL=write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../src/write.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,EACvC,KAAK,EAAE,MAAM,GACZ,OAAO,CAST;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI3E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,CAYzE;AAKD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAkBxE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAa7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAKpF"}
package/dist/write.js ADDED
@@ -0,0 +1,126 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { readExtractedDate } from './render/transcript.js';
4
+ /**
5
+ * Writes `render(date)` to `path` unless the file there already says the same
6
+ * thing. Returns whether it wrote.
7
+ *
8
+ * The date is preserved when the content around it is identical, so
9
+ * `extracted:` comes to mean "the day this transcript last changed" rather than
10
+ * "the day the extractor last ran". Without that, a rerun touches every
11
+ * transcript and the resulting commit is a wall of one-line diffs with nothing
12
+ * in it — the shape of a diff nobody reads, in the one directory where a real
13
+ * change to an old transcript would matter.
14
+ *
15
+ * Takes a render *function* rather than a string so the prior date can be fed
16
+ * back in. The renderer stays pure; the impurity lives here, where the I/O
17
+ * already is.
18
+ */
19
+ export function writeIfChanged(path, render, today) {
20
+ if (existsSync(path)) {
21
+ const existing = readFileSync(path, 'utf8');
22
+ const prior = readExtractedDate(existing);
23
+ if (prior !== null && render(prior) === existing)
24
+ return false;
25
+ }
26
+ writeFileSync(path, render(today));
27
+ return true;
28
+ }
29
+ /** Writes a file only when its bytes differ. For output with no date header. */
30
+ export function writePlainIfChanged(path, contents) {
31
+ if (existsSync(path) && readFileSync(path, 'utf8') === contents)
32
+ return false;
33
+ writeFileSync(path, contents);
34
+ return true;
35
+ }
36
+ /**
37
+ * Writes images that are not already on disk.
38
+ *
39
+ * Filenames are content hashes, so an existing file with the right name always
40
+ * has the right bytes and never needs rewriting.
41
+ */
42
+ export function writeImages(dir, images) {
43
+ if (images.length === 0)
44
+ return [];
45
+ mkdirSync(dir, { recursive: true });
46
+ const written = [];
47
+ for (const image of images) {
48
+ const path = join(dir, image.filename);
49
+ if (existsSync(path))
50
+ continue;
51
+ writeFileSync(path, image.data);
52
+ written.push(image.filename);
53
+ }
54
+ return written;
55
+ }
56
+ /** `<date>--<slug>--<8 hex>.md`, the shape `sessionFilename` writes. */
57
+ const TRANSCRIPT_NAME = /^\d{4}-\d{2}-\d{2}--.+--([0-9a-f]{8})\.md$/;
58
+ /**
59
+ * Deletes transcripts left behind when a session was renamed.
60
+ *
61
+ * A filename is built from the session's title, and a title is whatever the
62
+ * last `custom-title` or `ai-title` record says. Sessions get renamed as they
63
+ * go, so a session extracted twice across a rename lands under two names.
64
+ * Writing never removes the first, and the stale copy stays: a truncated
65
+ * transcript that reads exactly like evidence, absent from the index, citable
66
+ * by anyone who finds it.
67
+ *
68
+ * **Scoped to renames, never a directory sync.** A file is removed only when
69
+ * this run wrote a different name for the same session id. The logs are
70
+ * ephemeral and the output is the durable record, so a transcript whose JSONL
71
+ * has since been pruned must survive; a sync would delete precisely the
72
+ * transcripts that can no longer be regenerated.
73
+ */
74
+ export function pruneRenamed(outDir, written) {
75
+ if (!existsSync(outDir))
76
+ return [];
77
+ const keep = new Set(written);
78
+ const writtenIds = new Set(written.map((name) => TRANSCRIPT_NAME.exec(name)?.[1]).filter(Boolean));
79
+ const removed = readdirSync(outDir)
80
+ .filter((name) => !keep.has(name))
81
+ .filter((name) => {
82
+ const id = TRANSCRIPT_NAME.exec(name)?.[1];
83
+ return id !== undefined && writtenIds.has(id);
84
+ })
85
+ .sort();
86
+ for (const name of removed)
87
+ rmSync(join(outDir, name));
88
+ return removed;
89
+ }
90
+ /**
91
+ * Deletes transcripts for sessions the config now excludes.
92
+ *
93
+ * Without this, adding a UUID to `exclude` does nothing to what is already on
94
+ * disk, which is the opposite of what the user asked for.
95
+ */
96
+ export function pruneExcluded(outDir, excludedIds) {
97
+ if (!existsSync(outDir) || excludedIds.length === 0)
98
+ return [];
99
+ const prefixes = new Set(excludedIds.map((id) => id.slice(0, 8)));
100
+ const removed = readdirSync(outDir)
101
+ .filter((name) => {
102
+ const id = TRANSCRIPT_NAME.exec(name)?.[1];
103
+ return id !== undefined && prefixes.has(id);
104
+ })
105
+ .sort();
106
+ for (const name of removed)
107
+ rmSync(join(outDir, name));
108
+ return removed;
109
+ }
110
+ /**
111
+ * Images in `imageDir` that no transcript references.
112
+ *
113
+ * **Reported, never deleted.** Content-hash naming makes "unreferenced" an
114
+ * exact fact rather than a guess, so the report is trustworthy — but an image
115
+ * is the one output that cannot be regenerated once its log is pruned, which
116
+ * puts it on the wrong side of the rule that this tool never deletes what it
117
+ * cannot rebuild.
118
+ */
119
+ export function findOrphanImages(imageDir, referenced) {
120
+ if (!existsSync(imageDir))
121
+ return [];
122
+ return readdirSync(imageDir)
123
+ .filter((name) => !referenced.has(name))
124
+ .sort();
125
+ }
126
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../src/write.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@alexgsdev/claude-code-transcript-export",
3
+ "version": "0.1.0",
4
+ "description": "Extract Claude Code session logs into readable, committable markdown transcripts.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "transcript",
9
+ "export",
10
+ "jsonl"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/alexgs/claude-code-transcript-export.git"
16
+ },
17
+ "homepage": "https://github.com/alexgs/claude-code-transcript-export#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/alexgs/claude-code-transcript-export/issues"
20
+ },
21
+ "type": "module",
22
+ "engines": {
23
+ "node": ">=22"
24
+ },
25
+ "bin": {
26
+ "cctx": "dist/cli/index.js"
27
+ },
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.build.json",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "format": "prettier --write .",
48
+ "format:check": "prettier --check .",
49
+ "prepack": "npm run build"
50
+ },
51
+ "dependencies": {
52
+ "yaml": "^2.9.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.10.0",
56
+ "prettier": "^3.4.0",
57
+ "typescript": "^5.7.0",
58
+ "vitest": "^2.1.0"
59
+ },
60
+ "volta": {
61
+ "node": "22.23.2",
62
+ "npm": "11.5.2"
63
+ }
64
+ }