@herbertgao/pi-extensions 2026.8.9 → 2026.8.11
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/README.md +4 -2
- package/node_modules/@herbertgao/resume-from/LICENSE +21 -0
- package/node_modules/@herbertgao/resume-from/README.md +163 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/adapter.d.ts +25 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/adapter.js +60 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/contract.d.ts +8 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/entries.d.ts +30 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/entries.js +107 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/index.d.ts +3 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/index.js +2 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/layout.d.ts +29 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/layout.js +80 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/readback.d.ts +9 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/readback.js +39 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/reader.d.ts +37 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/reader.js +424 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/redaction.d.ts +26 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/redaction.js +132 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/validation.d.ts +4 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/validation.js +39 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/writer.d.ts +23 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/writer.js +306 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/contract.d.ts +15 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/index.d.ts +6 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/index.js +44 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/read.d.ts +28 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/read.js +270 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/readback.d.ts +9 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/readback.js +47 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/redaction.d.ts +26 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/redaction.js +132 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/rollout.d.ts +75 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/rollout.js +185 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/validation.d.ts +7 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/validation.js +78 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/write.d.ts +17 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/codex/write.js +141 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/contract.d.ts +74 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/adapter.d.ts +15 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/adapter.js +209 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/contract.d.ts +24 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/format.d.ts +162 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/format.js +223 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/index.d.ts +7 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/index.js +10 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/parse.d.ts +41 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/parse.js +371 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/redaction.d.ts +26 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/redaction.js +132 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/serialize.d.ts +22 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/serialize.js +87 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/validate.d.ts +14 -0
- package/node_modules/@herbertgao/resume-from/dist/adapters/pi/validate.js +134 -0
- package/node_modules/@herbertgao/resume-from/dist/bin.d.ts +35 -0
- package/node_modules/@herbertgao/resume-from/dist/bin.js +135 -0
- package/node_modules/@herbertgao/resume-from/dist/contract.d.ts +10 -0
- package/node_modules/@herbertgao/resume-from/dist/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/host/agents.d.ts +29 -0
- package/node_modules/@herbertgao/resume-from/dist/host/agents.js +20 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/args.d.ts +21 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/args.js +187 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/contract.d.ts +27 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/index.d.ts +2 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/index.js +1 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/presentation.d.ts +2 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/presentation.js +11 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/render.d.ts +3 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/render.js +70 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/runner.d.ts +8 -0
- package/node_modules/@herbertgao/resume-from/dist/host/cli/runner.js +125 -0
- package/node_modules/@herbertgao/resume-from/dist/host/contract.d.ts +42 -0
- package/node_modules/@herbertgao/resume-from/dist/host/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/host/entry.d.ts +52 -0
- package/node_modules/@herbertgao/resume-from/dist/host/entry.js +91 -0
- package/node_modules/@herbertgao/resume-from/dist/host/index.d.ts +11 -0
- package/node_modules/@herbertgao/resume-from/dist/host/index.js +10 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/command.d.ts +18 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/command.js +103 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/contract.d.ts +34 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/index.d.ts +6 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/index.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/picker.d.ts +16 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/picker.js +35 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/presentation.d.ts +2 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/presentation.js +11 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/register.d.ts +25 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/register.js +11 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/ui.d.ts +20 -0
- package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/ui.js +1 -0
- package/node_modules/@herbertgao/resume-from/dist/host/profile.d.ts +8 -0
- package/node_modules/@herbertgao/resume-from/dist/host/profile.js +31 -0
- package/node_modules/@herbertgao/resume-from/dist/host/registry.d.ts +8 -0
- package/node_modules/@herbertgao/resume-from/dist/host/registry.js +33 -0
- package/node_modules/@herbertgao/resume-from/dist/host/wiring.d.ts +41 -0
- package/node_modules/@herbertgao/resume-from/dist/host/wiring.js +67 -0
- package/node_modules/@herbertgao/resume-from/dist/import/confirmation.d.ts +6 -0
- package/node_modules/@herbertgao/resume-from/dist/import/confirmation.js +27 -0
- package/node_modules/@herbertgao/resume-from/dist/import/contract.d.ts +41 -0
- package/node_modules/@herbertgao/resume-from/dist/import/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/contract.d.ts +58 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/errors.d.ts +9 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/errors.js +12 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/finder.d.ts +10 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/finder.js +129 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/homes.d.ts +23 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/homes.js +83 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/index.d.ts +2 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/index.js +2 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/ordering.d.ts +3 -0
- package/node_modules/@herbertgao/resume-from/dist/import/discovery/ordering.js +21 -0
- package/node_modules/@herbertgao/resume-from/dist/import/errors.d.ts +22 -0
- package/node_modules/@herbertgao/resume-from/dist/import/errors.js +20 -0
- package/node_modules/@herbertgao/resume-from/dist/import/index.d.ts +3 -0
- package/node_modules/@herbertgao/resume-from/dist/import/index.js +5 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/contract.d.ts +44 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/errors.d.ts +15 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/errors.js +15 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/handover.d.ts +5 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/handover.js +12 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/index.d.ts +4 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/index.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/lander.d.ts +7 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/lander.js +133 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/marker.d.ts +9 -0
- package/node_modules/@herbertgao/resume-from/dist/import/landing/marker.js +46 -0
- package/node_modules/@herbertgao/resume-from/dist/import/pipeline.d.ts +17 -0
- package/node_modules/@herbertgao/resume-from/dist/import/pipeline.js +134 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/builder.d.ts +6 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/builder.js +76 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/contract.d.ts +39 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/format.d.ts +19 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/format.js +45 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/index.d.ts +2 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/index.js +1 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/warnings.d.ts +11 -0
- package/node_modules/@herbertgao/resume-from/dist/import/preview/warnings.js +80 -0
- package/node_modules/@herbertgao/resume-from/dist/import/transfer/contract.d.ts +48 -0
- package/node_modules/@herbertgao/resume-from/dist/import/transfer/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/import/transfer/index.d.ts +2 -0
- package/node_modules/@herbertgao/resume-from/dist/import/transfer/index.js +1 -0
- package/node_modules/@herbertgao/resume-from/dist/import/transfer/rules.d.ts +3 -0
- package/node_modules/@herbertgao/resume-from/dist/import/transfer/rules.js +228 -0
- package/node_modules/@herbertgao/resume-from/dist/import/wiring.d.ts +24 -0
- package/node_modules/@herbertgao/resume-from/dist/import/wiring.js +27 -0
- package/node_modules/@herbertgao/resume-from/dist/index.d.ts +36 -0
- package/node_modules/@herbertgao/resume-from/dist/index.js +36 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/contract.d.ts +35 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/defaults.d.ts +16 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/defaults.js +22 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/errors.d.ts +9 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/errors.js +12 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/index.d.ts +5 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/index.js +6 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/loader.d.ts +10 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/loader.js +47 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/paths.d.ts +13 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/paths.js +37 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/validate.d.ts +9 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/config/validate.js +117 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/repo/contract.d.ts +30 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/repo/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/repo/git.d.ts +21 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/repo/git.js +74 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/repo/index.d.ts +2 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/repo/index.js +1 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/repo/reader.d.ts +8 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/repo/reader.js +130 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/store/contract.d.ts +31 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/store/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/store/file-committer.d.ts +3 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/store/file-committer.js +391 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/store/index.d.ts +2 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/store/index.js +2 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/tokens/contract.d.ts +12 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/tokens/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/tokens/estimator.d.ts +12 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/tokens/estimator.js +111 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/tokens/index.d.ts +2 -0
- package/node_modules/@herbertgao/resume-from/dist/platform/tokens/index.js +1 -0
- package/node_modules/@herbertgao/resume-from/dist/session/contract.d.ts +109 -0
- package/node_modules/@herbertgao/resume-from/dist/session/contract.js +4 -0
- package/node_modules/@herbertgao/resume-from/package.json +94 -0
- package/node_modules/@herbertgao/resume-from/shims/pi/extensions/resume-from.js +118 -0
- package/package.json +6 -2
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a Claude Code session into the neutral vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Two rules shape everything here: no result body crosses (FR-24) and no system prompt,
|
|
5
|
+
* environment value, token or hidden reasoning crosses (FR-28, NG-7). Both are enforced by
|
|
6
|
+
* what this file builds, not by what a caller does with it.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile, stat } from "node:fs/promises";
|
|
9
|
+
import { asArray, asObject, asString, ENTRY_TYPE_ASSISTANT, ENTRY_TYPE_SUMMARY, ENTRY_TYPE_USER, parseJsonl, resolveActiveEntryPath, toIsoUtc, } from "./entries.js";
|
|
10
|
+
import { listSessionFiles, sessionIdOf } from "./layout.js";
|
|
11
|
+
import { redactSensitiveStructure, redactSensitiveText } from "./redaction.js";
|
|
12
|
+
/** FR-25: every dropped body is marked in text the model can read. */
|
|
13
|
+
export const DROPPED_MARKER = "(content dropped: imported session, may be stale)";
|
|
14
|
+
export const UNREADABLE_TITLE = "(unreadable session file)";
|
|
15
|
+
export const UNTITLED = "(no user message)";
|
|
16
|
+
const MAX_TITLE_CHARS = 80;
|
|
17
|
+
const MAX_ARGUMENT_CHARS = 80;
|
|
18
|
+
const MAX_ARGUMENTS_CHARS = 200;
|
|
19
|
+
/** Claude Code tools that change the repository (FR-26). */
|
|
20
|
+
const MUTATING_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
21
|
+
/** Claude Code tools that only look. Everything else is "unknown", including Bash. */
|
|
22
|
+
const READ_ONLY_TOOLS = new Set([
|
|
23
|
+
"Read",
|
|
24
|
+
"Grep",
|
|
25
|
+
"Glob",
|
|
26
|
+
"LS",
|
|
27
|
+
"NotebookRead",
|
|
28
|
+
"WebFetch",
|
|
29
|
+
"WebSearch",
|
|
30
|
+
"TodoRead",
|
|
31
|
+
]);
|
|
32
|
+
/** Input keys that name the file a mutating call changed. */
|
|
33
|
+
const PATH_KEYS = ["file_path", "notebook_path", "path", "filePath"];
|
|
34
|
+
function truncate(text, max) {
|
|
35
|
+
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
36
|
+
}
|
|
37
|
+
function oneLine(text) {
|
|
38
|
+
return text.replace(/\s+/g, " ").trim();
|
|
39
|
+
}
|
|
40
|
+
function effectOf(toolName) {
|
|
41
|
+
if (MUTATING_TOOLS.has(toolName))
|
|
42
|
+
return "mutating";
|
|
43
|
+
if (READ_ONLY_TOOLS.has(toolName))
|
|
44
|
+
return "read-only";
|
|
45
|
+
return "unknown";
|
|
46
|
+
}
|
|
47
|
+
function renderValue(value) {
|
|
48
|
+
if (typeof value === "string")
|
|
49
|
+
return `'${truncate(oneLine(value), MAX_ARGUMENT_CHARS)}'`;
|
|
50
|
+
if (value === null || typeof value === "number" || typeof value === "boolean")
|
|
51
|
+
return String(value);
|
|
52
|
+
return truncate(oneLine(JSON.stringify(value) ?? ""), MAX_ARGUMENT_CHARS);
|
|
53
|
+
}
|
|
54
|
+
/** The redacted source arguments, shortened so one call stays one line. */
|
|
55
|
+
export function renderArguments(input) {
|
|
56
|
+
const safeInput = redactSensitiveStructure(input);
|
|
57
|
+
const object = asObject(safeInput);
|
|
58
|
+
if (object === null)
|
|
59
|
+
return safeInput === undefined ? "" : renderValue(safeInput);
|
|
60
|
+
return truncate(Object.values(object).map(renderValue).join(", "), MAX_ARGUMENTS_CHARS);
|
|
61
|
+
}
|
|
62
|
+
/** The text of a tool result, used for its size only. It is never carried (FR-24). */
|
|
63
|
+
function resultBodyText(content) {
|
|
64
|
+
if (typeof content === "string")
|
|
65
|
+
return content;
|
|
66
|
+
const blocks = asArray(content);
|
|
67
|
+
if (blocks === null)
|
|
68
|
+
return null;
|
|
69
|
+
const texts = blocks
|
|
70
|
+
.map((block) => asString(asObject(block)?.text))
|
|
71
|
+
.filter((text) => text !== null);
|
|
72
|
+
return texts.length === 0 ? null : texts.join("\n");
|
|
73
|
+
}
|
|
74
|
+
/** True when the source recorded a result of any shape — text, blocks, or a structured object. */
|
|
75
|
+
function hasResultBody(content) {
|
|
76
|
+
if (typeof content === "string")
|
|
77
|
+
return content !== "";
|
|
78
|
+
if (Array.isArray(content))
|
|
79
|
+
return content.length > 0;
|
|
80
|
+
return asObject(content) !== null;
|
|
81
|
+
}
|
|
82
|
+
/** Exactly one line about the outcome (FR-23). */
|
|
83
|
+
function summarizeResult(result) {
|
|
84
|
+
if (result === undefined)
|
|
85
|
+
return { text: "no result recorded", bodyDropped: false };
|
|
86
|
+
if (result.body === null) {
|
|
87
|
+
// A structured result: its size is unknown, but it existed and it is being dropped (FR-25).
|
|
88
|
+
if (result.present) {
|
|
89
|
+
return {
|
|
90
|
+
text: result.isError ? "error, result dropped" : "result recorded",
|
|
91
|
+
bodyDropped: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
text: result.isError ? "error" : "no result recorded",
|
|
96
|
+
bodyDropped: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const lines = result.body === "" ? 0 : result.body.split("\n").length;
|
|
100
|
+
const size = `${lines} line${lines === 1 ? "" : "s"}`;
|
|
101
|
+
return {
|
|
102
|
+
text: result.isError ? `error, ${size}` : size,
|
|
103
|
+
bodyDropped: result.body.length > 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function toolCallRecord(toolName, input, result) {
|
|
107
|
+
const argumentsText = renderArguments(input);
|
|
108
|
+
const { text, bodyDropped } = summarizeResult(result);
|
|
109
|
+
const line = `${toolName}(${argumentsText}) → ${text}`;
|
|
110
|
+
return {
|
|
111
|
+
toolName,
|
|
112
|
+
argumentsText,
|
|
113
|
+
outcomeLine: redactSensitiveText(bodyDropped ? `${line} ${DROPPED_MARKER}` : line),
|
|
114
|
+
effect: effectOf(toolName),
|
|
115
|
+
bodyDropped,
|
|
116
|
+
// FR-54: false when no tool_result block existed for this call id (broken tail signal).
|
|
117
|
+
resultRecorded: result !== undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Tool results, keyed by the id of the call they answer. */
|
|
121
|
+
function collectResults(entries) {
|
|
122
|
+
const results = new Map();
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (entry.type !== ENTRY_TYPE_USER)
|
|
125
|
+
continue;
|
|
126
|
+
const blocks = asArray(asObject(entry.message)?.content);
|
|
127
|
+
if (blocks === null)
|
|
128
|
+
continue;
|
|
129
|
+
for (const raw of blocks) {
|
|
130
|
+
const block = asObject(raw);
|
|
131
|
+
if (block === null || block.type !== "tool_result")
|
|
132
|
+
continue;
|
|
133
|
+
const id = asString(block.tool_use_id);
|
|
134
|
+
if (id === null)
|
|
135
|
+
continue;
|
|
136
|
+
results.set(id, {
|
|
137
|
+
body: resultBodyText(block.content),
|
|
138
|
+
present: hasResultBody(block.content) || asObject(entry.toolUseResult) !== null,
|
|
139
|
+
isError: block.is_error === true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
/** The visible text of a message. Tool-result blocks contribute nothing (FR-24). */
|
|
146
|
+
function messageText(content) {
|
|
147
|
+
if (typeof content === "string")
|
|
148
|
+
return content;
|
|
149
|
+
const blocks = asArray(content);
|
|
150
|
+
if (blocks === null)
|
|
151
|
+
return "";
|
|
152
|
+
const texts = [];
|
|
153
|
+
for (const raw of blocks) {
|
|
154
|
+
const block = asObject(raw);
|
|
155
|
+
if (block?.type === "text") {
|
|
156
|
+
const text = asString(block.text);
|
|
157
|
+
if (text !== null)
|
|
158
|
+
texts.push(text);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return texts.join("\n");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Claude records slash commands, their stdout, and !-prefixed shell input as user-shaped
|
|
165
|
+
* carriers. None of it is conversation, so none of it crosses (FR-28) — a carrier kept as a
|
|
166
|
+
* turn would also become the session title when it is the first user-shaped entry.
|
|
167
|
+
*/
|
|
168
|
+
function isLocalCommandCarrier(content) {
|
|
169
|
+
const text = messageText(content).trim();
|
|
170
|
+
return (/(?:^|\n)<local-command-stdout>[\s\S]*<\/local-command-stdout>(?:\n|$)/.test(text) ||
|
|
171
|
+
/(?:^|\n)<local-command-caveat>[\s\S]*<\/local-command-caveat>(?:\n|$)/.test(text) ||
|
|
172
|
+
// Tag order varies by Claude Code version: <command-message> may come first.
|
|
173
|
+
/^<command-(?:name|message|args)>[\s\S]*<\/command-(?:name|message|args)>/.test(text) ||
|
|
174
|
+
/^<bash-input>[\s\S]*<\/bash-input>(?:\n|$)/.test(text) ||
|
|
175
|
+
/^<bash-std(?:out|err)>[\s\S]*<\/bash-std(?:out|err)>(?:\n|$)/.test(text));
|
|
176
|
+
}
|
|
177
|
+
function firstLine(text, max) {
|
|
178
|
+
const line = oneLine(text.split("\n")[0] ?? "");
|
|
179
|
+
return truncate(line, max);
|
|
180
|
+
}
|
|
181
|
+
/** Read a whole session file into canonical turns. Pure: it takes the text, not a path. */
|
|
182
|
+
export function readSessionText(text) {
|
|
183
|
+
const parsed = parseJsonl(text);
|
|
184
|
+
const active = parsed.unreadable === null
|
|
185
|
+
? resolveActiveEntryPath(parsed.entries)
|
|
186
|
+
: { entries: [], unreadable: null };
|
|
187
|
+
const unreadable = parsed.unreadable ?? active.unreadable;
|
|
188
|
+
const results = collectResults(active.entries);
|
|
189
|
+
const turns = [];
|
|
190
|
+
const changedPaths = [];
|
|
191
|
+
let skipped = parsed.entries.length - active.entries.length;
|
|
192
|
+
let repoPath = null;
|
|
193
|
+
let branch = null;
|
|
194
|
+
if (unreadable === null) {
|
|
195
|
+
// Repository ownership is session metadata, not active-branch metadata. Claude can reset the
|
|
196
|
+
// active chain after changing cwd, but the session still belongs to the directory it started in.
|
|
197
|
+
for (const entry of parsed.entries) {
|
|
198
|
+
if (entry.isSidechain === true)
|
|
199
|
+
continue;
|
|
200
|
+
repoPath = asString(entry.cwd);
|
|
201
|
+
if (repoPath !== null)
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const push = (turn) => {
|
|
206
|
+
// FR-28, security: credentials pasted as message text must not cross to a different vendor.
|
|
207
|
+
// Tool-call turns carry text: "" so the condition is a no-op there (no double-redaction).
|
|
208
|
+
const text = turn.text.length > 0 ? redactSensitiveText(turn.text) : turn.text;
|
|
209
|
+
turns.push({ ...turn, text, index: turns.length });
|
|
210
|
+
};
|
|
211
|
+
for (const entry of active.entries) {
|
|
212
|
+
if (branch === null)
|
|
213
|
+
branch = asString(entry.gitBranch);
|
|
214
|
+
// A sidechain is a separate sub-conversation, not a turn of this session.
|
|
215
|
+
if (entry.isSidechain === true) {
|
|
216
|
+
skipped++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const timestamp = toIsoUtc(entry.timestamp);
|
|
220
|
+
if (entry.type === ENTRY_TYPE_USER && entry.isCompactSummary === true) {
|
|
221
|
+
const summary = messageText(asObject(entry.message)?.content);
|
|
222
|
+
if (summary.trim() === "") {
|
|
223
|
+
skipped++;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
push({
|
|
227
|
+
role: "agent",
|
|
228
|
+
kind: "summary",
|
|
229
|
+
text: summary,
|
|
230
|
+
toolCall: null,
|
|
231
|
+
timestamp,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (entry.type === ENTRY_TYPE_USER) {
|
|
237
|
+
// Meta entries carry injected context — environment blocks, command output, reminders.
|
|
238
|
+
// FR-28 keeps all of it out of the canonical model.
|
|
239
|
+
if (entry.isMeta === true) {
|
|
240
|
+
skipped++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const content = asObject(entry.message)?.content;
|
|
244
|
+
if (isLocalCommandCarrier(content)) {
|
|
245
|
+
skipped++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const text = messageText(content);
|
|
249
|
+
if (text.trim() === "")
|
|
250
|
+
continue; // a tool-result carrier; its outcome already crossed
|
|
251
|
+
push({ role: "user", kind: "message", text, toolCall: null, timestamp });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (entry.type === ENTRY_TYPE_ASSISTANT) {
|
|
255
|
+
const content = asObject(entry.message)?.content;
|
|
256
|
+
if (typeof content === "string") {
|
|
257
|
+
push({
|
|
258
|
+
role: "agent",
|
|
259
|
+
kind: "message",
|
|
260
|
+
text: content,
|
|
261
|
+
toolCall: null,
|
|
262
|
+
timestamp,
|
|
263
|
+
});
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const blocks = asArray(content);
|
|
267
|
+
if (blocks === null) {
|
|
268
|
+
skipped++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
let pending = [];
|
|
272
|
+
const flush = () => {
|
|
273
|
+
if (pending.length === 0)
|
|
274
|
+
return;
|
|
275
|
+
push({
|
|
276
|
+
role: "agent",
|
|
277
|
+
kind: "message",
|
|
278
|
+
text: pending.join("\n"),
|
|
279
|
+
toolCall: null,
|
|
280
|
+
timestamp,
|
|
281
|
+
});
|
|
282
|
+
pending = [];
|
|
283
|
+
};
|
|
284
|
+
for (const raw of blocks) {
|
|
285
|
+
const block = asObject(raw);
|
|
286
|
+
const type = block?.type;
|
|
287
|
+
if (block === null) {
|
|
288
|
+
skipped++;
|
|
289
|
+
}
|
|
290
|
+
else if (type === "text") {
|
|
291
|
+
const value = asString(block.text);
|
|
292
|
+
if (value !== null)
|
|
293
|
+
pending.push(value);
|
|
294
|
+
}
|
|
295
|
+
else if (type === "tool_use") {
|
|
296
|
+
flush();
|
|
297
|
+
const name = asString(block.name) ?? "unknown-tool";
|
|
298
|
+
const id = asString(block.id);
|
|
299
|
+
const record = toolCallRecord(name, block.input, id === null ? undefined : results.get(id));
|
|
300
|
+
if (record.effect === "mutating") {
|
|
301
|
+
const input = asObject(block.input);
|
|
302
|
+
for (const key of PATH_KEYS) {
|
|
303
|
+
const value = asString(input?.[key]);
|
|
304
|
+
if (value !== null) {
|
|
305
|
+
if (!changedPaths.includes(value))
|
|
306
|
+
changedPaths.push(value);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
push({
|
|
312
|
+
role: "agent",
|
|
313
|
+
kind: "tool-call",
|
|
314
|
+
text: "",
|
|
315
|
+
toolCall: record,
|
|
316
|
+
timestamp,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
// thinking, redacted_thinking, server tool state: hidden reasoning and vendor state
|
|
321
|
+
// never cross (FR-28, NG-8).
|
|
322
|
+
skipped++;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
flush();
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (entry.type === ENTRY_TYPE_SUMMARY) {
|
|
329
|
+
const summary = asString(entry.summary);
|
|
330
|
+
if (summary !== null) {
|
|
331
|
+
push({
|
|
332
|
+
role: "agent",
|
|
333
|
+
kind: "summary",
|
|
334
|
+
text: summary,
|
|
335
|
+
toolCall: null,
|
|
336
|
+
timestamp,
|
|
337
|
+
});
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Every other entry type of C-3: system, attachment, file history, mode, and the rest.
|
|
342
|
+
skipped++;
|
|
343
|
+
}
|
|
344
|
+
const firstUser = turns.find((turn) => turn.role === "user" && turn.kind === "message");
|
|
345
|
+
const stamps = turns
|
|
346
|
+
.map((turn) => turn.timestamp)
|
|
347
|
+
.filter((value) => value !== null);
|
|
348
|
+
return {
|
|
349
|
+
turns,
|
|
350
|
+
skipped,
|
|
351
|
+
unreadable,
|
|
352
|
+
title: firstUser === undefined ? "" : firstLine(firstUser.text, MAX_TITLE_CHARS),
|
|
353
|
+
startedAt: stamps[0] ?? null,
|
|
354
|
+
updatedAt: stamps[stamps.length - 1] ?? null,
|
|
355
|
+
repoPath,
|
|
356
|
+
branch,
|
|
357
|
+
changedPaths,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** One row per session file of the home, newest first (FR-11, FR-14). */
|
|
361
|
+
export async function listSessions(home) {
|
|
362
|
+
const descriptors = [];
|
|
363
|
+
// Whole-file read and full parse per file: title, turn count, and updatedAt all require
|
|
364
|
+
// parsing the entire JSONL (FR-11). Benchmarked at ≈ 5 ms per 2 MB file (warm OS cache);
|
|
365
|
+
// 200 such files cost ≈ 1 s. Ceiling accepted; revisit if a home with thousands of
|
|
366
|
+
// large sessions makes the listing noticeably slow to users.
|
|
367
|
+
for (const file of await listSessionFiles(home)) {
|
|
368
|
+
let text;
|
|
369
|
+
let modified;
|
|
370
|
+
try {
|
|
371
|
+
text = await readFile(file, "utf8");
|
|
372
|
+
modified = (await stat(file)).mtime.toISOString();
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const read = readSessionText(text);
|
|
378
|
+
const broken = read.unreadable !== null;
|
|
379
|
+
descriptors.push({
|
|
380
|
+
ref: { agent: "claude-code", home, id: sessionIdOf(file) },
|
|
381
|
+
title: broken ? UNREADABLE_TITLE : read.title === "" ? UNTITLED : read.title,
|
|
382
|
+
startedAt: broken ? modified : (read.startedAt ?? modified),
|
|
383
|
+
updatedAt: broken ? modified : (read.updatedAt ?? modified),
|
|
384
|
+
turnCount: broken ? 0 : read.turns.length,
|
|
385
|
+
repoPath: read.repoPath,
|
|
386
|
+
filePath: file,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
descriptors.sort((a, b) => {
|
|
390
|
+
if (a.updatedAt !== b.updatedAt)
|
|
391
|
+
return a.updatedAt < b.updatedAt ? 1 : -1;
|
|
392
|
+
return a.ref.id < b.ref.id ? -1 : 1;
|
|
393
|
+
});
|
|
394
|
+
return descriptors;
|
|
395
|
+
}
|
|
396
|
+
/** One session in the neutral vocabulary. A file cut mid-entry is reported, never guessed at. */
|
|
397
|
+
export async function loadSession(descriptor) {
|
|
398
|
+
let text;
|
|
399
|
+
try {
|
|
400
|
+
text = await readFile(descriptor.filePath, "utf8");
|
|
401
|
+
}
|
|
402
|
+
catch (cause) {
|
|
403
|
+
throw new Error(`cannot read Claude Code session file ${descriptor.filePath}`, { cause });
|
|
404
|
+
}
|
|
405
|
+
const read = readSessionText(text);
|
|
406
|
+
if (read.unreadable !== null) {
|
|
407
|
+
throw new Error(`unreadable Claude Code session file ${descriptor.filePath}: ${read.unreadable}`);
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
provenance: {
|
|
411
|
+
ref: descriptor.ref,
|
|
412
|
+
title: descriptor.title,
|
|
413
|
+
startedAt: read.startedAt ?? descriptor.startedAt,
|
|
414
|
+
updatedAt: read.updatedAt ?? descriptor.updatedAt,
|
|
415
|
+
repo: {
|
|
416
|
+
// Claude Code records the branch on every entry but never the commit.
|
|
417
|
+
commit: null,
|
|
418
|
+
branch: read.branch,
|
|
419
|
+
changedPaths: read.changedPaths,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
turns: read.turns,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction — shared implementation kept in three adapter subfolders.
|
|
3
|
+
*
|
|
4
|
+
* Files:
|
|
5
|
+
* src/adapters/claude-code/redaction.ts
|
|
6
|
+
* src/adapters/codex/redaction.ts
|
|
7
|
+
* src/adapters/pi/redaction.ts
|
|
8
|
+
*
|
|
9
|
+
* These three files are byte-identical by design. A fix in one MUST be applied to all three.
|
|
10
|
+
* src/adapters/boundary.test.ts asserts byte equality and will fail if they drift.
|
|
11
|
+
*
|
|
12
|
+
* Why not a single shared module: src/adapters/contract.ts is types-only (no behaviour). A
|
|
13
|
+
* platform service would be imported as behaviour, and T-ROO-7 requires adapter-to-platform
|
|
14
|
+
* cross-module imports to go through contract.js — which can only export types, not functions.
|
|
15
|
+
*/
|
|
16
|
+
/** Stable replacement used for credentials that must not cross an adapter boundary. */
|
|
17
|
+
export declare const REDACTED_VALUE = "[REDACTED]";
|
|
18
|
+
/**
|
|
19
|
+
* Redact credentials in JSON-like tool inputs without changing the caller's value.
|
|
20
|
+
* Adapter-specific parsing stays in each adapter; this function only handles secret values.
|
|
21
|
+
*/
|
|
22
|
+
export declare function redactSensitiveStructure(value: unknown): unknown;
|
|
23
|
+
/** Redact recognizable credentials from shell commands, headers, and unstructured arguments. */
|
|
24
|
+
export declare function redactSensitiveText(text: string): string;
|
|
25
|
+
/** Redact a source-recorded argument string, preserving valid JSON when it is JSON. */
|
|
26
|
+
export declare function redactSensitiveArgumentsText(text: string): string;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction — shared implementation kept in three adapter subfolders.
|
|
3
|
+
*
|
|
4
|
+
* Files:
|
|
5
|
+
* src/adapters/claude-code/redaction.ts
|
|
6
|
+
* src/adapters/codex/redaction.ts
|
|
7
|
+
* src/adapters/pi/redaction.ts
|
|
8
|
+
*
|
|
9
|
+
* These three files are byte-identical by design. A fix in one MUST be applied to all three.
|
|
10
|
+
* src/adapters/boundary.test.ts asserts byte equality and will fail if they drift.
|
|
11
|
+
*
|
|
12
|
+
* Why not a single shared module: src/adapters/contract.ts is types-only (no behaviour). A
|
|
13
|
+
* platform service would be imported as behaviour, and T-ROO-7 requires adapter-to-platform
|
|
14
|
+
* cross-module imports to go through contract.js — which can only export types, not functions.
|
|
15
|
+
*/
|
|
16
|
+
/** Stable replacement used for credentials that must not cross an adapter boundary. */
|
|
17
|
+
export const REDACTED_VALUE = "[REDACTED]";
|
|
18
|
+
const ENVIRONMENT_KEYS = new Set(["env", "environment", "environmentvariables"]);
|
|
19
|
+
const SENSITIVE_KEYS = new Set([
|
|
20
|
+
"authorization",
|
|
21
|
+
"clientsecret",
|
|
22
|
+
"cookie",
|
|
23
|
+
"credentials",
|
|
24
|
+
"password",
|
|
25
|
+
"passwd",
|
|
26
|
+
"privatekey",
|
|
27
|
+
"proxyauthorization",
|
|
28
|
+
"secret",
|
|
29
|
+
"setcookie",
|
|
30
|
+
"signingkey",
|
|
31
|
+
"token",
|
|
32
|
+
]);
|
|
33
|
+
const PRIVATE_KEY_PATTERN = /-----BEGIN ([A-Z0-9 ]*PRIVATE KEY)-----[\s\S]*?-----END \1-----/g;
|
|
34
|
+
const AUTHORIZATION_PATTERN = /(\b(?:authorization|proxy-authorization)\s*:\s*)(?:Bearer|Basic)\s+[^\s,;]+/gi;
|
|
35
|
+
const API_HEADER_PATTERN = /(\b(?:x-api-key|api-key|x-auth-token)\s*:\s*)[^\s,;]+/gi;
|
|
36
|
+
const CREDENTIAL_PATTERN = /\b(?:sk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{12,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,})\b/g;
|
|
37
|
+
const ASSIGNMENT_PATTERN = /\b([A-Za-z_][A-Za-z0-9_]*)(\s*=\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s;&|]+)/g;
|
|
38
|
+
const FLAG_PATTERN = /(--(?:api[-_]?key|access[-_]?token|auth[-_]?token|token|password|passwd|secret|client[-_]?secret|private[-_]?key))(=|\s+)("[^"\r\n]*"|'[^'\r\n]*'|[^\s;&|]+)/gi;
|
|
39
|
+
const USER_CREDENTIAL_PATTERN = /(^|[\s;&|])(-u|--user)(=|\s+)("[^"\r\n]*"|'[^'\r\n]*'|[^\s;&|]+)/g;
|
|
40
|
+
const URI_USERINFO_PATTERN = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/:@]+):([^\s/@]+)@/gi;
|
|
41
|
+
// Unquoted value requires a structural delimiter or line/string end following it, OR a digit
|
|
42
|
+
// somewhere in the token (C-RED-1: "token: expired" is plain English and stays, while
|
|
43
|
+
// "password: hunter2 # prod" carries a credential that must not cross even mid-line).
|
|
44
|
+
const JSON_LIKE_PATTERN = /(["']?)([A-Za-z_][A-Za-z0-9_-]*)\1(\s*:\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}]+(?=[,;}]|[\r\n]|$)|[^\s,;}]*\d[^\s,;}]*)/g;
|
|
45
|
+
function normalizedKey(key) {
|
|
46
|
+
return key.toLowerCase().replaceAll(/[^a-z0-9]/g, "");
|
|
47
|
+
}
|
|
48
|
+
function isSensitiveKey(key) {
|
|
49
|
+
const normalized = normalizedKey(key);
|
|
50
|
+
return (SENSITIVE_KEYS.has(normalized) ||
|
|
51
|
+
normalized.endsWith("apikey") ||
|
|
52
|
+
normalized.endsWith("accesskey") ||
|
|
53
|
+
normalized.endsWith("password") ||
|
|
54
|
+
normalized.endsWith("privatekey") ||
|
|
55
|
+
normalized.endsWith("secret") ||
|
|
56
|
+
normalized.endsWith("token"));
|
|
57
|
+
}
|
|
58
|
+
function redactEnvironmentValue(value) {
|
|
59
|
+
if (Array.isArray(value))
|
|
60
|
+
return value.map(redactEnvironmentValue);
|
|
61
|
+
if (typeof value === "object" && value !== null) {
|
|
62
|
+
return Object.fromEntries(Object.keys(value).map((key) => [
|
|
63
|
+
key,
|
|
64
|
+
redactEnvironmentValue(value[key]),
|
|
65
|
+
]));
|
|
66
|
+
}
|
|
67
|
+
return value === undefined ? undefined : REDACTED_VALUE;
|
|
68
|
+
}
|
|
69
|
+
function redactSensitiveArray(values) {
|
|
70
|
+
const redacted = values.map(redactSensitiveStructure);
|
|
71
|
+
for (let index = 0; index < values.length - 1; index += 1) {
|
|
72
|
+
const flag = values[index];
|
|
73
|
+
if (typeof flag === "string" &&
|
|
74
|
+
(flag.trim() === "-u" || flag.trim() === "--user") &&
|
|
75
|
+
typeof values[index + 1] === "string") {
|
|
76
|
+
redacted[index + 1] = REDACTED_VALUE;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return redacted;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Redact credentials in JSON-like tool inputs without changing the caller's value.
|
|
83
|
+
* Adapter-specific parsing stays in each adapter; this function only handles secret values.
|
|
84
|
+
*/
|
|
85
|
+
export function redactSensitiveStructure(value) {
|
|
86
|
+
if (typeof value === "string")
|
|
87
|
+
return redactSensitiveText(value);
|
|
88
|
+
if (Array.isArray(value))
|
|
89
|
+
return redactSensitiveArray(value);
|
|
90
|
+
if (typeof value !== "object" || value === null)
|
|
91
|
+
return value;
|
|
92
|
+
const redacted = {};
|
|
93
|
+
for (const [key, child] of Object.entries(value)) {
|
|
94
|
+
if (isSensitiveKey(key)) {
|
|
95
|
+
redacted[key] = REDACTED_VALUE;
|
|
96
|
+
}
|
|
97
|
+
else if ((normalizedKey(key) === "user" || normalizedKey(key) === "username") &&
|
|
98
|
+
typeof child === "string" &&
|
|
99
|
+
child.includes(":")) {
|
|
100
|
+
redacted[key] = REDACTED_VALUE;
|
|
101
|
+
}
|
|
102
|
+
else if (ENVIRONMENT_KEYS.has(normalizedKey(key))) {
|
|
103
|
+
redacted[key] = redactEnvironmentValue(child);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
redacted[key] = redactSensitiveStructure(child);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return redacted;
|
|
110
|
+
}
|
|
111
|
+
/** Redact recognizable credentials from shell commands, headers, and unstructured arguments. */
|
|
112
|
+
export function redactSensitiveText(text) {
|
|
113
|
+
return text
|
|
114
|
+
.replace(PRIVATE_KEY_PATTERN, REDACTED_VALUE)
|
|
115
|
+
.replace(AUTHORIZATION_PATTERN, `$1${REDACTED_VALUE}`)
|
|
116
|
+
.replace(API_HEADER_PATTERN, `$1${REDACTED_VALUE}`)
|
|
117
|
+
.replace(FLAG_PATTERN, `$1$2${REDACTED_VALUE}`)
|
|
118
|
+
.replace(USER_CREDENTIAL_PATTERN, `$1$2$3${REDACTED_VALUE}`)
|
|
119
|
+
.replace(URI_USERINFO_PATTERN, `$1$2:${REDACTED_VALUE}@`)
|
|
120
|
+
.replace(ASSIGNMENT_PATTERN, (match, key, separator) => isSensitiveKey(key) ? `${key}${separator}${REDACTED_VALUE}` : match)
|
|
121
|
+
.replace(JSON_LIKE_PATTERN, (match, quote, key, separator) => isSensitiveKey(key) ? `${quote}${key}${quote}${separator}${REDACTED_VALUE}` : match)
|
|
122
|
+
.replace(CREDENTIAL_PATTERN, REDACTED_VALUE);
|
|
123
|
+
}
|
|
124
|
+
/** Redact a source-recorded argument string, preserving valid JSON when it is JSON. */
|
|
125
|
+
export function redactSensitiveArgumentsText(text) {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.stringify(redactSensitiveStructure(JSON.parse(text)));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return redactSensitiveText(text);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SessionId } from "./contract.js";
|
|
2
|
+
import { type RawEntry } from "./entries.js";
|
|
3
|
+
/** Structural facts required for a committed Claude transcript to be resumable by identity. */
|
|
4
|
+
export declare function isStructurallyOpenable(entries: RawEntry[], sessionId: SessionId): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { asArray, asObject, asString, ENTRY_TYPE_ASSISTANT, ENTRY_TYPE_USER, resolveActiveEntryPath, toIsoUtc, } from "./entries.js";
|
|
2
|
+
function hasVisibleContent(content) {
|
|
3
|
+
return (typeof content === "string" && content.length > 0) || (asArray(content)?.length ?? 0) > 0;
|
|
4
|
+
}
|
|
5
|
+
function isStoredTurn(entry, sessionId) {
|
|
6
|
+
const expectedRole = entry.type === ENTRY_TYPE_ASSISTANT ? "assistant" : "user";
|
|
7
|
+
const message = asObject(entry.message);
|
|
8
|
+
if (asString(entry.uuid) === null ||
|
|
9
|
+
entry.sessionId !== sessionId ||
|
|
10
|
+
toIsoUtc(entry.timestamp) === null ||
|
|
11
|
+
(entry.parentUuid !== null && typeof entry.parentUuid !== "string") ||
|
|
12
|
+
message?.role !== expectedRole ||
|
|
13
|
+
!hasVisibleContent(message.content)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (entry.type === ENTRY_TYPE_ASSISTANT) {
|
|
17
|
+
return asString(message.model) !== null && asObject(message.usage) !== null;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/** Structural facts required for a committed Claude transcript to be resumable by identity. */
|
|
22
|
+
export function isStructurallyOpenable(entries, sessionId) {
|
|
23
|
+
let turnCount = 0;
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.sessionId !== undefined && entry.sessionId !== sessionId)
|
|
26
|
+
return false;
|
|
27
|
+
if (entry.type !== ENTRY_TYPE_USER && entry.type !== ENTRY_TYPE_ASSISTANT)
|
|
28
|
+
continue;
|
|
29
|
+
turnCount++;
|
|
30
|
+
if (!isStoredTurn(entry, sessionId))
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (turnCount === 0)
|
|
34
|
+
return false;
|
|
35
|
+
const active = resolveActiveEntryPath(entries);
|
|
36
|
+
if (active.unreadable !== null)
|
|
37
|
+
return false;
|
|
38
|
+
return active.entries.some((entry) => entry.type === ENTRY_TYPE_USER || entry.type === ENTRY_TYPE_ASSISTANT);
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a canonical session into a new Claude Code session file.
|
|
3
|
+
*
|
|
4
|
+
* C-9 measured that a file holding one `user` entry and one `assistant` entry is listed by the
|
|
5
|
+
* native resume list and opened by `claude --resume <id>` as native turns. This module writes
|
|
6
|
+
* exactly those two entry types and nothing else: the other eight types of C-3 are eight more
|
|
7
|
+
* ways to corrupt an internal application database.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here touches the filesystem except to read. `serialize` returns bytes;
|
|
10
|
+
* `src/import/landing/` creates the files (FR-49, FR-53).
|
|
11
|
+
*/
|
|
12
|
+
import type { CanonicalSession, ProvenanceMarker, SerializedSession, TargetProfile, ValidationDefect } from "./contract.js";
|
|
13
|
+
/** The version C-9 was measured against. Written so the target reads a version it knows. */
|
|
14
|
+
export declare const CLAUDE_CODE_FORMAT_VERSION = "2.1.220";
|
|
15
|
+
/** The model field of an imported turn. No model produced it, and none is claimed. */
|
|
16
|
+
export declare const IMPORTED_MODEL = "imported";
|
|
17
|
+
export declare const USER_TYPE = "external";
|
|
18
|
+
/** Bytes only: the file is created by `src/import/landing/`, never here (FR-49, FR-53). */
|
|
19
|
+
export declare function serialize(session: CanonicalSession, target: TargetProfile, marker: ProvenanceMarker, context: {
|
|
20
|
+
cwd: string;
|
|
21
|
+
}): SerializedSession;
|
|
22
|
+
/** Every structural defect, not the first (FR-50). Empty means the bytes may be placed. */
|
|
23
|
+
export declare function validate(serialized: SerializedSession): ValidationDefect[];
|