dexto 2.0.0-beta.0 → 2.0.0-beta.1
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/dist/agents/coding-agent/coding-agent.yml +0 -2
- package/dist/cli/agent-audit.d.ts +12 -0
- package/dist/cli/agent-audit.d.ts.map +1 -0
- package/dist/cli/agent-audit.js +52 -0
- package/dist/cli/cloud/command.d.ts +3 -39
- package/dist/cli/cloud/command.d.ts.map +1 -1
- package/dist/cli/cloud/command.js +12 -60
- package/dist/cli/cloud/commands/account.d.ts.map +1 -1
- package/dist/cli/cloud/commands/account.js +2 -1
- package/dist/cli/cloud/commands/agents.d.ts.map +1 -1
- package/dist/cli/cloud/commands/agents.js +3 -1
- package/dist/cli/cloud/commands/artifacts.d.ts +3 -0
- package/dist/cli/cloud/commands/artifacts.d.ts.map +1 -0
- package/dist/cli/cloud/commands/artifacts.js +267 -0
- package/dist/cli/cloud/commands/automations.d.ts.map +1 -1
- package/dist/cli/cloud/commands/automations.js +2 -1
- package/dist/cli/cloud/commands/chats.d.ts.map +1 -1
- package/dist/cli/cloud/commands/chats.js +20 -5
- package/dist/cli/cloud/commands/data-deletions.d.ts.map +1 -1
- package/dist/cli/cloud/commands/data-deletions.js +2 -1
- package/dist/cli/cloud/commands/deploys.d.ts.map +1 -1
- package/dist/cli/cloud/commands/deploys.js +3 -1
- package/dist/cli/cloud/commands/drive.d.ts.map +1 -1
- package/dist/cli/cloud/commands/drive.js +2 -1
- package/dist/cli/cloud/commands/files.d.ts.map +1 -1
- package/dist/cli/cloud/commands/files.js +3 -1
- package/dist/cli/cloud/commands/models.d.ts.map +1 -1
- package/dist/cli/cloud/commands/models.js +3 -1
- package/dist/cli/cloud/commands/profile.d.ts.map +1 -1
- package/dist/cli/cloud/commands/profile.js +2 -1
- package/dist/cli/cloud/commands/publish.d.ts.map +1 -1
- package/dist/cli/cloud/commands/publish.js +2 -1
- package/dist/cli/cloud/commands/sandboxes.d.ts.map +1 -1
- package/dist/cli/cloud/commands/sandboxes.js +2 -1
- package/dist/cli/cloud/commands/secrets.d.ts.map +1 -1
- package/dist/cli/cloud/commands/secrets.js +2 -1
- package/dist/cli/cloud/commands/skills.d.ts.map +1 -1
- package/dist/cli/cloud/commands/skills.js +2 -1
- package/dist/cli/cloud/commands/traces.d.ts +3 -0
- package/dist/cli/cloud/commands/traces.d.ts.map +1 -0
- package/dist/cli/cloud/commands/traces.js +142 -0
- package/dist/cli/cloud/commands/workspace-apps.d.ts.map +1 -1
- package/dist/cli/cloud/commands/workspace-apps.js +2 -1
- package/dist/cli/cloud/commands/workspace.d.ts.map +1 -1
- package/dist/cli/cloud/commands/workspace.js +3 -1
- package/dist/cli/cloud/host.d.ts.map +1 -1
- package/dist/cli/cloud/host.js +4 -0
- package/dist/cli/cloud/output.d.ts.map +1 -1
- package/dist/cli/cloud/output.js +2 -1
- package/dist/cli/cloud/run.d.ts.map +1 -1
- package/dist/cli/cloud/run.js +10 -10
- package/dist/cli/cloud/transfer.d.ts.map +1 -1
- package/dist/cli/cloud/transfer.js +2 -1
- package/dist/cli/cloud/v1.d.ts +2 -1
- package/dist/cli/cloud/v1.d.ts.map +1 -1
- package/dist/cli/cloud/v1.js +2 -1
- package/dist/cli/commands/auth/sign-in-commands.d.ts.map +1 -1
- package/dist/cli/commands/auth/sign-in-commands.js +2 -1
- package/dist/cli/commands/cloud/register.d.ts.map +1 -1
- package/dist/cli/commands/cloud/register.js +2 -0
- package/dist/cli/commands/run/headless.d.ts.map +1 -1
- package/dist/cli/commands/run/headless.js +55 -0
- package/dist/cli/commands/run/step-log.d.ts +11 -0
- package/dist/cli/commands/run/step-log.d.ts.map +1 -0
- package/dist/cli/commands/run/step-log.js +42 -0
- package/dist/cli/failure.d.ts +29 -0
- package/dist/cli/failure.d.ts.map +1 -0
- package/dist/cli/failure.js +31 -0
- package/dist/cli/help-json.d.ts.map +1 -1
- package/dist/cli/help-json.js +1 -1
- package/dist/cli/import/claude-code.d.ts +10 -0
- package/dist/cli/import/claude-code.d.ts.map +1 -0
- package/dist/cli/import/claude-code.js +131 -0
- package/dist/cli/import/codex.d.ts +27 -0
- package/dist/cli/import/codex.d.ts.map +1 -0
- package/dist/cli/import/codex.js +148 -0
- package/dist/cli/import/git.d.ts +19 -0
- package/dist/cli/import/git.d.ts.map +1 -0
- package/dist/cli/import/git.js +95 -0
- package/dist/cli/import/local-chats.d.ts +28 -0
- package/dist/cli/import/local-chats.d.ts.map +1 -0
- package/dist/cli/import/local-chats.js +129 -0
- package/dist/cli/import/redaction.d.ts +37 -0
- package/dist/cli/import/redaction.d.ts.map +1 -0
- package/dist/cli/import/redaction.js +148 -0
- package/dist/cli/import/test-utils.d.ts +16 -0
- package/dist/cli/import/test-utils.d.ts.map +1 -0
- package/dist/cli/import/test-utils.js +223 -0
- package/dist/cli/import/transcript.d.ts +75 -0
- package/dist/cli/import/transcript.d.ts.map +1 -0
- package/dist/cli/import/transcript.js +123 -0
- package/dist/cli/interaction.d.ts +18 -0
- package/dist/cli/interaction.d.ts.map +1 -0
- package/dist/cli/interaction.js +30 -0
- package/dist/cli/output.d.ts +9 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +6 -0
- package/dist/index-main.js +12 -1
- package/package.json +10 -10
- package/dist/cli/cloud/local-chats.d.ts +0 -73
- package/dist/cli/cloud/local-chats.d.ts.map +0 -1
- package/dist/cli/cloud/local-chats.js +0 -441
|
@@ -1,441 +0,0 @@
|
|
|
1
|
-
// Reads Claude Code and Codex chats on this machine into redacted, bounded turns to import.
|
|
2
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { UsageError } from './command.js';
|
|
5
|
-
export const LOCAL_CHAT_SOURCES = ['claude-code', 'codex'];
|
|
6
|
-
export const MAX_IMPORT_MESSAGES = 2_000;
|
|
7
|
-
export const MAX_MESSAGE_CHARS = 64_000;
|
|
8
|
-
const MAX_IMPORT_CHARS = 4_000_000;
|
|
9
|
-
const MAX_TITLE_CHARS = 200;
|
|
10
|
-
const MAX_CWD_CHARS = 1_024;
|
|
11
|
-
const MAX_BRANCH_CHARS = 255;
|
|
12
|
-
/** How much of one tool call's input or result an imported chat keeps. */
|
|
13
|
-
const MAX_TOOL_CHARS = 4_000;
|
|
14
|
-
/** A transcript file larger than this is not read. */
|
|
15
|
-
const MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
16
|
-
const SOURCE_SESSION_ID = /^[A-Za-z0-9._:-]{1,200}$/u;
|
|
17
|
-
// ---------------------------------------------------------------------------------------------
|
|
18
|
-
// Turns
|
|
19
|
-
// ---------------------------------------------------------------------------------------------
|
|
20
|
-
/**
|
|
21
|
-
* Collects turns as a chat shows them: each person's message, then one assistant message holding
|
|
22
|
-
* everything the agent said and ran until the next one.
|
|
23
|
-
*/
|
|
24
|
-
function createTurns() {
|
|
25
|
-
const messages = [];
|
|
26
|
-
let assistant = null;
|
|
27
|
-
let lastAt = null;
|
|
28
|
-
const flush = () => {
|
|
29
|
-
if (assistant !== null && assistant.parts.length > 0)
|
|
30
|
-
messages.push({
|
|
31
|
-
createdAt: assistant.createdAt,
|
|
32
|
-
role: 'assistant',
|
|
33
|
-
text: assistant.parts.join('\n\n'),
|
|
34
|
-
});
|
|
35
|
-
assistant = null;
|
|
36
|
-
};
|
|
37
|
-
return {
|
|
38
|
-
assistant(text, createdAt) {
|
|
39
|
-
if (text.trim() === '')
|
|
40
|
-
return;
|
|
41
|
-
assistant ??= { createdAt, parts: [] };
|
|
42
|
-
assistant.parts.push(text.trim());
|
|
43
|
-
lastAt = createdAt;
|
|
44
|
-
},
|
|
45
|
-
/** The turns, and when the last thing in them happened. */
|
|
46
|
-
done() {
|
|
47
|
-
flush();
|
|
48
|
-
return { lastAt, messages };
|
|
49
|
-
},
|
|
50
|
-
user(text, createdAt) {
|
|
51
|
-
if (text.trim() === '')
|
|
52
|
-
return;
|
|
53
|
-
flush();
|
|
54
|
-
messages.push({ createdAt, role: 'user', text: text.trim() });
|
|
55
|
-
lastAt = createdAt;
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function truncate(text, max) {
|
|
60
|
-
if (text.length <= max)
|
|
61
|
-
return text;
|
|
62
|
-
const marker = `\n… [${text.length - max} more characters left out]`;
|
|
63
|
-
return `${text.slice(0, Math.max(0, max - marker.length))}${marker}`;
|
|
64
|
-
}
|
|
65
|
-
/** Text in a fence no backtick run inside it can close. */
|
|
66
|
-
function fenced(text) {
|
|
67
|
-
const longest = Math.max(2, ...[...text.matchAll(/`+/gu)].map((run) => run[0].length));
|
|
68
|
-
const fence = '`'.repeat(longest + 1);
|
|
69
|
-
return `${fence}\n${text}\n${fence}`;
|
|
70
|
-
}
|
|
71
|
-
/** Redacted whole, then cut: a cut can leave a secret no pattern still matches. */
|
|
72
|
-
function toolBody(text) {
|
|
73
|
-
return fenced(truncate(redactSecrets(text).text, MAX_TOOL_CHARS));
|
|
74
|
-
}
|
|
75
|
-
function toolCallText(name, input) {
|
|
76
|
-
const body = typeof input === 'string' ? prettyJson(input) : JSON.stringify(input ?? null, null, 2);
|
|
77
|
-
return `**Tool call: ${name}**\n${toolBody(body)}`;
|
|
78
|
-
}
|
|
79
|
-
function toolResultText(output) {
|
|
80
|
-
return `**Tool result**\n${toolBody(output)}`;
|
|
81
|
-
}
|
|
82
|
-
/** A JSON string pretty-printed, or as it is when it isn't JSON. */
|
|
83
|
-
function prettyJson(text) {
|
|
84
|
-
try {
|
|
85
|
-
return JSON.stringify(JSON.parse(text), null, 2);
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return text;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function firstLine(text) {
|
|
92
|
-
const line = text.split('\n').find((each) => each.trim() !== '') ?? '';
|
|
93
|
-
return truncate(line.trim(), 80);
|
|
94
|
-
}
|
|
95
|
-
function isRecord(value) {
|
|
96
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
97
|
-
}
|
|
98
|
-
function stringField(value, key) {
|
|
99
|
-
const field = value[key];
|
|
100
|
-
return typeof field === 'string' && field !== '' ? field : null;
|
|
101
|
-
}
|
|
102
|
-
/** Each line that is a JSON object; a torn or foreign line is left out. */
|
|
103
|
-
function jsonLines(text) {
|
|
104
|
-
return text.split('\n').flatMap((line) => {
|
|
105
|
-
if (line.trim() === '')
|
|
106
|
-
return [];
|
|
107
|
-
try {
|
|
108
|
-
const value = JSON.parse(line);
|
|
109
|
-
return isRecord(value) ? [value] : [];
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
function validTime(value) {
|
|
117
|
-
if (value === null)
|
|
118
|
-
return null;
|
|
119
|
-
const time = new Date(value);
|
|
120
|
-
return Number.isNaN(time.getTime()) ? null : time.toISOString();
|
|
121
|
-
}
|
|
122
|
-
function finishChat(fields, { lastAt, messages }) {
|
|
123
|
-
const first = messages[0];
|
|
124
|
-
if (first === undefined || lastAt === null || !messages.some((m) => m.role === 'user'))
|
|
125
|
-
return null;
|
|
126
|
-
const firstUser = messages.find((message) => message.role === 'user');
|
|
127
|
-
return {
|
|
128
|
-
...fields,
|
|
129
|
-
messages,
|
|
130
|
-
startedAt: first.createdAt,
|
|
131
|
-
title: truncate(fields.title ?? firstLine(firstUser?.text ?? 'Imported chat'), 200),
|
|
132
|
-
updatedAt: lastAt,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
// ---------------------------------------------------------------------------------------------
|
|
136
|
-
// Claude Code
|
|
137
|
-
// ---------------------------------------------------------------------------------------------
|
|
138
|
-
/** What Claude Code writes as a `user` line but the person never typed: slash commands and their output. */
|
|
139
|
-
const CLAUDE_CODE_INJECTED = /^\s*<(command-name|command-message|local-command-[a-z]+)>/u;
|
|
140
|
-
/**
|
|
141
|
-
* A Claude Code session file: one Agent SDK message per line (`type` `user` or `assistant`, with
|
|
142
|
-
* Anthropic `message.content`), plus `summary` lines that title it. Sidechain (subagent) and meta
|
|
143
|
-
* lines are left out, as is thinking.
|
|
144
|
-
*/
|
|
145
|
-
export function parseClaudeCodeTranscript(text, file) {
|
|
146
|
-
const turns = createTurns();
|
|
147
|
-
let sessionId = null;
|
|
148
|
-
let cwd = null;
|
|
149
|
-
let gitBranch = null;
|
|
150
|
-
let title = null;
|
|
151
|
-
for (const line of jsonLines(text)) {
|
|
152
|
-
if (line.type === 'summary') {
|
|
153
|
-
title ??= stringField(line, 'summary');
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (line.type !== 'user' && line.type !== 'assistant')
|
|
157
|
-
continue;
|
|
158
|
-
if (line.isSidechain === true || line.isMeta === true)
|
|
159
|
-
continue;
|
|
160
|
-
const message = line.message;
|
|
161
|
-
const at = validTime(stringField(line, 'timestamp'));
|
|
162
|
-
if (!isRecord(message) || at === null)
|
|
163
|
-
continue;
|
|
164
|
-
sessionId ??= stringField(line, 'sessionId');
|
|
165
|
-
cwd ??= stringField(line, 'cwd');
|
|
166
|
-
gitBranch ??= stringField(line, 'gitBranch');
|
|
167
|
-
const content = message.content;
|
|
168
|
-
if (typeof content === 'string') {
|
|
169
|
-
if (line.type === 'assistant')
|
|
170
|
-
turns.assistant(content, at);
|
|
171
|
-
else if (!CLAUDE_CODE_INJECTED.test(content))
|
|
172
|
-
turns.user(content, at);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (!Array.isArray(content))
|
|
176
|
-
continue;
|
|
177
|
-
for (const block of content) {
|
|
178
|
-
if (!isRecord(block))
|
|
179
|
-
continue;
|
|
180
|
-
if (block.type === 'text' && typeof block.text === 'string') {
|
|
181
|
-
if (line.type === 'assistant')
|
|
182
|
-
turns.assistant(block.text, at);
|
|
183
|
-
else if (!CLAUDE_CODE_INJECTED.test(block.text))
|
|
184
|
-
turns.user(block.text, at);
|
|
185
|
-
}
|
|
186
|
-
else if (block.type === 'tool_use') {
|
|
187
|
-
turns.assistant(toolCallText(stringField(block, 'name') ?? 'tool', block.input), at);
|
|
188
|
-
}
|
|
189
|
-
else if (block.type === 'tool_result') {
|
|
190
|
-
turns.assistant(toolResultText(claudeToolResultText(block.content)), at);
|
|
191
|
-
}
|
|
192
|
-
else if (block.type === 'image' && line.type === 'user') {
|
|
193
|
-
turns.user('[An image was attached here; it was not imported.]', at);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const id = sessionId ?? path.basename(file, '.jsonl');
|
|
198
|
-
return finishChat({ cwd, gitBranch, id, path: file, source: 'claude-code', title }, turns.done());
|
|
199
|
-
}
|
|
200
|
-
function claudeToolResultText(content) {
|
|
201
|
-
if (typeof content === 'string')
|
|
202
|
-
return content;
|
|
203
|
-
if (!Array.isArray(content))
|
|
204
|
-
return '';
|
|
205
|
-
return content
|
|
206
|
-
.map((part) => isRecord(part) && typeof part.text === 'string'
|
|
207
|
-
? part.text
|
|
208
|
-
: isRecord(part) && part.type === 'image'
|
|
209
|
-
? '[image]'
|
|
210
|
-
: '')
|
|
211
|
-
.filter((part) => part !== '')
|
|
212
|
-
.join('\n');
|
|
213
|
-
}
|
|
214
|
-
// ---------------------------------------------------------------------------------------------
|
|
215
|
-
// Codex
|
|
216
|
-
// ---------------------------------------------------------------------------------------------
|
|
217
|
-
/** Context Codex sends as `user` messages that the person never typed. */
|
|
218
|
-
const CODEX_INJECTED = /^\s*(<environment_context>|<user_instructions>|<permissions instructions>|<turn_aborted>|# AGENTS\.md instructions)/u;
|
|
219
|
-
/**
|
|
220
|
-
* A Codex rollout: `{timestamp, type, payload}` lines, as `codex-rs/history` writes them. The
|
|
221
|
-
* `session_meta` line names the thread; `response_item` lines carry messages and tool calls.
|
|
222
|
-
* Developer and system messages, injected context and reasoning are left out.
|
|
223
|
-
*/
|
|
224
|
-
export function parseCodexRollout(text, file) {
|
|
225
|
-
const turns = createTurns();
|
|
226
|
-
let id = null;
|
|
227
|
-
let cwd = null;
|
|
228
|
-
let gitBranch = null;
|
|
229
|
-
for (const line of jsonLines(text)) {
|
|
230
|
-
const payload = line.payload;
|
|
231
|
-
if (!isRecord(payload))
|
|
232
|
-
continue;
|
|
233
|
-
if (line.type === 'session_meta') {
|
|
234
|
-
id ??= stringField(payload, 'id');
|
|
235
|
-
cwd ??= stringField(payload, 'cwd');
|
|
236
|
-
if (isRecord(payload.git))
|
|
237
|
-
gitBranch ??= stringField(payload.git, 'branch');
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
const at = validTime(stringField(line, 'timestamp'));
|
|
241
|
-
if (line.type !== 'response_item' || at === null)
|
|
242
|
-
continue;
|
|
243
|
-
switch (payload.type) {
|
|
244
|
-
case 'message': {
|
|
245
|
-
const said = codexContentText(payload.content);
|
|
246
|
-
if (payload.role === 'user' && !CODEX_INJECTED.test(said))
|
|
247
|
-
turns.user(said, at);
|
|
248
|
-
if (payload.role === 'assistant')
|
|
249
|
-
turns.assistant(said, at);
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
case 'function_call':
|
|
253
|
-
turns.assistant(toolCallText(stringField(payload, 'name') ?? 'tool', payload.arguments ?? ''), at);
|
|
254
|
-
break;
|
|
255
|
-
case 'custom_tool_call':
|
|
256
|
-
turns.assistant(toolCallText(stringField(payload, 'name') ?? 'tool', payload.input ?? ''), at);
|
|
257
|
-
break;
|
|
258
|
-
case 'local_shell_call': {
|
|
259
|
-
const action = isRecord(payload.action) ? payload.action : {};
|
|
260
|
-
const command = Array.isArray(action.command) ? action.command : [];
|
|
261
|
-
turns.assistant(toolCallText('shell', { command }), at);
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
case 'function_call_output':
|
|
265
|
-
case 'custom_tool_call_output':
|
|
266
|
-
turns.assistant(toolResultText(codexOutputText(payload.output)), at);
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return finishChat({
|
|
271
|
-
cwd,
|
|
272
|
-
gitBranch,
|
|
273
|
-
id: id ?? path.basename(file, '.jsonl'),
|
|
274
|
-
path: file,
|
|
275
|
-
source: 'codex',
|
|
276
|
-
title: null,
|
|
277
|
-
}, turns.done());
|
|
278
|
-
}
|
|
279
|
-
function codexContentText(content) {
|
|
280
|
-
if (!Array.isArray(content))
|
|
281
|
-
return '';
|
|
282
|
-
return content
|
|
283
|
-
.map((part) => (isRecord(part) && typeof part.text === 'string' ? part.text : ''))
|
|
284
|
-
.filter((part) => part !== '')
|
|
285
|
-
.join('\n');
|
|
286
|
-
}
|
|
287
|
-
function codexOutputText(output) {
|
|
288
|
-
if (typeof output === 'string')
|
|
289
|
-
return output;
|
|
290
|
-
if (isRecord(output) && typeof output.content === 'string')
|
|
291
|
-
return output.content;
|
|
292
|
-
return codexContentText(output);
|
|
293
|
-
}
|
|
294
|
-
// ---------------------------------------------------------------------------------------------
|
|
295
|
-
// Redaction and bounds
|
|
296
|
-
// ---------------------------------------------------------------------------------------------
|
|
297
|
-
/** Credential shapes, most specific first; each match becomes `[redacted:<kind>]`. */
|
|
298
|
-
const SECRET_PATTERNS = [
|
|
299
|
-
['private_key', /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/gu],
|
|
300
|
-
['secret_key', /\bsk-[A-Za-z0-9_-]{20,}/gu],
|
|
301
|
-
['github_token', /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{22,})/gu],
|
|
302
|
-
['aws_access_key', /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/gu],
|
|
303
|
-
['slack_token', /\bxox[abprs]-[A-Za-z0-9-]{10,}/gu],
|
|
304
|
-
['google_api_key', /\bAIza[0-9A-Za-z_-]{35}/gu],
|
|
305
|
-
['jwt', /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/gu],
|
|
306
|
-
['bearer_token', /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gu],
|
|
307
|
-
];
|
|
308
|
-
/** `password = value` and the like: the name stays, the value goes. */
|
|
309
|
-
const SECRET_ASSIGNMENT = /\b((?:[A-Za-z0-9_]*_)?(?:password|passwd|secret|token|api[_-]?key)["']?\s*[:=]\s*["']?)([^\s"'[]{8,})/giu;
|
|
310
|
-
const REDACTED_MARKER = /\[redacted:([a-z_]+)\]/gu;
|
|
311
|
-
export function redactSecrets(text) {
|
|
312
|
-
const counts = {};
|
|
313
|
-
const count = (kind) => {
|
|
314
|
-
counts[kind] = (counts[kind] ?? 0) + 1;
|
|
315
|
-
};
|
|
316
|
-
let redacted = text;
|
|
317
|
-
for (const [kind, pattern] of SECRET_PATTERNS)
|
|
318
|
-
redacted = redacted.replace(pattern, () => {
|
|
319
|
-
count(kind);
|
|
320
|
-
return `[redacted:${kind}]`;
|
|
321
|
-
});
|
|
322
|
-
redacted = redacted.replace(SECRET_ASSIGNMENT, (_match, name) => {
|
|
323
|
-
count('assignment');
|
|
324
|
-
return `${name}[redacted:assignment]`;
|
|
325
|
-
});
|
|
326
|
-
return { counts, text: redacted };
|
|
327
|
-
}
|
|
328
|
-
/** The chat as it is sent: redacted, each message within the size limit, or why it can't go. */
|
|
329
|
-
export function toImportInput(chat) {
|
|
330
|
-
if (!SOURCE_SESSION_ID.test(chat.id))
|
|
331
|
-
return { kind: 'skipped', reason: "Its session id isn't one Dexto takes." };
|
|
332
|
-
if (chat.messages.length > MAX_IMPORT_MESSAGES)
|
|
333
|
-
return { kind: 'skipped', reason: `It has more than ${MAX_IMPORT_MESSAGES} messages.` };
|
|
334
|
-
let truncated = 0;
|
|
335
|
-
let total = 0;
|
|
336
|
-
const messages = chat.messages.map((message) => {
|
|
337
|
-
const { text } = redactSecrets(message.text);
|
|
338
|
-
if (text.length > MAX_MESSAGE_CHARS)
|
|
339
|
-
truncated += 1;
|
|
340
|
-
const bounded = truncate(text, MAX_MESSAGE_CHARS);
|
|
341
|
-
total += bounded.length;
|
|
342
|
-
return { createdAt: message.createdAt, role: message.role, text: bounded };
|
|
343
|
-
});
|
|
344
|
-
if (total > MAX_IMPORT_CHARS)
|
|
345
|
-
return {
|
|
346
|
-
kind: 'skipped',
|
|
347
|
-
reason: `It has more than ${MAX_IMPORT_CHARS.toLocaleString('en-US')} characters.`,
|
|
348
|
-
};
|
|
349
|
-
const title = redactSecrets(chat.title).text.slice(0, MAX_TITLE_CHARS);
|
|
350
|
-
const cwd = chat.cwd === null ? null : redactSecrets(chat.cwd).text;
|
|
351
|
-
const gitBranch = chat.gitBranch === null ? null : redactSecrets(chat.gitBranch).text;
|
|
352
|
-
const input = {
|
|
353
|
-
...(cwd === null || cwd.length > MAX_CWD_CHARS ? {} : { cwd }),
|
|
354
|
-
...(gitBranch === null || gitBranch.length > MAX_BRANCH_CHARS ? {} : { gitBranch }),
|
|
355
|
-
messages,
|
|
356
|
-
source: chat.source,
|
|
357
|
-
sourceSessionId: chat.id,
|
|
358
|
-
title: title.trim() === '' ? 'Imported chat' : title,
|
|
359
|
-
};
|
|
360
|
-
return { input, kind: 'ready', redactions: redactionCounts(input), truncated };
|
|
361
|
-
}
|
|
362
|
-
/** Each `[redacted:<kind>]` in what is sent, including those made while reading tool output. */
|
|
363
|
-
function redactionCounts(input) {
|
|
364
|
-
const counts = {};
|
|
365
|
-
const sent = [input.title, input.cwd ?? '', input.gitBranch ?? ''];
|
|
366
|
-
for (const text of [...sent, ...input.messages.map((message) => message.text)])
|
|
367
|
-
for (const [, kind = ''] of text.matchAll(REDACTED_MARKER))
|
|
368
|
-
counts[kind] = (counts[kind] ?? 0) + 1;
|
|
369
|
-
return counts;
|
|
370
|
-
}
|
|
371
|
-
/** Where a source keeps its sessions on this machine. */
|
|
372
|
-
function localChatsFolder(runtime, source) {
|
|
373
|
-
if (source === 'claude-code')
|
|
374
|
-
return path.join(runtime.environment.CLAUDE_CONFIG_DIR ?? path.join(runtime.homeDirectory, '.claude'), 'projects');
|
|
375
|
-
return path.join(runtime.environment.CODEX_HOME ?? path.join(runtime.homeDirectory, '.codex'), 'sessions');
|
|
376
|
-
}
|
|
377
|
-
/** The source's sessions on this machine that match the query, newest first. */
|
|
378
|
-
export async function findLocalChats(runtime, query) {
|
|
379
|
-
if (query.dir !== null)
|
|
380
|
-
await readdir(query.dir).catch(() => {
|
|
381
|
-
throw new UsageError(`--dir ${query.dir} is not a folder this can read.`);
|
|
382
|
-
});
|
|
383
|
-
const folder = query.dir ?? localChatsFolder(runtime, query.source);
|
|
384
|
-
const since = query.since === null ? null : new Date(query.since);
|
|
385
|
-
const files = query.source === 'claude-code' ? await claudeCodeFiles(folder) : await codexFiles(folder);
|
|
386
|
-
const parse = query.source === 'claude-code' ? parseClaudeCodeTranscript : parseCodexRollout;
|
|
387
|
-
const within = query.folder === null ? null : path.resolve(query.folder);
|
|
388
|
-
const chats = [];
|
|
389
|
-
for (const file of files) {
|
|
390
|
-
const stats = await stat(file).catch(() => null);
|
|
391
|
-
if (stats === null || stats.size > MAX_FILE_BYTES)
|
|
392
|
-
continue;
|
|
393
|
-
const chat = parse(await readFile(file, 'utf8'), file);
|
|
394
|
-
if (chat === null)
|
|
395
|
-
continue;
|
|
396
|
-
if (since !== null && new Date(chat.updatedAt) < since)
|
|
397
|
-
continue;
|
|
398
|
-
if (within !== null && !insideFolder(chat.cwd, within))
|
|
399
|
-
continue;
|
|
400
|
-
chats.push(chat);
|
|
401
|
-
}
|
|
402
|
-
return chats.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
403
|
-
}
|
|
404
|
-
function insideFolder(cwd, folder) {
|
|
405
|
-
if (cwd === null)
|
|
406
|
-
return false;
|
|
407
|
-
const relative = path.relative(folder, cwd);
|
|
408
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
409
|
-
}
|
|
410
|
-
async function entries(folder) {
|
|
411
|
-
return await readdir(folder, { withFileTypes: true }).catch(() => []);
|
|
412
|
-
}
|
|
413
|
-
/** `projects/<project>/<session>.jsonl`; deeper files are a session's subagents. */
|
|
414
|
-
async function claudeCodeFiles(folder) {
|
|
415
|
-
const files = [];
|
|
416
|
-
for (const project of await entries(folder)) {
|
|
417
|
-
if (!project.isDirectory())
|
|
418
|
-
continue;
|
|
419
|
-
for (const file of await entries(path.join(folder, project.name)))
|
|
420
|
-
if (file.isFile() && file.name.endsWith('.jsonl'))
|
|
421
|
-
files.push(path.join(folder, project.name, file.name));
|
|
422
|
-
}
|
|
423
|
-
return files;
|
|
424
|
-
}
|
|
425
|
-
/** `sessions/YYYY/MM/DD/rollout-*.jsonl`; compressed rollouts are left out. */
|
|
426
|
-
async function codexFiles(folder) {
|
|
427
|
-
const files = [];
|
|
428
|
-
const visit = async (dir, depth) => {
|
|
429
|
-
for (const entry of await entries(dir)) {
|
|
430
|
-
const full = path.join(dir, entry.name);
|
|
431
|
-
if (entry.isDirectory() && depth < 3)
|
|
432
|
-
await visit(full, depth + 1);
|
|
433
|
-
else if (entry.isFile() &&
|
|
434
|
-
entry.name.startsWith('rollout-') &&
|
|
435
|
-
entry.name.endsWith('.jsonl'))
|
|
436
|
-
files.push(full);
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
await visit(folder, 0);
|
|
440
|
-
return files;
|
|
441
|
-
}
|