|
1 | 1 | import type { Argv } from "yargs" |
2 | 2 | import { Session } from "../../session" |
| 3 | +import { MessageV2 } from "../../session/message-v2" |
3 | 4 | import { SessionID } from "../../session/schema" |
4 | 5 | import { cmd } from "./cmd" |
5 | 6 | import { bootstrap } from "../bootstrap" |
6 | 7 | import { UI } from "../ui" |
7 | 8 | import * as prompts from "@clack/prompts" |
8 | 9 | import { EOL } from "os" |
9 | 10 | import { AppRuntime } from "@/effect/app-runtime" |
10 | | -import { Effect } from "effect" |
| 11 | + |
| 12 | +function redact(kind: string, id: string, value: string) { |
| 13 | + return value.trim() ? `[redacted:${kind}:${id}]` : value |
| 14 | +} |
| 15 | + |
| 16 | +function data(kind: string, id: string, value: Record<string, unknown> | undefined) { |
| 17 | + if (!value) return value |
| 18 | + return Object.keys(value).length ? { redacted: `${kind}:${id}` } : value |
| 19 | +} |
| 20 | + |
| 21 | +function span(id: string, value: { value: string; start: number; end: number }) { |
| 22 | + return { |
| 23 | + ...value, |
| 24 | + value: redact("file-text", id, value.value), |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +function diff(kind: string, diffs: { file: string; patch: string }[] | undefined) { |
| 29 | + return diffs?.map((item, i) => ({ |
| 30 | + ...item, |
| 31 | + file: redact(`${kind}-file`, String(i), item.file), |
| 32 | + patch: redact(`${kind}-patch`, String(i), item.patch), |
| 33 | + })) |
| 34 | +} |
| 35 | + |
| 36 | +function source(part: MessageV2.FilePart) { |
| 37 | + if (!part.source) return part.source |
| 38 | + if (part.source.type === "symbol") { |
| 39 | + return { |
| 40 | + ...part.source, |
| 41 | + path: redact("file-path", part.id, part.source.path), |
| 42 | + name: redact("file-symbol", part.id, part.source.name), |
| 43 | + text: span(part.id, part.source.text), |
| 44 | + } |
| 45 | + } |
| 46 | + if (part.source.type === "resource") { |
| 47 | + return { |
| 48 | + ...part.source, |
| 49 | + clientName: redact("file-client", part.id, part.source.clientName), |
| 50 | + uri: redact("file-uri", part.id, part.source.uri), |
| 51 | + text: span(part.id, part.source.text), |
| 52 | + } |
| 53 | + } |
| 54 | + return { |
| 55 | + ...part.source, |
| 56 | + path: redact("file-path", part.id, part.source.path), |
| 57 | + text: span(part.id, part.source.text), |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +function filepart(part: MessageV2.FilePart): MessageV2.FilePart { |
| 62 | + return { |
| 63 | + ...part, |
| 64 | + url: redact("file-url", part.id, part.url), |
| 65 | + filename: part.filename === undefined ? undefined : redact("file-name", part.id, part.filename), |
| 66 | + source: source(part), |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +function part(part: MessageV2.Part): MessageV2.Part { |
| 71 | + switch (part.type) { |
| 72 | + case "text": |
| 73 | + return { |
| 74 | + ...part, |
| 75 | + text: redact("text", part.id, part.text), |
| 76 | + metadata: data("text-metadata", part.id, part.metadata), |
| 77 | + } |
| 78 | + case "reasoning": |
| 79 | + return { |
| 80 | + ...part, |
| 81 | + text: redact("reasoning", part.id, part.text), |
| 82 | + metadata: data("reasoning-metadata", part.id, part.metadata), |
| 83 | + } |
| 84 | + case "file": |
| 85 | + return filepart(part) |
| 86 | + case "subtask": |
| 87 | + return { |
| 88 | + ...part, |
| 89 | + prompt: redact("subtask-prompt", part.id, part.prompt), |
| 90 | + description: redact("subtask-description", part.id, part.description), |
| 91 | + command: part.command === undefined ? undefined : redact("subtask-command", part.id, part.command), |
| 92 | + } |
| 93 | + case "tool": |
| 94 | + return { |
| 95 | + ...part, |
| 96 | + metadata: data("tool-metadata", part.id, part.metadata), |
| 97 | + state: |
| 98 | + part.state.status === "pending" |
| 99 | + ? { |
| 100 | + ...part.state, |
| 101 | + input: data("tool-input", part.id, part.state.input) ?? part.state.input, |
| 102 | + raw: redact("tool-raw", part.id, part.state.raw), |
| 103 | + } |
| 104 | + : part.state.status === "running" |
| 105 | + ? { |
| 106 | + ...part.state, |
| 107 | + input: data("tool-input", part.id, part.state.input) ?? part.state.input, |
| 108 | + title: part.state.title === undefined ? undefined : redact("tool-title", part.id, part.state.title), |
| 109 | + metadata: data("tool-state-metadata", part.id, part.state.metadata), |
| 110 | + } |
| 111 | + : part.state.status === "completed" |
| 112 | + ? { |
| 113 | + ...part.state, |
| 114 | + input: data("tool-input", part.id, part.state.input) ?? part.state.input, |
| 115 | + output: redact("tool-output", part.id, part.state.output), |
| 116 | + title: redact("tool-title", part.id, part.state.title), |
| 117 | + metadata: data("tool-state-metadata", part.id, part.state.metadata) ?? part.state.metadata, |
| 118 | + attachments: part.state.attachments?.map(filepart), |
| 119 | + } |
| 120 | + : { |
| 121 | + ...part.state, |
| 122 | + input: data("tool-input", part.id, part.state.input) ?? part.state.input, |
| 123 | + metadata: data("tool-state-metadata", part.id, part.state.metadata), |
| 124 | + }, |
| 125 | + } |
| 126 | + case "patch": |
| 127 | + return { |
| 128 | + ...part, |
| 129 | + hash: redact("patch", part.id, part.hash), |
| 130 | + files: part.files.map((item: string, i: number) => redact("patch-file", `${part.id}-${i}`, item)), |
| 131 | + } |
| 132 | + case "snapshot": |
| 133 | + return { |
| 134 | + ...part, |
| 135 | + snapshot: redact("snapshot", part.id, part.snapshot), |
| 136 | + } |
| 137 | + case "step-start": |
| 138 | + return { |
| 139 | + ...part, |
| 140 | + snapshot: part.snapshot === undefined ? undefined : redact("snapshot", part.id, part.snapshot), |
| 141 | + } |
| 142 | + case "step-finish": |
| 143 | + return { |
| 144 | + ...part, |
| 145 | + snapshot: part.snapshot === undefined ? undefined : redact("snapshot", part.id, part.snapshot), |
| 146 | + } |
| 147 | + case "agent": |
| 148 | + return { |
| 149 | + ...part, |
| 150 | + source: !part.source |
| 151 | + ? part.source |
| 152 | + : { |
| 153 | + ...part.source, |
| 154 | + value: redact("agent-source", part.id, part.source.value), |
| 155 | + }, |
| 156 | + } |
| 157 | + default: |
| 158 | + return part |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +const partFn = part |
| 163 | + |
| 164 | +function sanitize(data: { info: Session.Info; messages: MessageV2.WithParts[] }) { |
| 165 | + return { |
| 166 | + info: { |
| 167 | + ...data.info, |
| 168 | + title: redact("session-title", data.info.id, data.info.title), |
| 169 | + directory: redact("session-directory", data.info.id, data.info.directory), |
| 170 | + summary: !data.info.summary |
| 171 | + ? data.info.summary |
| 172 | + : { |
| 173 | + ...data.info.summary, |
| 174 | + diffs: diff("session-diff", data.info.summary.diffs), |
| 175 | + }, |
| 176 | + revert: !data.info.revert |
| 177 | + ? data.info.revert |
| 178 | + : { |
| 179 | + ...data.info.revert, |
| 180 | + snapshot: |
| 181 | + data.info.revert.snapshot === undefined |
| 182 | + ? undefined |
| 183 | + : redact("revert-snapshot", data.info.id, data.info.revert.snapshot), |
| 184 | + diff: |
| 185 | + data.info.revert.diff === undefined |
| 186 | + ? undefined |
| 187 | + : redact("revert-diff", data.info.id, data.info.revert.diff), |
| 188 | + }, |
| 189 | + }, |
| 190 | + messages: data.messages.map((msg) => ({ |
| 191 | + info: |
| 192 | + msg.info.role === "user" |
| 193 | + ? { |
| 194 | + ...msg.info, |
| 195 | + system: msg.info.system === undefined ? undefined : redact("system", msg.info.id, msg.info.system), |
| 196 | + summary: !msg.info.summary |
| 197 | + ? msg.info.summary |
| 198 | + : { |
| 199 | + ...msg.info.summary, |
| 200 | + title: |
| 201 | + msg.info.summary.title === undefined |
| 202 | + ? undefined |
| 203 | + : redact("summary-title", msg.info.id, msg.info.summary.title), |
| 204 | + body: |
| 205 | + msg.info.summary.body === undefined |
| 206 | + ? undefined |
| 207 | + : redact("summary-body", msg.info.id, msg.info.summary.body), |
| 208 | + diffs: diff("message-diff", msg.info.summary.diffs), |
| 209 | + }, |
| 210 | + } |
| 211 | + : { |
| 212 | + ...msg.info, |
| 213 | + path: { |
| 214 | + cwd: redact("cwd", msg.info.id, msg.info.path.cwd), |
| 215 | + root: redact("root", msg.info.id, msg.info.path.root), |
| 216 | + }, |
| 217 | + }, |
| 218 | + parts: msg.parts.map(partFn), |
| 219 | + })), |
| 220 | + } |
| 221 | +} |
11 | 222 |
|
12 | 223 | export const ExportCommand = cmd({ |
13 | 224 | command: "export [sessionID]", |
14 | 225 | describe: "export session data as JSON", |
15 | 226 | builder: (yargs: Argv) => { |
16 | | - return yargs.positional("sessionID", { |
17 | | - describe: "session id to export", |
18 | | - type: "string", |
19 | | - }) |
| 227 | + return yargs |
| 228 | + .positional("sessionID", { |
| 229 | + describe: "session id to export", |
| 230 | + type: "string", |
| 231 | + }) |
| 232 | + .option("sanitize", { |
| 233 | + describe: "redact sensitive transcript and file data", |
| 234 | + type: "boolean", |
| 235 | + }) |
20 | 236 | }, |
21 | 237 | handler: async (args) => { |
22 | 238 | await bootstrap(process.cwd(), async () => { |
@@ -69,26 +285,17 @@ export const ExportCommand = cmd({ |
69 | 285 | } |
70 | 286 |
|
71 | 287 | try { |
72 | | - const { sessionInfo, messages } = await AppRuntime.runPromise( |
73 | | - Effect.gen(function* () { |
74 | | - const session = yield* Session.Service |
75 | | - const sessionInfo = yield* session.get(sessionID!) |
76 | | - return { |
77 | | - sessionInfo, |
78 | | - messages: yield* session.messages({ sessionID: sessionInfo.id }), |
79 | | - } |
80 | | - }), |
| 288 | + const sessionInfo = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID!))) |
| 289 | + const messages = await AppRuntime.runPromise( |
| 290 | + Session.Service.use((svc) => svc.messages({ sessionID: sessionInfo.id })), |
81 | 291 | ) |
82 | 292 |
|
83 | 293 | const exportData = { |
84 | 294 | info: sessionInfo, |
85 | | - messages: messages.map((msg) => ({ |
86 | | - info: msg.info, |
87 | | - parts: msg.parts, |
88 | | - })), |
| 295 | + messages, |
89 | 296 | } |
90 | 297 |
|
91 | | - process.stdout.write(JSON.stringify(exportData, null, 2)) |
| 298 | + process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : exportData, null, 2)) |
92 | 299 | process.stdout.write(EOL) |
93 | 300 | } catch (error) { |
94 | 301 | UI.error(`Session not found: ${sessionID!}`) |
|
0 commit comments