linkshell-cli 0.3.27 → 0.3.29
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/cli/src/commands/setup.js +20 -2
- package/dist/cli/src/commands/setup.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +9 -24
- package/dist/cli/src/commands/upgrade.js.map +1 -1
- package/dist/cli/src/index.js +28 -26
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/providers.d.ts +3 -7
- package/dist/cli/src/providers.js +76 -19
- package/dist/cli/src/providers.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -10
- package/dist/cli/src/runtime/acp/acp-client.js +19 -146
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +62 -0
- package/dist/cli/src/runtime/acp/agent-session.js +1075 -0
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +1 -24
- package/dist/cli/src/runtime/acp/agent-workspace.js +162 -981
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +1 -3
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +35 -81
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -2
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +35 -54
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +12 -1
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +1 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -34
- package/dist/cli/src/runtime/acp/json-rpc.js +8 -32
- package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -3
- package/dist/cli/src/runtime/acp/provider-resolver.js +6 -26
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +31 -1
- package/dist/cli/src/runtime/bridge-session.js +991 -81
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
- package/dist/cli/src/runtime/screen-fallback.js +4 -4
- package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
- package/dist/cli/src/runtime/screen-share.d.ts +1 -1
- package/dist/cli/src/runtime/screen-share.js +7 -7
- package/dist/cli/src/runtime/screen-share.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +11940 -3423
- package/dist/shared-protocol/src/index.js +98 -164
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/setup.ts +31 -5
- package/src/commands/upgrade.ts +8 -28
- package/src/index.ts +34 -29
- package/src/providers.ts +108 -26
- package/src/runtime/acp/acp-client.ts +21 -159
- package/src/runtime/acp/agent-session.ts +1180 -0
- package/src/runtime/acp/agent-workspace.ts +170 -1068
- package/src/runtime/acp/claude-sdk-client.ts +37 -85
- package/src/runtime/acp/claude-stream-json-client.ts +41 -59
- package/src/runtime/acp/json-rpc.ts +14 -46
- package/src/runtime/acp/provider-resolver.ts +6 -29
- package/src/runtime/bridge-session.ts +1092 -84
- package/src/runtime/screen-fallback.ts +5 -5
- package/src/runtime/screen-share.ts +8 -8
- package/src/runtime/acp/claude-sessions.ts +0 -563
- package/src/runtime/acp/codex-rpc-bridge.ts +0 -221
- package/src/runtime/acp/codex-sessions.ts +0 -802
- package/src/types/linkshell-gateway.d.ts +0 -18
|
@@ -1,802 +0,0 @@
|
|
|
1
|
-
import { closeSync, existsSync, openSync, readFileSync, readdirSync, readSync, statSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
const SAMPLE_BYTES = 512 * 1024;
|
|
6
|
-
const HISTORY_BYTES = 2 * 1024 * 1024;
|
|
7
|
-
const MAX_SESSIONS = 200;
|
|
8
|
-
const MAX_HISTORY_ITEMS = 200;
|
|
9
|
-
|
|
10
|
-
export interface CodexStoredSession {
|
|
11
|
-
id: string;
|
|
12
|
-
cwd: string;
|
|
13
|
-
title?: string;
|
|
14
|
-
createdAt?: number;
|
|
15
|
-
lastModified: number;
|
|
16
|
-
archived?: boolean;
|
|
17
|
-
status?: string;
|
|
18
|
-
runningTurnId?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface StoredAgentTimelineItem {
|
|
22
|
-
id: string;
|
|
23
|
-
conversationId: string;
|
|
24
|
-
type: "message" | "tool_call";
|
|
25
|
-
kind?: "tool_activity" | "command_execution" | "file_change";
|
|
26
|
-
itemId?: string;
|
|
27
|
-
role?: "user" | "assistant" | "system";
|
|
28
|
-
content?: Array<{ type: "text"; text: string }>;
|
|
29
|
-
text?: string;
|
|
30
|
-
toolCall?: {
|
|
31
|
-
id: string;
|
|
32
|
-
name: string;
|
|
33
|
-
input?: string;
|
|
34
|
-
output?: string;
|
|
35
|
-
createdAt?: number;
|
|
36
|
-
status: "pending" | "running" | "completed" | "failed";
|
|
37
|
-
};
|
|
38
|
-
commandExecution?: {
|
|
39
|
-
command?: string;
|
|
40
|
-
cwd?: string;
|
|
41
|
-
output?: string;
|
|
42
|
-
exitCode?: number | null;
|
|
43
|
-
status?: "pending" | "running" | "completed" | "failed";
|
|
44
|
-
};
|
|
45
|
-
fileChange?: {
|
|
46
|
-
entries: Array<{
|
|
47
|
-
path: string;
|
|
48
|
-
kind?: string;
|
|
49
|
-
added?: number;
|
|
50
|
-
removed?: number;
|
|
51
|
-
}>;
|
|
52
|
-
diff?: string;
|
|
53
|
-
summary?: string;
|
|
54
|
-
status?: "pending" | "running" | "completed" | "failed";
|
|
55
|
-
};
|
|
56
|
-
createdAt: number;
|
|
57
|
-
updatedAt?: number;
|
|
58
|
-
metadata?: Record<string, unknown>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface CodexIndexEntry {
|
|
62
|
-
id: string;
|
|
63
|
-
title?: string;
|
|
64
|
-
updatedAt?: number;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
type StoredToolStatus = NonNullable<StoredAgentTimelineItem["toolCall"]>["status"];
|
|
68
|
-
|
|
69
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
70
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
71
|
-
? value as Record<string, unknown>
|
|
72
|
-
: undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function parseTimestamp(value: unknown): number | undefined {
|
|
76
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
77
|
-
return value > 10_000_000_000 ? value : value * 1000;
|
|
78
|
-
}
|
|
79
|
-
if (typeof value === "string" && value.trim()) {
|
|
80
|
-
const parsed = Date.parse(value);
|
|
81
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
82
|
-
}
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function normalizeTitle(value: string | undefined): string | undefined {
|
|
87
|
-
const compact = value?.replace(/\s+/g, " ").trim();
|
|
88
|
-
if (!compact) return undefined;
|
|
89
|
-
return compact.length > 80 ? `${compact.slice(0, 77)}...` : compact;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function sessionIdFromRolloutFile(fileName: string): string | undefined {
|
|
93
|
-
const withoutExt = basename(fileName, ".jsonl");
|
|
94
|
-
const match = withoutExt.match(/(019[a-z0-9-]+)$/i);
|
|
95
|
-
return match?.[1] ?? undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function readSample(filePath: string, size: number): string {
|
|
99
|
-
let fd: number | undefined;
|
|
100
|
-
try {
|
|
101
|
-
fd = openSync(filePath, "r");
|
|
102
|
-
if (size <= SAMPLE_BYTES * 2) {
|
|
103
|
-
const buffer = Buffer.alloc(size);
|
|
104
|
-
const bytesRead = readSync(fd, buffer, 0, size, 0);
|
|
105
|
-
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const head = Buffer.alloc(SAMPLE_BYTES);
|
|
109
|
-
const tail = Buffer.alloc(SAMPLE_BYTES);
|
|
110
|
-
const headBytes = readSync(fd, head, 0, SAMPLE_BYTES, 0);
|
|
111
|
-
const tailBytes = readSync(fd, tail, 0, SAMPLE_BYTES, Math.max(0, size - SAMPLE_BYTES));
|
|
112
|
-
return `${head.subarray(0, headBytes).toString("utf8")}\n${tail.subarray(0, tailBytes).toString("utf8")}`;
|
|
113
|
-
} catch {
|
|
114
|
-
return "";
|
|
115
|
-
} finally {
|
|
116
|
-
if (typeof fd === "number") {
|
|
117
|
-
try {
|
|
118
|
-
closeSync(fd);
|
|
119
|
-
} catch {
|
|
120
|
-
// Best-effort local history scan.
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function readHistorySample(filePath: string, size: number): string {
|
|
127
|
-
try {
|
|
128
|
-
if (size <= HISTORY_BYTES) return readFileSync(filePath, "utf8");
|
|
129
|
-
} catch {
|
|
130
|
-
return "";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let fd: number | undefined;
|
|
134
|
-
try {
|
|
135
|
-
fd = openSync(filePath, "r");
|
|
136
|
-
const buffer = Buffer.alloc(HISTORY_BYTES);
|
|
137
|
-
const bytesRead = readSync(fd, buffer, 0, HISTORY_BYTES, Math.max(0, size - HISTORY_BYTES));
|
|
138
|
-
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
139
|
-
} catch {
|
|
140
|
-
return "";
|
|
141
|
-
} finally {
|
|
142
|
-
if (typeof fd === "number") {
|
|
143
|
-
try {
|
|
144
|
-
closeSync(fd);
|
|
145
|
-
} catch {
|
|
146
|
-
// Best-effort local history scan.
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function loadSessionIndex(root: string): Map<string, CodexIndexEntry> {
|
|
153
|
-
const indexPath = join(root, "session_index.jsonl");
|
|
154
|
-
const entries = new Map<string, CodexIndexEntry>();
|
|
155
|
-
if (!existsSync(indexPath)) return entries;
|
|
156
|
-
|
|
157
|
-
let statSize = 0;
|
|
158
|
-
try {
|
|
159
|
-
statSize = statSync(indexPath).size;
|
|
160
|
-
} catch {
|
|
161
|
-
return entries;
|
|
162
|
-
}
|
|
163
|
-
for (const line of readSample(indexPath, statSize).split(/\r?\n/)) {
|
|
164
|
-
const trimmed = line.trim();
|
|
165
|
-
if (!trimmed.startsWith("{")) continue;
|
|
166
|
-
try {
|
|
167
|
-
const entry = asRecord(JSON.parse(trimmed));
|
|
168
|
-
if (!entry) continue;
|
|
169
|
-
const id = typeof entry.id === "string" ? entry.id : undefined;
|
|
170
|
-
if (!id) continue;
|
|
171
|
-
entries.set(id, {
|
|
172
|
-
id,
|
|
173
|
-
title: normalizeTitle(
|
|
174
|
-
typeof entry.thread_name === "string"
|
|
175
|
-
? entry.thread_name
|
|
176
|
-
: typeof entry.title === "string"
|
|
177
|
-
? entry.title
|
|
178
|
-
: undefined,
|
|
179
|
-
),
|
|
180
|
-
updatedAt: parseTimestamp(entry.updated_at ?? entry.updatedAt),
|
|
181
|
-
});
|
|
182
|
-
} catch {
|
|
183
|
-
// Ignore malformed index lines.
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return entries;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function readCodexSessionFile(filePath: string, fallbackCwd: string): Omit<CodexStoredSession, "lastModified"> & {
|
|
190
|
-
lastModified?: number;
|
|
191
|
-
} | undefined {
|
|
192
|
-
let statSize = 0;
|
|
193
|
-
let statMtime: number | undefined;
|
|
194
|
-
try {
|
|
195
|
-
const stat = statSync(filePath);
|
|
196
|
-
statSize = stat.size;
|
|
197
|
-
statMtime = stat.mtimeMs;
|
|
198
|
-
} catch {
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
let id = sessionIdFromRolloutFile(filePath);
|
|
203
|
-
let cwd: string | undefined;
|
|
204
|
-
let title: string | undefined;
|
|
205
|
-
let createdAt: number | undefined;
|
|
206
|
-
let lastActivityAt: number | undefined;
|
|
207
|
-
for (const line of readSample(filePath, statSize).split(/\r?\n/)) {
|
|
208
|
-
const trimmed = line.trim();
|
|
209
|
-
if (!trimmed.startsWith("{")) continue;
|
|
210
|
-
try {
|
|
211
|
-
const entry = asRecord(JSON.parse(trimmed));
|
|
212
|
-
const payload = asRecord(entry?.payload);
|
|
213
|
-
if (entry?.type === "session_meta" && payload) {
|
|
214
|
-
if (typeof payload.id === "string") id = payload.id;
|
|
215
|
-
if (typeof payload.cwd === "string" && payload.cwd.trim()) cwd = payload.cwd;
|
|
216
|
-
createdAt ??= parseTimestamp(payload.timestamp);
|
|
217
|
-
}
|
|
218
|
-
if (entry?.type === "event_msg" && payload?.type === "user_message") {
|
|
219
|
-
title ??= normalizeTitle(normalizeHistoryText(payload.message));
|
|
220
|
-
}
|
|
221
|
-
const timestamp = parseTimestamp(entry?.timestamp);
|
|
222
|
-
createdAt ??= timestamp;
|
|
223
|
-
if (timestamp) lastActivityAt = timestamp;
|
|
224
|
-
} catch {
|
|
225
|
-
// The sample may contain partial JSONL lines.
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (!id) return undefined;
|
|
230
|
-
return {
|
|
231
|
-
id,
|
|
232
|
-
cwd: cwd ?? resolve(fallbackCwd),
|
|
233
|
-
title,
|
|
234
|
-
createdAt,
|
|
235
|
-
lastModified: lastActivityAt ?? statMtime,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function collectJsonlFiles(dir: string, result: Array<{ path: string; archived: boolean }>, archived: boolean): void {
|
|
240
|
-
if (!existsSync(dir)) return;
|
|
241
|
-
try {
|
|
242
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
243
|
-
const path = join(dir, entry.name);
|
|
244
|
-
if (entry.isDirectory()) {
|
|
245
|
-
collectJsonlFiles(path, result, archived);
|
|
246
|
-
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
247
|
-
result.push({ path, archived });
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
} catch {
|
|
251
|
-
// Ignore unreadable local history directories.
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function findCodexSessionFile(sessionId: string, inputCwd: string): { path: string; archived: boolean } | undefined {
|
|
256
|
-
const root = join(homedir(), ".codex");
|
|
257
|
-
if (!existsSync(root)) return undefined;
|
|
258
|
-
const files: Array<{ path: string; archived: boolean }> = [];
|
|
259
|
-
collectJsonlFiles(join(root, "sessions"), files, false);
|
|
260
|
-
collectJsonlFiles(join(root, "archived_sessions"), files, true);
|
|
261
|
-
|
|
262
|
-
let best: { path: string; archived: boolean; lastModified: number } | undefined;
|
|
263
|
-
for (const file of files) {
|
|
264
|
-
const fileId = sessionIdFromRolloutFile(file.path);
|
|
265
|
-
let metadata: ReturnType<typeof readCodexSessionFile> | undefined;
|
|
266
|
-
if (fileId !== sessionId) {
|
|
267
|
-
metadata = readCodexSessionFile(file.path, inputCwd);
|
|
268
|
-
if (metadata?.id !== sessionId) continue;
|
|
269
|
-
}
|
|
270
|
-
let lastModified = metadata?.lastModified;
|
|
271
|
-
if (!lastModified) {
|
|
272
|
-
try {
|
|
273
|
-
lastModified = statSync(file.path).mtimeMs;
|
|
274
|
-
} catch {
|
|
275
|
-
lastModified = 0;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (!best || file.archived || lastModified > best.lastModified) {
|
|
279
|
-
best = { ...file, lastModified };
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return best;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function normalizeHistoryText(value: unknown): string | undefined {
|
|
286
|
-
if (typeof value === "string") {
|
|
287
|
-
const text = value.replace(/\r\n/g, "\n").trim();
|
|
288
|
-
return text || undefined;
|
|
289
|
-
}
|
|
290
|
-
if (Array.isArray(value)) {
|
|
291
|
-
const text = value
|
|
292
|
-
.map((part) => {
|
|
293
|
-
if (typeof part === "string") return part;
|
|
294
|
-
const record = asRecord(part);
|
|
295
|
-
return typeof record?.text === "string"
|
|
296
|
-
? record.text
|
|
297
|
-
: typeof record?.content === "string"
|
|
298
|
-
? record.content
|
|
299
|
-
: "";
|
|
300
|
-
})
|
|
301
|
-
.filter(Boolean)
|
|
302
|
-
.join("\n")
|
|
303
|
-
.trim();
|
|
304
|
-
return text || undefined;
|
|
305
|
-
}
|
|
306
|
-
const record = asRecord(value);
|
|
307
|
-
if (!record) return undefined;
|
|
308
|
-
return normalizeHistoryText(record.content ?? record.text ?? record.message);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function stringifyHistoryValue(value: unknown): string | undefined {
|
|
312
|
-
if (value === undefined || value === null || value === "") return undefined;
|
|
313
|
-
if (typeof value === "string") return value;
|
|
314
|
-
try {
|
|
315
|
-
return JSON.stringify(value, null, 2);
|
|
316
|
-
} catch {
|
|
317
|
-
return String(value);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function visibleCodexRole(value: unknown): "user" | "assistant" | undefined {
|
|
322
|
-
return value === "user" || value === "assistant" ? value : undefined;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function isInjectedCodexContext(text: string): boolean {
|
|
326
|
-
const trimmed = text.trimStart();
|
|
327
|
-
return (
|
|
328
|
-
trimmed.startsWith("<permissions instructions>") ||
|
|
329
|
-
trimmed.startsWith("<app-context>") ||
|
|
330
|
-
trimmed.startsWith("<environment_context>") ||
|
|
331
|
-
trimmed.startsWith("<skill>") ||
|
|
332
|
-
trimmed.startsWith("<turn_aborted>") ||
|
|
333
|
-
trimmed.startsWith("<developer") ||
|
|
334
|
-
trimmed.startsWith("# AGENTS.md instructions") ||
|
|
335
|
-
trimmed.startsWith("AGENTS.md instructions for ") ||
|
|
336
|
-
trimmed.includes("\n# AGENTS.md\n") ||
|
|
337
|
-
trimmed.includes("\n<INSTRUCTIONS>")
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function messageDedupeKey(role: "user" | "assistant" | "system", text: string): string {
|
|
342
|
-
return `${role}:${text.replace(/\s+/g, " ").trim().slice(0, 400)}`;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function historyMessage(
|
|
346
|
-
conversationId: string,
|
|
347
|
-
index: number,
|
|
348
|
-
role: "user" | "assistant" | "system",
|
|
349
|
-
text: string,
|
|
350
|
-
createdAt: number,
|
|
351
|
-
source: string,
|
|
352
|
-
): StoredAgentTimelineItem {
|
|
353
|
-
const id = `history:${source}:${index}`;
|
|
354
|
-
return {
|
|
355
|
-
id,
|
|
356
|
-
conversationId,
|
|
357
|
-
type: "message",
|
|
358
|
-
role,
|
|
359
|
-
content: [{ type: "text", text }],
|
|
360
|
-
text,
|
|
361
|
-
createdAt,
|
|
362
|
-
metadata: { source: "device-history", provider: "codex" },
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function historyToolName(name: string | undefined): string {
|
|
367
|
-
if (!name) return "工具";
|
|
368
|
-
if (name.endsWith("exec_command") || name.endsWith("write_stdin")) return "命令";
|
|
369
|
-
if (name === "apply_patch") return "文件修改";
|
|
370
|
-
return name;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function historyToolKind(name: string | undefined): "tool_activity" | "command_execution" | "file_change" {
|
|
374
|
-
if (name === "apply_patch") return "file_change";
|
|
375
|
-
return name?.endsWith("exec_command") || name?.endsWith("write_stdin") ? "command_execution" : "tool_activity";
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function commandFromCodexTool(name: string | undefined, rawInput: unknown, output?: string): StoredAgentTimelineItem["commandExecution"] {
|
|
379
|
-
if (!name?.endsWith("exec_command") && !name?.endsWith("write_stdin")) return undefined;
|
|
380
|
-
let input = asRecord(rawInput);
|
|
381
|
-
if (!input && typeof rawInput === "string" && rawInput.trim().startsWith("{")) {
|
|
382
|
-
try {
|
|
383
|
-
input = asRecord(JSON.parse(rawInput));
|
|
384
|
-
} catch {
|
|
385
|
-
input = undefined;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const command = typeof input?.cmd === "string"
|
|
389
|
-
? input.cmd
|
|
390
|
-
: typeof input?.chars === "string"
|
|
391
|
-
? input.chars
|
|
392
|
-
: undefined;
|
|
393
|
-
const cwd = typeof input?.workdir === "string" ? input.workdir : undefined;
|
|
394
|
-
if (!command && !cwd && !output) return undefined;
|
|
395
|
-
return { command, cwd, output, status: output === undefined ? "running" : "completed" };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function patchTextFromCodexTool(name: string | undefined, rawInput: unknown, input: string | undefined): string | undefined {
|
|
399
|
-
if (name !== "apply_patch") return undefined;
|
|
400
|
-
if (typeof rawInput === "string") return rawInput;
|
|
401
|
-
const record = asRecord(rawInput);
|
|
402
|
-
for (const key of ["patch", "input", "text", "content"]) {
|
|
403
|
-
const value = record?.[key];
|
|
404
|
-
if (typeof value === "string" && value.trim()) return value;
|
|
405
|
-
}
|
|
406
|
-
return input;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function fileChangeFromApplyPatch(
|
|
410
|
-
patchText: string | undefined,
|
|
411
|
-
status: StoredToolStatus,
|
|
412
|
-
): StoredAgentTimelineItem["fileChange"] | undefined {
|
|
413
|
-
if (!patchText?.trim()) return undefined;
|
|
414
|
-
const entries: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"] = [];
|
|
415
|
-
let current: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] | undefined;
|
|
416
|
-
|
|
417
|
-
const flush = () => {
|
|
418
|
-
if (!current?.path) return;
|
|
419
|
-
const existing = entries.find((entry) => entry.path === current!.path);
|
|
420
|
-
if (existing) {
|
|
421
|
-
existing.added = (existing.added ?? 0) + (current.added ?? 0);
|
|
422
|
-
existing.removed = (existing.removed ?? 0) + (current.removed ?? 0);
|
|
423
|
-
existing.kind ??= current.kind;
|
|
424
|
-
} else {
|
|
425
|
-
entries.push(current);
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
for (const rawLine of patchText.split(/\r?\n/)) {
|
|
430
|
-
const add = rawLine.match(/^\*\*\* Add File:\s+(.+)$/);
|
|
431
|
-
const update = rawLine.match(/^\*\*\* Update File:\s+(.+)$/);
|
|
432
|
-
const del = rawLine.match(/^\*\*\* Delete File:\s+(.+)$/);
|
|
433
|
-
const move = rawLine.match(/^\*\*\* Move to:\s+(.+)$/);
|
|
434
|
-
if (add || update || del) {
|
|
435
|
-
flush();
|
|
436
|
-
current = {
|
|
437
|
-
path: (add?.[1] ?? update?.[1] ?? del?.[1] ?? "").trim(),
|
|
438
|
-
kind: add ? "create" : del ? "delete" : "update",
|
|
439
|
-
added: 0,
|
|
440
|
-
removed: 0,
|
|
441
|
-
};
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
if (move?.[1] && current) {
|
|
445
|
-
current.path = move[1].trim();
|
|
446
|
-
current.kind = "move";
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
if (!current) continue;
|
|
450
|
-
if (rawLine.startsWith("+") && !rawLine.startsWith("+++")) {
|
|
451
|
-
current.added = (current.added ?? 0) + 1;
|
|
452
|
-
} else if (rawLine.startsWith("-") && !rawLine.startsWith("---")) {
|
|
453
|
-
current.removed = (current.removed ?? 0) + 1;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
flush();
|
|
457
|
-
|
|
458
|
-
if (entries.length === 0) return undefined;
|
|
459
|
-
return {
|
|
460
|
-
entries,
|
|
461
|
-
diff: patchText,
|
|
462
|
-
summary: entries.map((entry) => [entry.kind, entry.path].filter(Boolean).join(" ")).join("\n"),
|
|
463
|
-
status,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function upsertHistoryTool(
|
|
468
|
-
itemsById: Map<string, StoredAgentTimelineItem>,
|
|
469
|
-
conversationId: string,
|
|
470
|
-
callId: string,
|
|
471
|
-
name: string | undefined,
|
|
472
|
-
rawInput: unknown,
|
|
473
|
-
input: string | undefined,
|
|
474
|
-
createdAt: number,
|
|
475
|
-
): void {
|
|
476
|
-
const id = `history-tool:${callId}`;
|
|
477
|
-
const existing = itemsById.get(id);
|
|
478
|
-
const commandExecution = commandFromCodexTool(name, rawInput, existing?.commandExecution?.output);
|
|
479
|
-
const status: StoredToolStatus = existing?.toolCall?.status ?? "running";
|
|
480
|
-
const fileChange = fileChangeFromApplyPatch(patchTextFromCodexTool(name, rawInput, input), status);
|
|
481
|
-
itemsById.set(id, {
|
|
482
|
-
id,
|
|
483
|
-
conversationId,
|
|
484
|
-
type: "tool_call",
|
|
485
|
-
kind: fileChange ? "file_change" : historyToolKind(name),
|
|
486
|
-
itemId: callId,
|
|
487
|
-
toolCall: {
|
|
488
|
-
id: callId,
|
|
489
|
-
name: fileChange ? "文件修改" : historyToolName(name),
|
|
490
|
-
input: fileChange?.summary ?? input ?? existing?.toolCall?.input,
|
|
491
|
-
output: existing?.toolCall?.output,
|
|
492
|
-
createdAt: existing?.toolCall?.createdAt ?? createdAt,
|
|
493
|
-
status,
|
|
494
|
-
},
|
|
495
|
-
commandExecution: commandExecution ?? existing?.commandExecution,
|
|
496
|
-
fileChange: fileChange ?? existing?.fileChange,
|
|
497
|
-
text: fileChange
|
|
498
|
-
? `已编辑 ${fileChange.entries.length} 个文件`
|
|
499
|
-
: existing?.text,
|
|
500
|
-
createdAt: existing?.createdAt ?? createdAt,
|
|
501
|
-
updatedAt: createdAt,
|
|
502
|
-
metadata: { source: "device-history", provider: "codex" },
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function completeHistoryTool(
|
|
507
|
-
itemsById: Map<string, StoredAgentTimelineItem>,
|
|
508
|
-
conversationId: string,
|
|
509
|
-
callId: string,
|
|
510
|
-
output: string | undefined,
|
|
511
|
-
createdAt: number,
|
|
512
|
-
): void {
|
|
513
|
-
const id = `history-tool:${callId}`;
|
|
514
|
-
const existing = itemsById.get(id);
|
|
515
|
-
const commandExecution = existing?.commandExecution
|
|
516
|
-
? { ...existing.commandExecution, output, status: "completed" as const }
|
|
517
|
-
: undefined;
|
|
518
|
-
const fileChange = existing?.fileChange
|
|
519
|
-
? { ...existing.fileChange, summary: existing.fileChange.summary ?? output, status: "completed" as const }
|
|
520
|
-
: undefined;
|
|
521
|
-
itemsById.set(id, {
|
|
522
|
-
id,
|
|
523
|
-
conversationId,
|
|
524
|
-
type: "tool_call",
|
|
525
|
-
kind: existing?.kind ?? "tool_activity",
|
|
526
|
-
itemId: callId,
|
|
527
|
-
toolCall: {
|
|
528
|
-
id: callId,
|
|
529
|
-
name: existing?.toolCall?.name ?? "工具",
|
|
530
|
-
input: existing?.toolCall?.input,
|
|
531
|
-
output,
|
|
532
|
-
createdAt: existing?.toolCall?.createdAt ?? createdAt,
|
|
533
|
-
status: "completed",
|
|
534
|
-
},
|
|
535
|
-
commandExecution,
|
|
536
|
-
fileChange,
|
|
537
|
-
text: fileChange ? existing?.text ?? `已编辑 ${fileChange.entries.length} 个文件` : existing?.text,
|
|
538
|
-
createdAt: existing?.createdAt ?? createdAt,
|
|
539
|
-
updatedAt: createdAt,
|
|
540
|
-
metadata: { source: "device-history", provider: "codex" },
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function relativeHistoryPath(path: string, cwd: string): string {
|
|
545
|
-
const normalized = path.replace(/\\/g, "/").replace(/^["']|["']$/g, "");
|
|
546
|
-
const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
547
|
-
return normalized.startsWith(`${normalizedCwd}/`)
|
|
548
|
-
? normalized.slice(normalizedCwd.length + 1)
|
|
549
|
-
: normalized;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function countDiffLines(diff: string | undefined): { added: number; removed: number } {
|
|
553
|
-
if (!diff) return { added: 0, removed: 0 };
|
|
554
|
-
let added = 0;
|
|
555
|
-
let removed = 0;
|
|
556
|
-
for (const line of diff.split("\n")) {
|
|
557
|
-
if (line.startsWith("+") && !line.startsWith("+++")) added += 1;
|
|
558
|
-
else if (line.startsWith("-") && !line.startsWith("---")) removed += 1;
|
|
559
|
-
}
|
|
560
|
-
return { added, removed };
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function countContentLines(content: unknown): number {
|
|
564
|
-
if (typeof content !== "string") return 0;
|
|
565
|
-
if (!content) return 0;
|
|
566
|
-
return content.split(/\r?\n/).filter((line) => line.length > 0).length;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function changeKind(value: string | undefined): string | undefined {
|
|
570
|
-
if (value === "add") return "create";
|
|
571
|
-
if (value === "delete") return "delete";
|
|
572
|
-
if (value === "update" || value === "move") return "update";
|
|
573
|
-
return value;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function patchApplyFileChange(
|
|
577
|
-
conversationId: string,
|
|
578
|
-
payload: Record<string, unknown>,
|
|
579
|
-
cwd: string,
|
|
580
|
-
createdAt: number,
|
|
581
|
-
): StoredAgentTimelineItem | undefined {
|
|
582
|
-
const changes = asRecord(payload.changes);
|
|
583
|
-
if (!changes) return undefined;
|
|
584
|
-
const entries: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"] = [];
|
|
585
|
-
const diffParts: string[] = [];
|
|
586
|
-
for (const [absolutePath, rawChange] of Object.entries(changes)) {
|
|
587
|
-
const change = asRecord(rawChange);
|
|
588
|
-
if (!change) continue;
|
|
589
|
-
const path = relativeHistoryPath(absolutePath, cwd);
|
|
590
|
-
const kind = changeKind(typeof change.type === "string" ? change.type : undefined);
|
|
591
|
-
const diff = typeof change.unified_diff === "string" ? change.unified_diff : undefined;
|
|
592
|
-
const stats = countDiffLines(diff);
|
|
593
|
-
const added = stats.added || (kind === "create" ? countContentLines(change.content) : 0);
|
|
594
|
-
const removed = stats.removed;
|
|
595
|
-
const entry: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] = { path };
|
|
596
|
-
if (kind) entry.kind = kind;
|
|
597
|
-
if (added > 0) entry.added = added;
|
|
598
|
-
if (removed > 0) entry.removed = removed;
|
|
599
|
-
entries.push(entry);
|
|
600
|
-
if (diff) {
|
|
601
|
-
diffParts.push([
|
|
602
|
-
`Path: ${path}`,
|
|
603
|
-
kind ? `Kind: ${kind}` : undefined,
|
|
604
|
-
`Totals: +${added} -${removed}`,
|
|
605
|
-
"",
|
|
606
|
-
"```diff",
|
|
607
|
-
diff.trimEnd(),
|
|
608
|
-
"```",
|
|
609
|
-
].filter((line): line is string => line !== undefined).join("\n"));
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
if (entries.length === 0) return undefined;
|
|
613
|
-
const callId = typeof payload.call_id === "string" ? payload.call_id : `patch-${createdAt}`;
|
|
614
|
-
const status = payload.success === false ? "failed" : "completed";
|
|
615
|
-
const totalAdded = entries.reduce((sum, entry) => sum + (entry.added ?? 0), 0);
|
|
616
|
-
const totalRemoved = entries.reduce((sum, entry) => sum + (entry.removed ?? 0), 0);
|
|
617
|
-
const summary = entries
|
|
618
|
-
.map((entry) => [entry.kind, entry.path].filter(Boolean).join(" "))
|
|
619
|
-
.join("\n");
|
|
620
|
-
const diff = diffParts.length > 0 ? diffParts.join("\n\n---\n\n") : undefined;
|
|
621
|
-
return {
|
|
622
|
-
id: `history-file-change:${callId}`,
|
|
623
|
-
conversationId,
|
|
624
|
-
type: "tool_call",
|
|
625
|
-
kind: "file_change",
|
|
626
|
-
itemId: callId,
|
|
627
|
-
toolCall: {
|
|
628
|
-
id: callId,
|
|
629
|
-
name: "文件修改",
|
|
630
|
-
input: summary,
|
|
631
|
-
output: diff ?? (typeof payload.stdout === "string" ? payload.stdout : undefined),
|
|
632
|
-
createdAt,
|
|
633
|
-
status,
|
|
634
|
-
},
|
|
635
|
-
fileChange: {
|
|
636
|
-
entries,
|
|
637
|
-
diff,
|
|
638
|
-
summary,
|
|
639
|
-
status,
|
|
640
|
-
},
|
|
641
|
-
text: `已编辑 ${entries.length} 个文件 +${totalAdded} -${totalRemoved}`,
|
|
642
|
-
createdAt,
|
|
643
|
-
updatedAt: createdAt,
|
|
644
|
-
metadata: { source: "device-history", provider: "codex" },
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
export function listCodexStoredSessions(inputCwd: string): { sessions: CodexStoredSession[] } {
|
|
649
|
-
const root = join(homedir(), ".codex");
|
|
650
|
-
if (!existsSync(root)) return { sessions: [] };
|
|
651
|
-
|
|
652
|
-
const index = loadSessionIndex(root);
|
|
653
|
-
const files: Array<{ path: string; archived: boolean }> = [];
|
|
654
|
-
collectJsonlFiles(join(root, "sessions"), files, false);
|
|
655
|
-
collectJsonlFiles(join(root, "archived_sessions"), files, true);
|
|
656
|
-
|
|
657
|
-
const sessionsById = new Map<string, CodexStoredSession>();
|
|
658
|
-
for (const file of files) {
|
|
659
|
-
const metadata = readCodexSessionFile(file.path, inputCwd);
|
|
660
|
-
if (!metadata) continue;
|
|
661
|
-
const indexed = index.get(metadata.id);
|
|
662
|
-
const session: CodexStoredSession = {
|
|
663
|
-
id: metadata.id,
|
|
664
|
-
cwd: metadata.cwd,
|
|
665
|
-
title: indexed?.title ?? metadata.title,
|
|
666
|
-
createdAt: metadata.createdAt,
|
|
667
|
-
lastModified: indexed?.updatedAt ?? metadata.lastModified ?? Date.now(),
|
|
668
|
-
archived: file.archived,
|
|
669
|
-
};
|
|
670
|
-
const existing = sessionsById.get(session.id);
|
|
671
|
-
if (!existing || session.lastModified > existing.lastModified || session.archived) {
|
|
672
|
-
sessionsById.set(session.id, session);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
for (const indexed of index.values()) {
|
|
677
|
-
if (sessionsById.has(indexed.id)) continue;
|
|
678
|
-
sessionsById.set(indexed.id, {
|
|
679
|
-
id: indexed.id,
|
|
680
|
-
cwd: resolve(inputCwd),
|
|
681
|
-
title: indexed.title,
|
|
682
|
-
lastModified: indexed.updatedAt ?? Date.now(),
|
|
683
|
-
archived: false,
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
return {
|
|
688
|
-
sessions: [...sessionsById.values()]
|
|
689
|
-
.sort((a, b) => b.lastModified - a.lastModified)
|
|
690
|
-
.slice(0, MAX_SESSIONS),
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
export function loadCodexStoredTimeline(
|
|
695
|
-
sessionId: string,
|
|
696
|
-
conversationId: string,
|
|
697
|
-
inputCwd: string,
|
|
698
|
-
options: { maxItems?: number } = {},
|
|
699
|
-
): { items: StoredAgentTimelineItem[] } {
|
|
700
|
-
const file = findCodexSessionFile(sessionId, inputCwd);
|
|
701
|
-
if (!file) return { items: [] };
|
|
702
|
-
let statMtime = Date.now();
|
|
703
|
-
let statSize = 0;
|
|
704
|
-
try {
|
|
705
|
-
const stat = statSync(file.path);
|
|
706
|
-
statMtime = stat.mtimeMs;
|
|
707
|
-
statSize = stat.size;
|
|
708
|
-
} catch {
|
|
709
|
-
return { items: [] };
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const itemsById = new Map<string, StoredAgentTimelineItem>();
|
|
713
|
-
const seenMessages = new Set<string>();
|
|
714
|
-
let index = 0;
|
|
715
|
-
for (const line of readHistorySample(file.path, statSize).split(/\r?\n/)) {
|
|
716
|
-
const trimmed = line.trim();
|
|
717
|
-
if (!trimmed.startsWith("{")) continue;
|
|
718
|
-
try {
|
|
719
|
-
const entry = asRecord(JSON.parse(trimmed));
|
|
720
|
-
const payload = asRecord(entry?.payload);
|
|
721
|
-
const createdAt =
|
|
722
|
-
parseTimestamp(entry?.timestamp ?? payload?.created_at ?? payload?.started_at ?? payload?.completed_at) ??
|
|
723
|
-
statMtime + index;
|
|
724
|
-
if (entry?.type === "event_msg" && payload) {
|
|
725
|
-
const eventType = typeof payload.type === "string" ? payload.type : undefined;
|
|
726
|
-
if (eventType === "user_message") {
|
|
727
|
-
const text = normalizeHistoryText(payload.message);
|
|
728
|
-
if (!text || isInjectedCodexContext(text)) continue;
|
|
729
|
-
const dedupeKey = messageDedupeKey("user", text);
|
|
730
|
-
if (seenMessages.has(dedupeKey)) continue;
|
|
731
|
-
seenMessages.add(dedupeKey);
|
|
732
|
-
itemsById.set(`history:${sessionId}:${index}`, historyMessage(conversationId, index, "user", text, createdAt, sessionId));
|
|
733
|
-
index += 1;
|
|
734
|
-
} else if (eventType === "agent_message") {
|
|
735
|
-
const text = normalizeHistoryText(payload.message);
|
|
736
|
-
if (!text || isInjectedCodexContext(text)) continue;
|
|
737
|
-
const dedupeKey = messageDedupeKey("assistant", text);
|
|
738
|
-
if (seenMessages.has(dedupeKey)) continue;
|
|
739
|
-
seenMessages.add(dedupeKey);
|
|
740
|
-
itemsById.set(`history:${sessionId}:${index}`, historyMessage(conversationId, index, "assistant", text, createdAt, sessionId));
|
|
741
|
-
index += 1;
|
|
742
|
-
} else if (eventType === "patch_apply_end") {
|
|
743
|
-
const item = patchApplyFileChange(conversationId, payload, inputCwd, createdAt);
|
|
744
|
-
if (item) itemsById.set(item.id, item);
|
|
745
|
-
}
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (entry?.type !== "response_item" || !payload) continue;
|
|
750
|
-
const responseType = typeof payload.type === "string" ? payload.type : undefined;
|
|
751
|
-
if (responseType === "message") {
|
|
752
|
-
const role = visibleCodexRole(payload.role);
|
|
753
|
-
if (!role) continue;
|
|
754
|
-
const text = normalizeHistoryText(payload.message);
|
|
755
|
-
const contentText = text ?? normalizeHistoryText(payload.content);
|
|
756
|
-
if (!contentText || isInjectedCodexContext(contentText)) continue;
|
|
757
|
-
const dedupeKey = messageDedupeKey(role, contentText);
|
|
758
|
-
if (seenMessages.has(dedupeKey)) continue;
|
|
759
|
-
seenMessages.add(dedupeKey);
|
|
760
|
-
itemsById.set(`history:${sessionId}:${index}`, historyMessage(conversationId, index, role, contentText, createdAt, sessionId));
|
|
761
|
-
index += 1;
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
if (responseType === "function_call") {
|
|
765
|
-
const callId = typeof payload.call_id === "string"
|
|
766
|
-
? payload.call_id
|
|
767
|
-
: typeof payload.id === "string"
|
|
768
|
-
? payload.id
|
|
769
|
-
: undefined;
|
|
770
|
-
if (!callId) continue;
|
|
771
|
-
const name = typeof payload.name === "string" ? payload.name : undefined;
|
|
772
|
-
upsertHistoryTool(
|
|
773
|
-
itemsById,
|
|
774
|
-
conversationId,
|
|
775
|
-
callId,
|
|
776
|
-
name,
|
|
777
|
-
payload.arguments,
|
|
778
|
-
stringifyHistoryValue(payload.arguments),
|
|
779
|
-
createdAt,
|
|
780
|
-
);
|
|
781
|
-
continue;
|
|
782
|
-
}
|
|
783
|
-
if (responseType === "function_call_output") {
|
|
784
|
-
const callId = typeof payload.call_id === "string"
|
|
785
|
-
? payload.call_id
|
|
786
|
-
: typeof payload.id === "string"
|
|
787
|
-
? payload.id
|
|
788
|
-
: undefined;
|
|
789
|
-
if (!callId) continue;
|
|
790
|
-
completeHistoryTool(itemsById, conversationId, callId, stringifyHistoryValue(payload.output), createdAt);
|
|
791
|
-
}
|
|
792
|
-
} catch {
|
|
793
|
-
// Ignore malformed or partial JSONL lines in the history window.
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
return {
|
|
798
|
-
items: [...itemsById.values()]
|
|
799
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
800
|
-
.slice(-Math.max(1, options.maxItems ?? MAX_HISTORY_ITEMS)),
|
|
801
|
-
};
|
|
802
|
-
}
|