@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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/cli/args.d.ts +23 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +85 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +230 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +35 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/probe.d.ts +18 -0
- package/dist/cli/probe.d.ts.map +1 -0
- package/dist/cli/probe.js +83 -0
- package/dist/cli/probe.js.map +1 -0
- package/dist/commits.d.ts +31 -0
- package/dist/commits.d.ts.map +1 -0
- package/dist/commits.js +59 -0
- package/dist/commits.js.map +1 -0
- package/dist/config.d.ts +65 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +189 -0
- package/dist/config.js.map +1 -0
- package/dist/content.d.ts +50 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +141 -0
- package/dist/content.js.map +1 -0
- package/dist/discover.d.ts +55 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +116 -0
- package/dist/discover.js.map +1 -0
- package/dist/extract.d.ts +47 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +127 -0
- package/dist/extract.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +47 -0
- package/dist/paths.js.map +1 -0
- package/dist/render/index.d.ts +21 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +68 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/transcript.d.ts +31 -0
- package/dist/render/transcript.d.ts.map +1 -0
- package/dist/render/transcript.js +54 -0
- package/dist/render/transcript.js.map +1 -0
- package/dist/session.d.ts +30 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +99 -0
- package/dist/session.js.map +1 -0
- package/dist/titles.d.ts +26 -0
- package/dist/titles.d.ts.map +1 -0
- package/dist/titles.js +51 -0
- package/dist/titles.js.map +1 -0
- package/dist/turns.d.ts +62 -0
- package/dist/turns.d.ts.map +1 -0
- package/dist/turns.js +108 -0
- package/dist/turns.js.map +1 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/dist/write.d.ts +61 -0
- package/dist/write.d.ts.map +1 -0
- package/dist/write.js +126 -0
- package/dist/write.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readLogFile } from '../discover.js';
|
|
2
|
+
import { isHumanTurn } from '../turns.js';
|
|
3
|
+
/** Record types this package knows how to do something with. */
|
|
4
|
+
const HANDLED_RECORDS = new Set([
|
|
5
|
+
'user',
|
|
6
|
+
'assistant',
|
|
7
|
+
'ai-title',
|
|
8
|
+
'custom-title',
|
|
9
|
+
'agent-name',
|
|
10
|
+
'continued-in',
|
|
11
|
+
'relocated',
|
|
12
|
+
]);
|
|
13
|
+
const HANDLED_BLOCKS = new Set([
|
|
14
|
+
'text',
|
|
15
|
+
'thinking',
|
|
16
|
+
'tool_use',
|
|
17
|
+
'tool_result',
|
|
18
|
+
'image',
|
|
19
|
+
]);
|
|
20
|
+
export function probeRecords(files) {
|
|
21
|
+
const recordTypes = new Map();
|
|
22
|
+
const blockTypes = new Map();
|
|
23
|
+
let userRecords = 0;
|
|
24
|
+
let humanTurns = 0;
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
for (const record of file.records) {
|
|
27
|
+
const type = record.type ?? '(untyped)';
|
|
28
|
+
recordTypes.set(type, (recordTypes.get(type) ?? 0) + 1);
|
|
29
|
+
if (type === 'user') {
|
|
30
|
+
userRecords += 1;
|
|
31
|
+
if (isHumanTurn(record))
|
|
32
|
+
humanTurns += 1;
|
|
33
|
+
}
|
|
34
|
+
const content = record.message?.content;
|
|
35
|
+
if (Array.isArray(content)) {
|
|
36
|
+
for (const block of content) {
|
|
37
|
+
const blockType = block?.type ?? '(untyped)';
|
|
38
|
+
blockTypes.set(blockType, (blockTypes.get(blockType) ?? 0) + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const sorted = (map) => Object.fromEntries([...map.entries()].sort((a, b) => b[1] - a[1]));
|
|
44
|
+
return {
|
|
45
|
+
files: files.length,
|
|
46
|
+
recordTypes: sorted(recordTypes),
|
|
47
|
+
blockTypes: sorted(blockTypes),
|
|
48
|
+
userRecords,
|
|
49
|
+
humanTurns,
|
|
50
|
+
unhandledRecords: [...recordTypes.keys()]
|
|
51
|
+
.filter((t) => !HANDLED_RECORDS.has(t))
|
|
52
|
+
.sort(),
|
|
53
|
+
unhandledBlocks: [...blockTypes.keys()]
|
|
54
|
+
.filter((t) => !HANDLED_BLOCKS.has(t))
|
|
55
|
+
.sort(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function probe(paths) {
|
|
59
|
+
return probeRecords(paths.map((path) => ({ path, records: readLogFile(path) })));
|
|
60
|
+
}
|
|
61
|
+
export function formatProbe(report) {
|
|
62
|
+
const lines = [`session logs: ${report.files}`];
|
|
63
|
+
if (report.files === 0)
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
const table = (counts) => Object.entries(counts).map(([key, value]) => ` ${key}: ${value}`);
|
|
66
|
+
lines.push('', 'record types:', ...table(report.recordTypes));
|
|
67
|
+
lines.push('', 'content block types:', ...table(report.blockTypes));
|
|
68
|
+
const ratio = report.userRecords === 0
|
|
69
|
+
? '0'
|
|
70
|
+
: ((report.humanTurns / report.userRecords) * 100).toFixed(1);
|
|
71
|
+
lines.push('', `user records: ${report.userRecords}, of which human turns: ${report.humanTurns} (${ratio}%)`, '(the difference is tool results and injected context, which are not turns)');
|
|
72
|
+
// The reason this command exists: the schema is observed, not documented.
|
|
73
|
+
if (report.unhandledRecords.length > 0) {
|
|
74
|
+
lines.push('', `record types this version does not handle:`);
|
|
75
|
+
lines.push(...report.unhandledRecords.map((t) => ` ${t}`));
|
|
76
|
+
}
|
|
77
|
+
if (report.unhandledBlocks.length > 0) {
|
|
78
|
+
lines.push('', `content blocks this version does not handle:`);
|
|
79
|
+
lines.push(...report.unhandledBlocks.map((t) => ` ${t}`));
|
|
80
|
+
}
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/cli/probe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Session } from './types.js';
|
|
2
|
+
/** One commit, as `readCommits` parses it out of `git log`. */
|
|
3
|
+
export interface Commit {
|
|
4
|
+
hash: string;
|
|
5
|
+
/** Author date. Stable across a rebase; committer date is not. */
|
|
6
|
+
at: string;
|
|
7
|
+
subject: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The repository's commits, for pairing each session with work done during it.
|
|
11
|
+
*
|
|
12
|
+
* Failure is silent and returns nothing: extraction must not depend on git
|
|
13
|
+
* being present, on the tree being a repository, or on any commit existing.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readCommits(cwd: string, runGit?: (cwd: string) => string): Commit[];
|
|
16
|
+
/**
|
|
17
|
+
* Commits authored between a session's first and last record, oldest first.
|
|
18
|
+
*
|
|
19
|
+
* Read as "committed while this session was open", which is not the same claim
|
|
20
|
+
* as "committed by this session" — a commit made by hand mid-session lands here
|
|
21
|
+
* too, and two concurrent sessions both list what falls in the overlap.
|
|
22
|
+
*
|
|
23
|
+
* A window rather than the `Claude-Session` commit trailer, which looks like
|
|
24
|
+
* the exact key for this: that id appears nowhere structural in a log. It shows
|
|
25
|
+
* up only inside conversation content — in the text of a commit command, in
|
|
26
|
+
* `git log` output pasted back as a tool result, in prose discussing it — so a
|
|
27
|
+
* session quoting another session's id offers two candidates and no way to
|
|
28
|
+
* choose. A window is less precise and does not lie about its precision.
|
|
29
|
+
*/
|
|
30
|
+
export declare function commitsInWindow(commits: Commit[], session: Session): Commit[];
|
|
31
|
+
//# sourceMappingURL=commits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commits.d.ts","sourceRoot":"","sources":["../src/commits.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,+DAA+D;AAC/D,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAKrB,GACH,MAAM,EAAE,CAgBV;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAc7E"}
|
package/dist/commits.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
const FORMAT = '%h%x1f%aI%x1f%s';
|
|
3
|
+
/**
|
|
4
|
+
* The repository's commits, for pairing each session with work done during it.
|
|
5
|
+
*
|
|
6
|
+
* Failure is silent and returns nothing: extraction must not depend on git
|
|
7
|
+
* being present, on the tree being a repository, or on any commit existing.
|
|
8
|
+
*/
|
|
9
|
+
export function readCommits(cwd, runGit = (dir) => execFileSync('git', ['log', `--format=${FORMAT}`], {
|
|
10
|
+
cwd: dir,
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
13
|
+
})) {
|
|
14
|
+
let out;
|
|
15
|
+
try {
|
|
16
|
+
out = runGit(cwd);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
return out
|
|
22
|
+
.split('\n')
|
|
23
|
+
.filter((line) => line.trim() !== '')
|
|
24
|
+
.map((line) => {
|
|
25
|
+
const [hash, at, ...rest] = line.split('\x1f');
|
|
26
|
+
return { hash: hash ?? '', at: at ?? '', subject: rest.join('\x1f') };
|
|
27
|
+
})
|
|
28
|
+
.filter((commit) => commit.hash !== '' && commit.at !== '');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Commits authored between a session's first and last record, oldest first.
|
|
32
|
+
*
|
|
33
|
+
* Read as "committed while this session was open", which is not the same claim
|
|
34
|
+
* as "committed by this session" — a commit made by hand mid-session lands here
|
|
35
|
+
* too, and two concurrent sessions both list what falls in the overlap.
|
|
36
|
+
*
|
|
37
|
+
* A window rather than the `Claude-Session` commit trailer, which looks like
|
|
38
|
+
* the exact key for this: that id appears nowhere structural in a log. It shows
|
|
39
|
+
* up only inside conversation content — in the text of a commit command, in
|
|
40
|
+
* `git log` output pasted back as a tool result, in prose discussing it — so a
|
|
41
|
+
* session quoting another session's id offers two candidates and no way to
|
|
42
|
+
* choose. A window is less precise and does not lie about its precision.
|
|
43
|
+
*/
|
|
44
|
+
export function commitsInWindow(commits, session) {
|
|
45
|
+
const { startedAt, endedAt } = session;
|
|
46
|
+
if (startedAt === undefined || endedAt === undefined)
|
|
47
|
+
return [];
|
|
48
|
+
const start = Date.parse(startedAt);
|
|
49
|
+
const end = Date.parse(endedAt);
|
|
50
|
+
if (Number.isNaN(start) || Number.isNaN(end))
|
|
51
|
+
return [];
|
|
52
|
+
return commits
|
|
53
|
+
.filter((commit) => {
|
|
54
|
+
const at = Date.parse(commit.at);
|
|
55
|
+
return !Number.isNaN(at) && at >= start && at <= end;
|
|
56
|
+
})
|
|
57
|
+
.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=commits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commits.js","sourceRoot":"","sources":["../src/commits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAWlD,MAAM,MAAM,GAAG,iBAAiB,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,GAAW,EACX,SAAkC,CAAC,GAAG,EAAE,EAAE,CACxC,YAAY,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,MAAM,EAAE,CAAC,EAAE;IACjD,GAAG,EAAE,GAAG;IACR,QAAQ,EAAE,MAAM;IAChB,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;CAC5B,CAAC;IAEJ,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,GAAG;SACP,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;SACpC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC/C,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IACxE,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,EAAE,IAAI,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,OAAiB,EAAE,OAAgB;IACjE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IACvC,IAAI,SAAS,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAEhE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAExD,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE;QACjB,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI,GAAG,CAAC;IACvD,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACzD,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ContentPolicy, ImagePolicy, ThinkingPolicy, ToolPolicy } from './types.js';
|
|
2
|
+
/** A bad flag, a bad config, a missing directory — anything the user can fix. */
|
|
3
|
+
export declare class UsageError extends Error {
|
|
4
|
+
}
|
|
5
|
+
/** Config filenames, in precedence order. First match wins. */
|
|
6
|
+
export declare const CONFIG_NAMES: readonly [".claude-export.yaml", ".claude-export.json"];
|
|
7
|
+
export interface IndexConfig {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
/** Project-specific prose, inserted verbatim. */
|
|
10
|
+
preamble: string | null;
|
|
11
|
+
listExcluded: 'count' | 'ids' | 'none';
|
|
12
|
+
}
|
|
13
|
+
export interface Config {
|
|
14
|
+
out: string;
|
|
15
|
+
include: string[];
|
|
16
|
+
exclude: string[];
|
|
17
|
+
tools: ToolPolicy;
|
|
18
|
+
thinking: ThinkingPolicy;
|
|
19
|
+
images: ImagePolicy;
|
|
20
|
+
imageDir: string;
|
|
21
|
+
subagents: 'ignore' | 'capture';
|
|
22
|
+
skipEmpty: boolean;
|
|
23
|
+
skipActive: boolean;
|
|
24
|
+
activeGraceMinutes: number;
|
|
25
|
+
commits: boolean;
|
|
26
|
+
index: IndexConfig;
|
|
27
|
+
}
|
|
28
|
+
export declare const DEFAULT_CONFIG: Config;
|
|
29
|
+
export interface ResolvedConfig {
|
|
30
|
+
/** The directory holding the config file. This is the project root. */
|
|
31
|
+
root: string;
|
|
32
|
+
/** Absolute path of the config file, or null when defaults were used. */
|
|
33
|
+
configPath: string | null;
|
|
34
|
+
config: Config;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The nearest config file at or above `from`.
|
|
38
|
+
*
|
|
39
|
+
* The directory containing it is the project root, which is what makes `cctx`
|
|
40
|
+
* work from any subdirectory. The walk stops at the filesystem root; a config
|
|
41
|
+
* in an unexpected parent silently claiming a nested repo is the one failure
|
|
42
|
+
* mode of walk-up discovery, which is why every run prints the root it
|
|
43
|
+
* resolved.
|
|
44
|
+
*/
|
|
45
|
+
export declare function findConfig(from: string): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Merges a parsed config object over the defaults.
|
|
48
|
+
*
|
|
49
|
+
* Unknown keys are an error naming the key, not a silent ignore: a typo in
|
|
50
|
+
* `exclude` that quietly does nothing is how a session you meant to withhold
|
|
51
|
+
* ends up in a committed transcript.
|
|
52
|
+
*/
|
|
53
|
+
export declare function mergeConfig(raw: unknown): Config;
|
|
54
|
+
/** Parses a config file by extension. */
|
|
55
|
+
export declare function parseConfigFile(path: string, contents: string): Config;
|
|
56
|
+
/**
|
|
57
|
+
* Discovers and loads the config, or reports that there is none.
|
|
58
|
+
*
|
|
59
|
+
* `projectOverride` skips discovery entirely, for `--project`: a root without a
|
|
60
|
+
* config file, using defaults.
|
|
61
|
+
*/
|
|
62
|
+
export declare function loadConfig(from: string, projectOverride?: string): ResolvedConfig | null;
|
|
63
|
+
/** The content policy a config implies. */
|
|
64
|
+
export declare function contentPolicy(config: Config): ContentPolicy;
|
|
65
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,aAAa,EACb,WAAW,EACX,cAAc,EACd,UAAU,EACX,MAAM,YAAY,CAAC;AAEpB,iFAAiF;AACjF,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,+DAA+D;AAC/D,eAAO,MAAM,YAAY,yDAA0D,CAAC;AAEpF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,cAAc,CAAC;IACzB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,QAAQ,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,eAAO,MAAM,cAAc,EAAE,MAgB5B,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAYtD;AAyBD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CA4EhD;AAED,yCAAyC;AACzC,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYtE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,MAAM,GACvB,cAAc,GAAG,IAAI,CAiBvB;AAED,2CAA2C;AAC3C,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAE3D"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
/** A bad flag, a bad config, a missing directory — anything the user can fix. */
|
|
5
|
+
export class UsageError extends Error {
|
|
6
|
+
}
|
|
7
|
+
/** Config filenames, in precedence order. First match wins. */
|
|
8
|
+
export const CONFIG_NAMES = ['.claude-export.yaml', '.claude-export.json'];
|
|
9
|
+
export const DEFAULT_CONFIG = {
|
|
10
|
+
out: 'docs/sessions',
|
|
11
|
+
include: [],
|
|
12
|
+
exclude: [],
|
|
13
|
+
tools: 'strip',
|
|
14
|
+
thinking: 'drop',
|
|
15
|
+
images: 'extract',
|
|
16
|
+
imageDir: 'images',
|
|
17
|
+
subagents: 'ignore',
|
|
18
|
+
skipEmpty: true,
|
|
19
|
+
// Capturing an in-progress session is the default: a partial transcript
|
|
20
|
+
// beats no transcript, and it self-heals on the next run. See spec §9.3.
|
|
21
|
+
skipActive: false,
|
|
22
|
+
activeGraceMinutes: 30,
|
|
23
|
+
commits: false,
|
|
24
|
+
index: { enabled: true, preamble: null, listExcluded: 'count' },
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The nearest config file at or above `from`.
|
|
28
|
+
*
|
|
29
|
+
* The directory containing it is the project root, which is what makes `cctx`
|
|
30
|
+
* work from any subdirectory. The walk stops at the filesystem root; a config
|
|
31
|
+
* in an unexpected parent silently claiming a nested repo is the one failure
|
|
32
|
+
* mode of walk-up discovery, which is why every run prints the root it
|
|
33
|
+
* resolved.
|
|
34
|
+
*/
|
|
35
|
+
export function findConfig(from) {
|
|
36
|
+
let dir = resolve(from);
|
|
37
|
+
for (;;) {
|
|
38
|
+
for (const name of CONFIG_NAMES) {
|
|
39
|
+
const candidate = join(dir, name);
|
|
40
|
+
if (existsSync(candidate))
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
const parent = dirname(dir);
|
|
44
|
+
if (parent === dir)
|
|
45
|
+
return null;
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const ENUMS = {
|
|
50
|
+
tools: ['strip', 'keep'],
|
|
51
|
+
thinking: ['drop', 'keep'],
|
|
52
|
+
images: ['extract', 'marker'],
|
|
53
|
+
subagents: ['ignore', 'capture'],
|
|
54
|
+
};
|
|
55
|
+
function assertEnum(key, value) {
|
|
56
|
+
const allowed = ENUMS[key];
|
|
57
|
+
if (typeof value !== 'string' || !allowed.includes(value)) {
|
|
58
|
+
throw new UsageError(`\`${key}\` must be one of ${allowed.join(' | ')}, got ${JSON.stringify(value)}.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function assertStringArray(key, value) {
|
|
62
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== 'string')) {
|
|
63
|
+
throw new UsageError(`\`${key}\` must be a list of session UUIDs.`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Merges a parsed config object over the defaults.
|
|
69
|
+
*
|
|
70
|
+
* Unknown keys are an error naming the key, not a silent ignore: a typo in
|
|
71
|
+
* `exclude` that quietly does nothing is how a session you meant to withhold
|
|
72
|
+
* ends up in a committed transcript.
|
|
73
|
+
*/
|
|
74
|
+
export function mergeConfig(raw) {
|
|
75
|
+
if (raw === null || raw === undefined)
|
|
76
|
+
return { ...DEFAULT_CONFIG };
|
|
77
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
78
|
+
throw new UsageError('config must be a mapping of options.');
|
|
79
|
+
}
|
|
80
|
+
const source = raw;
|
|
81
|
+
const config = { ...DEFAULT_CONFIG, index: { ...DEFAULT_CONFIG.index } };
|
|
82
|
+
for (const [key, value] of Object.entries(source)) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
switch (key) {
|
|
86
|
+
case 'out':
|
|
87
|
+
case 'imageDir':
|
|
88
|
+
if (typeof value !== 'string' || value === '') {
|
|
89
|
+
throw new UsageError(`\`${key}\` must be a non-empty path.`);
|
|
90
|
+
}
|
|
91
|
+
config[key] = value;
|
|
92
|
+
break;
|
|
93
|
+
case 'include':
|
|
94
|
+
case 'exclude':
|
|
95
|
+
config[key] = assertStringArray(key, value);
|
|
96
|
+
break;
|
|
97
|
+
case 'tools':
|
|
98
|
+
case 'thinking':
|
|
99
|
+
case 'images':
|
|
100
|
+
case 'subagents':
|
|
101
|
+
assertEnum(key, value);
|
|
102
|
+
config[key] = value;
|
|
103
|
+
break;
|
|
104
|
+
case 'skipEmpty':
|
|
105
|
+
case 'skipActive':
|
|
106
|
+
case 'commits':
|
|
107
|
+
if (typeof value !== 'boolean')
|
|
108
|
+
throw new UsageError(`\`${key}\` must be true or false.`);
|
|
109
|
+
config[key] = value;
|
|
110
|
+
break;
|
|
111
|
+
case 'activeGraceMinutes':
|
|
112
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
113
|
+
throw new UsageError('`activeGraceMinutes` must be a non-negative number.');
|
|
114
|
+
}
|
|
115
|
+
config.activeGraceMinutes = value;
|
|
116
|
+
break;
|
|
117
|
+
case 'index': {
|
|
118
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
119
|
+
throw new UsageError('`index` must be a mapping.');
|
|
120
|
+
}
|
|
121
|
+
for (const [k, v] of Object.entries(value)) {
|
|
122
|
+
if (k === 'enabled') {
|
|
123
|
+
if (typeof v !== 'boolean')
|
|
124
|
+
throw new UsageError('`index.enabled` must be true or false.');
|
|
125
|
+
config.index.enabled = v;
|
|
126
|
+
}
|
|
127
|
+
else if (k === 'preamble') {
|
|
128
|
+
if (v !== null && typeof v !== 'string') {
|
|
129
|
+
throw new UsageError('`index.preamble` must be a string or null.');
|
|
130
|
+
}
|
|
131
|
+
config.index.preamble = v;
|
|
132
|
+
}
|
|
133
|
+
else if (k === 'listExcluded') {
|
|
134
|
+
if (v !== 'count' && v !== 'ids' && v !== 'none') {
|
|
135
|
+
throw new UsageError('`index.listExcluded` must be one of count | ids | none.');
|
|
136
|
+
}
|
|
137
|
+
config.index.listExcluded = v;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
throw new UsageError(`unknown config key \`index.${k}\`.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
throw new UsageError(`unknown config key \`${key}\`.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return config;
|
|
150
|
+
}
|
|
151
|
+
/** Parses a config file by extension. */
|
|
152
|
+
export function parseConfigFile(path, contents) {
|
|
153
|
+
let raw;
|
|
154
|
+
try {
|
|
155
|
+
raw = path.endsWith('.json') ? JSON.parse(contents) : parseYaml(contents);
|
|
156
|
+
}
|
|
157
|
+
catch (cause) {
|
|
158
|
+
throw new UsageError(`${path} is not valid ${path.endsWith('.json') ? 'JSON' : 'YAML'}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
159
|
+
}
|
|
160
|
+
return mergeConfig(raw);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Discovers and loads the config, or reports that there is none.
|
|
164
|
+
*
|
|
165
|
+
* `projectOverride` skips discovery entirely, for `--project`: a root without a
|
|
166
|
+
* config file, using defaults.
|
|
167
|
+
*/
|
|
168
|
+
export function loadConfig(from, projectOverride) {
|
|
169
|
+
if (projectOverride !== undefined) {
|
|
170
|
+
return {
|
|
171
|
+
root: resolve(projectOverride),
|
|
172
|
+
configPath: null,
|
|
173
|
+
config: { ...DEFAULT_CONFIG, index: { ...DEFAULT_CONFIG.index } },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const configPath = findConfig(from);
|
|
177
|
+
if (configPath === null)
|
|
178
|
+
return null;
|
|
179
|
+
return {
|
|
180
|
+
root: dirname(configPath),
|
|
181
|
+
configPath,
|
|
182
|
+
config: parseConfigFile(configPath, readFileSync(configPath, 'utf8')),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** The content policy a config implies. */
|
|
186
|
+
export function contentPolicy(config) {
|
|
187
|
+
return { tools: config.tools, thinking: config.thinking, images: config.images };
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ContentPolicy, RawBlock, RawRecord, SessionImage } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Text with harness scaffolding removed, contents and all.
|
|
4
|
+
*
|
|
5
|
+
* The command *name* goes with the rest, deliberately. Leaving it behind turns
|
|
6
|
+
* `/clear` into a one-word human turn — something the author never said — and
|
|
7
|
+
* shifts every turn number after it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function stripHarnessTags(text: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Wraps content in a fence long enough to contain it. Tool payloads carry
|
|
12
|
+
* markdown, which carries its own fences; a fixed ``` would let one break out.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fence(content: string, info: string): string;
|
|
15
|
+
/** Title lowercased, non-alphanumerics collapsed to hyphens, truncated to 60. */
|
|
16
|
+
export declare function slugify(title: string): string;
|
|
17
|
+
export interface RenderContext {
|
|
18
|
+
policy?: ContentPolicy;
|
|
19
|
+
/** First 8 characters of the session id, for naming extracted images. */
|
|
20
|
+
sessionId8?: string;
|
|
21
|
+
/** Images found while rendering are pushed here for the writer to persist. */
|
|
22
|
+
collected?: SessionImage[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One image block as either a link to an extracted file or a marker.
|
|
26
|
+
*
|
|
27
|
+
* Naming by content hash is what keeps reruns stable: identical bytes always
|
|
28
|
+
* produce an identical filename, so the same screenshot pasted twice is stored
|
|
29
|
+
* once and a rerun rewrites nothing. An unrecognized media type falls back to
|
|
30
|
+
* a marker rather than guessing an extension.
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderImage(block: RawBlock, context: RenderContext): string;
|
|
33
|
+
/**
|
|
34
|
+
* One content block as markdown, or `null` when policy drops it.
|
|
35
|
+
*
|
|
36
|
+
* - `text` — verbatim.
|
|
37
|
+
* - `thinking` — dropped by default. Exploratory reasoning misleads a later
|
|
38
|
+
* reader: a hypothesis the assistant talked itself out of reads like a
|
|
39
|
+
* proposal if you find it out of context.
|
|
40
|
+
* - `tool_use` / `tool_result` — dropped by default. Anything authored in a
|
|
41
|
+
* Claude Code session lands in the working tree and is captured by the
|
|
42
|
+
* commit, so the payload is redundant with git while making up most of the
|
|
43
|
+
* raw bytes.
|
|
44
|
+
* - `image` — extracted to a file, or a marker.
|
|
45
|
+
* - anything else — a visible marker, never a silent drop.
|
|
46
|
+
*/
|
|
47
|
+
export declare function renderBlock(block: RawBlock, context?: RenderContext): string | null;
|
|
48
|
+
/** The text of one record under the given policy. */
|
|
49
|
+
export declare function recordText(record: RawRecord, context?: RenderContext): string;
|
|
50
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAoBnF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAK3D;AAED,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAgBD,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,CAuB3E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,QAAQ,EACf,OAAO,GAAE,aAAkB,GAC1B,MAAM,GAAG,IAAI,CAiCf;AAED,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAUjF"}
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { DEFAULT_CONTENT_POLICY } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Harness scaffolding wrapped around a human turn — a slash command's
|
|
5
|
+
* expansion, the caveat block preceding it, an injected reminder. Not authored
|
|
6
|
+
* prose; a record that is only this is not a turn at all.
|
|
7
|
+
*/
|
|
8
|
+
const HARNESS_TAGS = [
|
|
9
|
+
'local-command-caveat',
|
|
10
|
+
'local-command-stdout',
|
|
11
|
+
'command-name',
|
|
12
|
+
'command-message',
|
|
13
|
+
'command-args',
|
|
14
|
+
'system-reminder',
|
|
15
|
+
];
|
|
16
|
+
const PAIRED = new RegExp(`<(${HARNESS_TAGS.join('|')})>[\\s\\S]*?</\\1>`, 'g');
|
|
17
|
+
const UNPAIRED = new RegExp(`</?(${HARNESS_TAGS.join('|')})>`, 'g');
|
|
18
|
+
/**
|
|
19
|
+
* Text with harness scaffolding removed, contents and all.
|
|
20
|
+
*
|
|
21
|
+
* The command *name* goes with the rest, deliberately. Leaving it behind turns
|
|
22
|
+
* `/clear` into a one-word human turn — something the author never said — and
|
|
23
|
+
* shifts every turn number after it.
|
|
24
|
+
*/
|
|
25
|
+
export function stripHarnessTags(text) {
|
|
26
|
+
return text.replace(PAIRED, '').replace(UNPAIRED, '').trim();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wraps content in a fence long enough to contain it. Tool payloads carry
|
|
30
|
+
* markdown, which carries its own fences; a fixed ``` would let one break out.
|
|
31
|
+
*/
|
|
32
|
+
export function fence(content, info) {
|
|
33
|
+
let longest = 0;
|
|
34
|
+
for (const run of content.matchAll(/`+/g))
|
|
35
|
+
longest = Math.max(longest, run[0].length);
|
|
36
|
+
const ticks = '`'.repeat(Math.max(3, longest + 1));
|
|
37
|
+
return `${ticks}${info}\n${content}\n${ticks}`;
|
|
38
|
+
}
|
|
39
|
+
/** Title lowercased, non-alphanumerics collapsed to hyphens, truncated to 60. */
|
|
40
|
+
export function slugify(title) {
|
|
41
|
+
const collapsed = title
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
44
|
+
.replace(/^-+|-+$/g, '');
|
|
45
|
+
return collapsed.slice(0, 60).replace(/-+$/, '') || 'untitled';
|
|
46
|
+
}
|
|
47
|
+
const EXTENSIONS = {
|
|
48
|
+
'image/png': 'png',
|
|
49
|
+
'image/jpeg': 'jpg',
|
|
50
|
+
'image/gif': 'gif',
|
|
51
|
+
'image/webp': 'webp',
|
|
52
|
+
};
|
|
53
|
+
/** Bytes in a human-readable size, for the marker form. */
|
|
54
|
+
function humanSize(bytes) {
|
|
55
|
+
if (bytes < 1024)
|
|
56
|
+
return `${bytes} B`;
|
|
57
|
+
if (bytes < 1024 * 1024)
|
|
58
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
59
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One image block as either a link to an extracted file or a marker.
|
|
63
|
+
*
|
|
64
|
+
* Naming by content hash is what keeps reruns stable: identical bytes always
|
|
65
|
+
* produce an identical filename, so the same screenshot pasted twice is stored
|
|
66
|
+
* once and a rerun rewrites nothing. An unrecognized media type falls back to
|
|
67
|
+
* a marker rather than guessing an extension.
|
|
68
|
+
*/
|
|
69
|
+
export function renderImage(block, context) {
|
|
70
|
+
const media = block.source?.media_type ?? 'unknown';
|
|
71
|
+
const data = block.source?.data;
|
|
72
|
+
if (typeof data !== 'string' || data === '') {
|
|
73
|
+
return `> [image: ${media}, no data]`;
|
|
74
|
+
}
|
|
75
|
+
const bytes = Buffer.from(data, 'base64');
|
|
76
|
+
const extension = EXTENSIONS[media];
|
|
77
|
+
if (context.policy?.images === 'marker' || extension === undefined) {
|
|
78
|
+
return `> [image: ${media}, ${humanSize(bytes.byteLength)}]`;
|
|
79
|
+
}
|
|
80
|
+
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8);
|
|
81
|
+
const filename = `${context.sessionId8 ?? 'session'}-${hash}.${extension}`;
|
|
82
|
+
if (context.collected && !context.collected.some((i) => i.filename === filename)) {
|
|
83
|
+
context.collected.push({ filename, mediaType: media, data: bytes });
|
|
84
|
+
}
|
|
85
|
+
return ``;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* One content block as markdown, or `null` when policy drops it.
|
|
89
|
+
*
|
|
90
|
+
* - `text` — verbatim.
|
|
91
|
+
* - `thinking` — dropped by default. Exploratory reasoning misleads a later
|
|
92
|
+
* reader: a hypothesis the assistant talked itself out of reads like a
|
|
93
|
+
* proposal if you find it out of context.
|
|
94
|
+
* - `tool_use` / `tool_result` — dropped by default. Anything authored in a
|
|
95
|
+
* Claude Code session lands in the working tree and is captured by the
|
|
96
|
+
* commit, so the payload is redundant with git while making up most of the
|
|
97
|
+
* raw bytes.
|
|
98
|
+
* - `image` — extracted to a file, or a marker.
|
|
99
|
+
* - anything else — a visible marker, never a silent drop.
|
|
100
|
+
*/
|
|
101
|
+
export function renderBlock(block, context = {}) {
|
|
102
|
+
const policy = context.policy ?? DEFAULT_CONTENT_POLICY;
|
|
103
|
+
switch (block.type) {
|
|
104
|
+
case 'text':
|
|
105
|
+
return typeof block.text === 'string' ? block.text : '';
|
|
106
|
+
case 'thinking':
|
|
107
|
+
return policy.thinking === 'keep'
|
|
108
|
+
? fence(typeof block.thinking === 'string' ? block.thinking : '', 'thinking')
|
|
109
|
+
: null;
|
|
110
|
+
case 'tool_use': {
|
|
111
|
+
if (policy.tools === 'strip')
|
|
112
|
+
return null;
|
|
113
|
+
const name = typeof block.name === 'string' ? block.name : 'unknown';
|
|
114
|
+
return fence(JSON.stringify(block.input ?? null, null, 2), `tool_use:${name}`);
|
|
115
|
+
}
|
|
116
|
+
case 'tool_result': {
|
|
117
|
+
if (policy.tools === 'strip')
|
|
118
|
+
return null;
|
|
119
|
+
const name = typeof block.name === 'string' ? block.name : 'unknown';
|
|
120
|
+
return fence(JSON.stringify(block.content ?? null, null, 2), `tool_result:${name}`);
|
|
121
|
+
}
|
|
122
|
+
case 'image':
|
|
123
|
+
return renderImage(block, context);
|
|
124
|
+
default:
|
|
125
|
+
return `> [unhandled block type: ${block.type ?? 'undefined'}]`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** The text of one record under the given policy. */
|
|
129
|
+
export function recordText(record, context = {}) {
|
|
130
|
+
const content = record.message?.content;
|
|
131
|
+
if (typeof content === 'string')
|
|
132
|
+
return content.trim();
|
|
133
|
+
if (!Array.isArray(content))
|
|
134
|
+
return '';
|
|
135
|
+
return content
|
|
136
|
+
.map((block) => renderBlock(block, context))
|
|
137
|
+
.filter((part) => part !== null && part.trim() !== '')
|
|
138
|
+
.join('\n\n')
|
|
139
|
+
.trim();
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=content.js.map
|