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.
Files changed (64) hide show
  1. package/dist/cli/src/commands/setup.js +20 -2
  2. package/dist/cli/src/commands/setup.js.map +1 -1
  3. package/dist/cli/src/commands/upgrade.js +9 -24
  4. package/dist/cli/src/commands/upgrade.js.map +1 -1
  5. package/dist/cli/src/index.js +28 -26
  6. package/dist/cli/src/index.js.map +1 -1
  7. package/dist/cli/src/providers.d.ts +3 -7
  8. package/dist/cli/src/providers.js +76 -19
  9. package/dist/cli/src/providers.js.map +1 -1
  10. package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -10
  11. package/dist/cli/src/runtime/acp/acp-client.js +19 -146
  12. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
  13. package/dist/cli/src/runtime/acp/agent-session.d.ts +62 -0
  14. package/dist/cli/src/runtime/acp/agent-session.js +1075 -0
  15. package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
  16. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +1 -24
  17. package/dist/cli/src/runtime/acp/agent-workspace.js +162 -981
  18. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  19. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +1 -3
  20. package/dist/cli/src/runtime/acp/claude-sdk-client.js +35 -81
  21. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
  22. package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -2
  23. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +35 -54
  24. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
  25. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +12 -1
  26. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +1 -1
  27. package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -34
  28. package/dist/cli/src/runtime/acp/json-rpc.js +8 -32
  29. package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
  30. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -3
  31. package/dist/cli/src/runtime/acp/provider-resolver.js +6 -26
  32. package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
  33. package/dist/cli/src/runtime/bridge-session.d.ts +31 -1
  34. package/dist/cli/src/runtime/bridge-session.js +991 -81
  35. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  36. package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
  37. package/dist/cli/src/runtime/screen-fallback.js +4 -4
  38. package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
  39. package/dist/cli/src/runtime/screen-share.d.ts +1 -1
  40. package/dist/cli/src/runtime/screen-share.js +7 -7
  41. package/dist/cli/src/runtime/screen-share.js.map +1 -1
  42. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  43. package/dist/shared-protocol/src/index.d.ts +11940 -3423
  44. package/dist/shared-protocol/src/index.js +98 -164
  45. package/dist/shared-protocol/src/index.js.map +1 -1
  46. package/package.json +5 -5
  47. package/src/commands/setup.ts +31 -5
  48. package/src/commands/upgrade.ts +8 -28
  49. package/src/index.ts +34 -29
  50. package/src/providers.ts +108 -26
  51. package/src/runtime/acp/acp-client.ts +21 -159
  52. package/src/runtime/acp/agent-session.ts +1180 -0
  53. package/src/runtime/acp/agent-workspace.ts +170 -1068
  54. package/src/runtime/acp/claude-sdk-client.ts +37 -85
  55. package/src/runtime/acp/claude-stream-json-client.ts +41 -59
  56. package/src/runtime/acp/json-rpc.ts +14 -46
  57. package/src/runtime/acp/provider-resolver.ts +6 -29
  58. package/src/runtime/bridge-session.ts +1092 -84
  59. package/src/runtime/screen-fallback.ts +5 -5
  60. package/src/runtime/screen-share.ts +8 -8
  61. package/src/runtime/acp/claude-sessions.ts +0 -563
  62. package/src/runtime/acp/codex-rpc-bridge.ts +0 -221
  63. package/src/runtime/acp/codex-sessions.ts +0 -802
  64. package/src/types/linkshell-gateway.d.ts +0 -18
@@ -10,7 +10,7 @@ export interface ScreenFallbackOptions {
10
10
  fps: number;
11
11
  quality: number;
12
12
  scale: number;
13
- hostDeviceId: string;
13
+ sessionId: string;
14
14
  onFrame: (envelope: Envelope) => void;
15
15
  onStatus: (envelope: Envelope) => void;
16
16
  }
@@ -38,7 +38,7 @@ export class ScreenFallback {
38
38
  this.options.onStatus(
39
39
  createEnvelope({
40
40
  type: "screen.status",
41
- hostDeviceId: this.options.hostDeviceId,
41
+ sessionId: this.options.sessionId,
42
42
  payload: { active: true, mode: "fallback" as const },
43
43
  }),
44
44
  );
@@ -64,7 +64,7 @@ export class ScreenFallback {
64
64
  this.options.onStatus(
65
65
  createEnvelope({
66
66
  type: "screen.status",
67
- hostDeviceId: this.options.hostDeviceId,
67
+ sessionId: this.options.sessionId,
68
68
  payload: { active: false, mode: "off" as const },
69
69
  }),
70
70
  );
@@ -88,7 +88,7 @@ export class ScreenFallback {
88
88
  this.options.onFrame(
89
89
  createEnvelope({
90
90
  type: "screen.frame",
91
- hostDeviceId: this.options.hostDeviceId,
91
+ sessionId: this.options.sessionId,
92
92
  payload: {
93
93
  data: base64,
94
94
  width: dims.width,
@@ -108,7 +108,7 @@ export class ScreenFallback {
108
108
  this.options.onFrame(
109
109
  createEnvelope({
110
110
  type: "screen.frame",
111
- hostDeviceId: this.options.hostDeviceId,
111
+ sessionId: this.options.sessionId,
112
112
  payload: {
113
113
  data: chunk,
114
114
  width: dims.width,
@@ -22,7 +22,7 @@ function detectMacScreenIndex(): string {
22
22
  }
23
23
 
24
24
  export interface ScreenShareOptions {
25
- hostDeviceId: string;
25
+ sessionId: string;
26
26
  fps: number;
27
27
  quality: number;
28
28
  scale: number;
@@ -111,7 +111,7 @@ export class ScreenShare {
111
111
  this.options.onSignal(
112
112
  createEnvelope({
113
113
  type: "screen.ice",
114
- hostDeviceId: this.options.hostDeviceId,
114
+ sessionId: this.options.sessionId,
115
115
  payload: {
116
116
  candidate: candidate.candidate,
117
117
  sdpMid: candidate.sdpMid ?? null,
@@ -128,7 +128,7 @@ export class ScreenShare {
128
128
  this.options.onStatus(
129
129
  createEnvelope({
130
130
  type: "screen.status",
131
- hostDeviceId: this.options.hostDeviceId,
131
+ sessionId: this.options.sessionId,
132
132
  payload: { active: true, mode: "webrtc" as const },
133
133
  }),
134
134
  );
@@ -136,7 +136,7 @@ export class ScreenShare {
136
136
  this.options.onStatus(
137
137
  createEnvelope({
138
138
  type: "screen.status",
139
- hostDeviceId: this.options.hostDeviceId,
139
+ sessionId: this.options.sessionId,
140
140
  payload: { active: false, mode: "off" as const, error: `WebRTC ${state}` },
141
141
  }),
142
142
  );
@@ -152,7 +152,7 @@ export class ScreenShare {
152
152
  this.options.onStatus(
153
153
  createEnvelope({
154
154
  type: "screen.status",
155
- hostDeviceId: this.options.hostDeviceId,
155
+ sessionId: this.options.sessionId,
156
156
  payload: { active: true, mode: "webrtc" as const },
157
157
  }),
158
158
  );
@@ -160,7 +160,7 @@ export class ScreenShare {
160
160
  this.options.onSignal(
161
161
  createEnvelope({
162
162
  type: "screen.offer",
163
- hostDeviceId: this.options.hostDeviceId,
163
+ sessionId: this.options.sessionId,
164
164
  payload: { sdp: offer.sdp },
165
165
  }),
166
166
  );
@@ -172,7 +172,7 @@ export class ScreenShare {
172
172
  this.options.onStatus(
173
173
  createEnvelope({
174
174
  type: "screen.status",
175
- hostDeviceId: this.options.hostDeviceId,
175
+ sessionId: this.options.sessionId,
176
176
  payload: {
177
177
  active: false,
178
178
  mode: "off" as const,
@@ -221,7 +221,7 @@ export class ScreenShare {
221
221
  this.options.onStatus(
222
222
  createEnvelope({
223
223
  type: "screen.status",
224
- hostDeviceId: this.options.hostDeviceId,
224
+ sessionId: this.options.sessionId,
225
225
  payload: { active: false, mode: "off" as const },
226
226
  }),
227
227
  );
@@ -1,563 +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 = 64 * 1024;
6
- const HISTORY_BYTES = 2 * 1024 * 1024;
7
- const MAX_SESSIONS = 200;
8
- const MAX_HISTORY_ITEMS = 200;
9
-
10
- export interface ClaudeStoredSession {
11
- id: string;
12
- cwd: string;
13
- title?: string;
14
- createdAt?: number;
15
- lastModified: number;
16
- }
17
-
18
- export interface StoredAgentTimelineItem {
19
- id: string;
20
- conversationId: string;
21
- type: "message" | "tool_call";
22
- kind?: "tool_activity" | "command_execution" | "file_change";
23
- itemId?: string;
24
- role?: "user" | "assistant" | "system";
25
- content?: Array<{ type: "text"; text: string }>;
26
- text?: string;
27
- toolCall?: {
28
- id: string;
29
- name: string;
30
- input?: string;
31
- output?: string;
32
- createdAt?: number;
33
- status: "pending" | "running" | "completed" | "failed";
34
- };
35
- commandExecution?: {
36
- command?: string;
37
- cwd?: string;
38
- output?: string;
39
- exitCode?: number | null;
40
- status?: "pending" | "running" | "completed" | "failed";
41
- };
42
- fileChange?: {
43
- entries: Array<{
44
- path: string;
45
- kind?: string;
46
- added?: number;
47
- removed?: number;
48
- }>;
49
- diff?: string;
50
- summary?: string;
51
- status?: "pending" | "running" | "completed" | "failed";
52
- };
53
- createdAt: number;
54
- updatedAt?: number;
55
- metadata?: Record<string, unknown>;
56
- }
57
-
58
- function asRecord(value: unknown): Record<string, unknown> | undefined {
59
- return value && typeof value === "object" && !Array.isArray(value)
60
- ? value as Record<string, unknown>
61
- : undefined;
62
- }
63
-
64
- function parseTimestamp(value: unknown): number | undefined {
65
- if (typeof value === "number" && Number.isFinite(value)) {
66
- return value > 10_000_000_000 ? value : value * 1000;
67
- }
68
- if (typeof value === "string" && value.trim()) {
69
- const parsed = Date.parse(value);
70
- return Number.isNaN(parsed) ? undefined : parsed;
71
- }
72
- return undefined;
73
- }
74
-
75
- function normalizeTitle(value: string | undefined): string | undefined {
76
- const compact = value?.replace(/\s+/g, " ").trim();
77
- if (!compact) return undefined;
78
- return compact.length > 80 ? `${compact.slice(0, 77)}...` : compact;
79
- }
80
-
81
- function findStringDeep(value: unknown, keys: string[], depth = 0): string | undefined {
82
- if (depth > 4) return undefined;
83
- const record = asRecord(value);
84
- if (!record) return undefined;
85
- for (const key of keys) {
86
- const candidate = record[key];
87
- if (typeof candidate === "string" && candidate.trim()) return candidate;
88
- }
89
- for (const candidate of Object.values(record)) {
90
- if (candidate && typeof candidate === "object") {
91
- const found = findStringDeep(candidate, keys, depth + 1);
92
- if (found) return found;
93
- }
94
- }
95
- return undefined;
96
- }
97
-
98
- function extractMessageText(value: unknown): string | undefined {
99
- if (typeof value === "string") return normalizeTitle(value);
100
- if (Array.isArray(value)) {
101
- const text = value
102
- .map((part) => {
103
- if (typeof part === "string") return part;
104
- const record = asRecord(part);
105
- if (record?.type === "tool_use" || record?.type === "tool_result") return "";
106
- return typeof record?.text === "string" ? record.text : "";
107
- })
108
- .join(" ");
109
- return normalizeTitle(text);
110
- }
111
- const record = asRecord(value);
112
- if (!record) return undefined;
113
- return extractMessageText(record.content ?? record.text);
114
- }
115
-
116
- function extractHistoryText(value: unknown): string | undefined {
117
- if (typeof value === "string") {
118
- const text = value.replace(/\r\n/g, "\n").trim();
119
- return text || undefined;
120
- }
121
- if (Array.isArray(value)) {
122
- const text = value
123
- .map((part) => {
124
- if (typeof part === "string") return part;
125
- const record = asRecord(part);
126
- return typeof record?.text === "string"
127
- ? record.text
128
- : typeof record?.content === "string"
129
- ? record.content
130
- : "";
131
- })
132
- .filter(Boolean)
133
- .join("\n")
134
- .trim();
135
- return text || undefined;
136
- }
137
- const record = asRecord(value);
138
- if (!record) return undefined;
139
- return extractHistoryText(record.content ?? record.text ?? record.message);
140
- }
141
-
142
- function extractVisibleClaudeText(value: unknown): string | undefined {
143
- if (!Array.isArray(value)) return extractHistoryText(value);
144
- const text = value
145
- .map((part) => {
146
- if (typeof part === "string") return part;
147
- const record = asRecord(part);
148
- if (!record || record.type === "tool_use" || record.type === "tool_result") return "";
149
- return typeof record.text === "string"
150
- ? record.text
151
- : typeof record.content === "string"
152
- ? record.content
153
- : "";
154
- })
155
- .filter(Boolean)
156
- .join("\n")
157
- .trim();
158
- return text || undefined;
159
- }
160
-
161
- function stringifyHistoryValue(value: unknown): string | undefined {
162
- if (value === undefined || value === null || value === "") return undefined;
163
- if (typeof value === "string") return value;
164
- try {
165
- return JSON.stringify(value, null, 2);
166
- } catch {
167
- return String(value);
168
- }
169
- }
170
-
171
- function guessCwdFromProjectDir(projectDirName: string, fallbackCwd: string): string {
172
- const trimmed = projectDirName.replace(/^-+/, "");
173
- if (!trimmed) return resolve(fallbackCwd);
174
- return `/${trimmed.split("-").filter(Boolean).join("/")}`;
175
- }
176
-
177
- function readSample(filePath: string, size: number): string {
178
- let fd: number | undefined;
179
- try {
180
- fd = openSync(filePath, "r");
181
- if (size <= SAMPLE_BYTES * 2) {
182
- const buffer = Buffer.alloc(size);
183
- const bytesRead = readSync(fd, buffer, 0, size, 0);
184
- return buffer.subarray(0, bytesRead).toString("utf8");
185
- }
186
-
187
- const head = Buffer.alloc(SAMPLE_BYTES);
188
- const tail = Buffer.alloc(SAMPLE_BYTES);
189
- const headBytes = readSync(fd, head, 0, SAMPLE_BYTES, 0);
190
- const tailBytes = readSync(fd, tail, 0, SAMPLE_BYTES, Math.max(0, size - SAMPLE_BYTES));
191
- return `${head.subarray(0, headBytes).toString("utf8")}\n${tail.subarray(0, tailBytes).toString("utf8")}`;
192
- } catch {
193
- return "";
194
- } finally {
195
- if (typeof fd === "number") {
196
- try {
197
- closeSync(fd);
198
- } catch {
199
- // Ignore close failures while listing best-effort local history.
200
- }
201
- }
202
- }
203
- }
204
-
205
- function readHistorySample(filePath: string, size: number): string {
206
- try {
207
- if (size <= HISTORY_BYTES) return readFileSync(filePath, "utf8");
208
- } catch {
209
- return "";
210
- }
211
-
212
- let fd: number | undefined;
213
- try {
214
- fd = openSync(filePath, "r");
215
- const buffer = Buffer.alloc(HISTORY_BYTES);
216
- const bytesRead = readSync(fd, buffer, 0, HISTORY_BYTES, Math.max(0, size - HISTORY_BYTES));
217
- return buffer.subarray(0, bytesRead).toString("utf8");
218
- } catch {
219
- return "";
220
- } finally {
221
- if (typeof fd === "number") {
222
- try {
223
- closeSync(fd);
224
- } catch {
225
- // Ignore close failures while reading best-effort local history.
226
- }
227
- }
228
- }
229
- }
230
-
231
- function readClaudeSessionMetadata(filePath: string, fallbackCwd: string): Omit<ClaudeStoredSession, "id" | "lastModified"> & {
232
- lastModified?: number;
233
- } {
234
- let statMtime: number | undefined;
235
- let statSize = 0;
236
- try {
237
- const stat = statSync(filePath);
238
- statMtime = stat.mtimeMs;
239
- statSize = stat.size;
240
- } catch {
241
- // Best effort only.
242
- }
243
-
244
- const sample = readSample(filePath, statSize);
245
- let cwd: string | undefined;
246
- let title: string | undefined;
247
- let createdAt: number | undefined;
248
- let lastActivityAt: number | undefined;
249
-
250
- for (const line of sample.split(/\r?\n/)) {
251
- const trimmed = line.trim();
252
- if (!trimmed.startsWith("{")) continue;
253
- try {
254
- const entry = JSON.parse(trimmed) as unknown;
255
- const record = asRecord(entry);
256
- if (!record) continue;
257
- cwd ??= findStringDeep(record, ["cwd", "workingDirectory", "workspacePath"]);
258
- const timestamp = parseTimestamp(record.timestamp ?? record.createdAt ?? record.created_at);
259
- createdAt ??= timestamp;
260
- if (timestamp) lastActivityAt = timestamp;
261
- if (!title && record.type === "user") {
262
- title = extractMessageText(asRecord(record.message)?.content ?? record.content);
263
- }
264
- } catch {
265
- // The sample may start in the middle of a JSONL line; skip partial lines.
266
- }
267
- }
268
-
269
- return {
270
- cwd: cwd ?? resolve(fallbackCwd),
271
- title,
272
- createdAt,
273
- lastModified: lastActivityAt ?? statMtime,
274
- };
275
- }
276
-
277
- export function listClaudeStoredSessions(inputCwd: string): { sessions: ClaudeStoredSession[] } {
278
- const root = join(homedir(), ".claude", "projects");
279
- if (!existsSync(root)) return { sessions: [] };
280
-
281
- const sessions: ClaudeStoredSession[] = [];
282
- try {
283
- for (const projectEntry of readdirSync(root, { withFileTypes: true })) {
284
- if (!projectEntry.isDirectory()) continue;
285
- const projectDir = join(root, projectEntry.name);
286
- const fallbackCwd = guessCwdFromProjectDir(projectEntry.name, inputCwd);
287
- for (const sessionEntry of readdirSync(projectDir, { withFileTypes: true })) {
288
- if (!sessionEntry.isFile() || !sessionEntry.name.endsWith(".jsonl")) continue;
289
- const filePath = join(projectDir, sessionEntry.name);
290
- try {
291
- const stat = statSync(filePath);
292
- const metadata = readClaudeSessionMetadata(filePath, fallbackCwd);
293
- sessions.push({
294
- id: basename(sessionEntry.name, ".jsonl"),
295
- cwd: metadata.cwd,
296
- title: metadata.title,
297
- createdAt: metadata.createdAt,
298
- lastModified: metadata.lastModified ?? stat.mtimeMs,
299
- });
300
- } catch {
301
- // Skip individual history files that disappear or are unreadable during the scan.
302
- }
303
- }
304
- }
305
- } catch {
306
- // Ignore unreadable Claude storage; the caller treats an empty list as no local history.
307
- }
308
-
309
- sessions.sort((a, b) => b.lastModified - a.lastModified);
310
- return { sessions: sessions.slice(0, MAX_SESSIONS) };
311
- }
312
-
313
- function findClaudeSessionFile(sessionId: string): string | undefined {
314
- const root = join(homedir(), ".claude", "projects");
315
- if (!existsSync(root)) return undefined;
316
- try {
317
- for (const projectEntry of readdirSync(root, { withFileTypes: true })) {
318
- if (!projectEntry.isDirectory()) continue;
319
- const projectDir = join(root, projectEntry.name);
320
- for (const sessionEntry of readdirSync(projectDir, { withFileTypes: true })) {
321
- if (!sessionEntry.isFile() || !sessionEntry.name.endsWith(".jsonl")) continue;
322
- if (basename(sessionEntry.name, ".jsonl") === sessionId) {
323
- return join(projectDir, sessionEntry.name);
324
- }
325
- }
326
- }
327
- } catch {
328
- return undefined;
329
- }
330
- return undefined;
331
- }
332
-
333
- function historyMessage(
334
- conversationId: string,
335
- index: number,
336
- role: "user" | "assistant" | "system",
337
- text: string,
338
- createdAt: number,
339
- source: string,
340
- ): StoredAgentTimelineItem {
341
- return {
342
- id: `history:${source}:${index}`,
343
- conversationId,
344
- type: "message",
345
- role,
346
- content: [{ type: "text", text }],
347
- text,
348
- createdAt,
349
- metadata: { source: "device-history", provider: "claude" },
350
- };
351
- }
352
-
353
- function claudeToolName(name: string | undefined): string {
354
- if (!name) return "工具";
355
- if (name === "Bash") return "命令";
356
- if (isClaudeFileTool(name)) return "文件修改";
357
- return name;
358
- }
359
-
360
- function isClaudeFileTool(name: string | undefined): boolean {
361
- return name === "Edit" || name === "MultiEdit" || name === "Write" || name === "NotebookEdit";
362
- }
363
-
364
- function claudeToolKind(name: string | undefined): "tool_activity" | "command_execution" | "file_change" {
365
- if (name === "Bash") return "command_execution";
366
- if (isClaudeFileTool(name)) return "file_change";
367
- return "tool_activity";
368
- }
369
-
370
- function claudeFileEntry(name: string | undefined, input: Record<string, unknown> | undefined): StoredAgentTimelineItem["fileChange"] | undefined {
371
- if (!isClaudeFileTool(name) || !input) return undefined;
372
- const rawPath = input.file_path ?? input.path ?? input.notebook_path;
373
- if (typeof rawPath !== "string" || !rawPath.trim()) return undefined;
374
- const path = rawPath.trim();
375
- const kind = name === "Write" ? "create" : "update";
376
- const edits = Array.isArray(input.edits) ? input.edits.map((entry) => asRecord(entry)).filter(Boolean) : [];
377
- const added = typeof input.new_string === "string"
378
- ? countNonEmptyLines(input.new_string)
379
- : typeof input.content === "string"
380
- ? countNonEmptyLines(input.content)
381
- : edits.length > 0
382
- ? edits.reduce((sum, edit) => sum + countNonEmptyLines(typeof edit?.new_string === "string" ? edit.new_string : ""), 0)
383
- : undefined;
384
- const removed = typeof input.old_string === "string"
385
- ? countNonEmptyLines(input.old_string)
386
- : edits.length > 0
387
- ? edits.reduce((sum, edit) => sum + countNonEmptyLines(typeof edit?.old_string === "string" ? edit.old_string : ""), 0)
388
- : undefined;
389
- const entry: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] = { path, kind };
390
- if (added && added > 0) entry.added = added;
391
- if (removed && removed > 0) entry.removed = removed;
392
- return {
393
- entries: [entry],
394
- summary: [kind, path].filter(Boolean).join(" "),
395
- status: "running",
396
- };
397
- }
398
-
399
- function countNonEmptyLines(value: string): number {
400
- return value.split(/\r?\n/).filter((line) => line.length > 0).length;
401
- }
402
-
403
- function claudeCommandExecution(input: Record<string, unknown> | undefined): StoredAgentTimelineItem["commandExecution"] | undefined {
404
- if (!input) return undefined;
405
- const command = typeof input.command === "string" ? input.command : undefined;
406
- const cwd = typeof input.cwd === "string" ? input.cwd : undefined;
407
- if (!command && !cwd) return undefined;
408
- return { command, cwd, status: "running" };
409
- }
410
-
411
- function createClaudeToolItem(
412
- conversationId: string,
413
- toolUseId: string,
414
- name: string | undefined,
415
- input: Record<string, unknown> | undefined,
416
- createdAt: number,
417
- ): StoredAgentTimelineItem {
418
- const kind = claudeToolKind(name);
419
- const inputText = stringifyHistoryValue(input);
420
- const fileChange = claudeFileEntry(name, input);
421
- return {
422
- id: `history-tool:${toolUseId}`,
423
- conversationId,
424
- type: "tool_call",
425
- kind,
426
- itemId: toolUseId,
427
- toolCall: {
428
- id: toolUseId,
429
- name: claudeToolName(name),
430
- input: inputText,
431
- createdAt,
432
- status: "running",
433
- },
434
- commandExecution: name === "Bash" ? claudeCommandExecution(input) : undefined,
435
- fileChange,
436
- text: fileChange ? `已编辑 ${fileChange.entries.length} 个文件` : undefined,
437
- createdAt,
438
- updatedAt: createdAt,
439
- metadata: { source: "device-history", provider: "claude", toolName: name },
440
- };
441
- }
442
-
443
- function completeClaudeToolItem(
444
- itemsById: Map<string, StoredAgentTimelineItem>,
445
- conversationId: string,
446
- toolUseId: string,
447
- output: string | undefined,
448
- failed: boolean,
449
- createdAt: number,
450
- ): void {
451
- const id = `history-tool:${toolUseId}`;
452
- const existing = itemsById.get(id);
453
- const status = failed ? "failed" : "completed";
454
- itemsById.set(id, {
455
- id,
456
- conversationId,
457
- type: "tool_call",
458
- kind: existing?.kind ?? "tool_activity",
459
- itemId: toolUseId,
460
- toolCall: {
461
- id: toolUseId,
462
- name: existing?.toolCall?.name ?? "工具",
463
- input: existing?.toolCall?.input,
464
- output,
465
- createdAt: existing?.toolCall?.createdAt ?? createdAt,
466
- status,
467
- },
468
- commandExecution: existing?.commandExecution
469
- ? { ...existing.commandExecution, output, status }
470
- : undefined,
471
- fileChange: existing?.fileChange
472
- ? { ...existing.fileChange, summary: output ?? existing.fileChange.summary, status }
473
- : undefined,
474
- text: existing?.text,
475
- createdAt: existing?.createdAt ?? createdAt,
476
- updatedAt: createdAt,
477
- metadata: existing?.metadata ?? { source: "device-history", provider: "claude" },
478
- });
479
- }
480
-
481
- export function loadClaudeStoredTimeline(
482
- sessionId: string,
483
- conversationId: string,
484
- options: { maxItems?: number } = {},
485
- ): { items: StoredAgentTimelineItem[] } {
486
- const filePath = findClaudeSessionFile(sessionId);
487
- if (!filePath) return { items: [] };
488
- let statMtime = Date.now();
489
- let statSize = 0;
490
- try {
491
- const stat = statSync(filePath);
492
- statMtime = stat.mtimeMs;
493
- statSize = stat.size;
494
- } catch {
495
- return { items: [] };
496
- }
497
-
498
- const itemsById = new Map<string, StoredAgentTimelineItem>();
499
- let index = 0;
500
- for (const line of readHistorySample(filePath, statSize).split(/\r?\n/)) {
501
- const trimmed = line.trim();
502
- if (!trimmed.startsWith("{")) continue;
503
- try {
504
- const record = asRecord(JSON.parse(trimmed));
505
- if (!record) continue;
506
- const message = asRecord(record.message);
507
- const rawRole = typeof record.role === "string"
508
- ? record.role
509
- : typeof message?.role === "string"
510
- ? message.role
511
- : typeof record.type === "string"
512
- ? record.type
513
- : undefined;
514
- const role = rawRole === "assistant" ? "assistant" : rawRole === "user" ? "user" : undefined;
515
- if (!role) continue;
516
- const createdAt =
517
- parseTimestamp(record.timestamp ?? record.createdAt ?? record.created_at) ??
518
- statMtime + index;
519
- const content = message?.content ?? record.content ?? record.text;
520
- const text = extractVisibleClaudeText(content);
521
- if (text) {
522
- itemsById.set(
523
- `history:${sessionId}:${index}`,
524
- historyMessage(conversationId, index, role, text, createdAt, sessionId),
525
- );
526
- index += 1;
527
- }
528
- if (Array.isArray(content)) {
529
- for (const part of content) {
530
- const block = asRecord(part);
531
- if (!block) continue;
532
- if (role === "assistant" && block.type === "tool_use") {
533
- const toolUseId = typeof block.id === "string" ? block.id : undefined;
534
- if (!toolUseId) continue;
535
- const name = typeof block.name === "string" ? block.name : undefined;
536
- const input = asRecord(block.input);
537
- const item = createClaudeToolItem(conversationId, toolUseId, name, input, createdAt);
538
- itemsById.set(item.id, item);
539
- } else if (role === "user" && block.type === "tool_result") {
540
- const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
541
- if (!toolUseId) continue;
542
- completeClaudeToolItem(
543
- itemsById,
544
- conversationId,
545
- toolUseId,
546
- extractHistoryText(block.content),
547
- block.is_error === true,
548
- createdAt,
549
- );
550
- }
551
- }
552
- }
553
- } catch {
554
- // Ignore malformed or partial JSONL lines in the history window.
555
- }
556
- }
557
-
558
- return {
559
- items: [...itemsById.values()]
560
- .sort((a, b) => a.createdAt - b.createdAt)
561
- .slice(-Math.max(1, options.maxItems ?? MAX_HISTORY_ITEMS)),
562
- };
563
- }