linkshell-cli 0.3.8 → 0.3.10

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.
@@ -18,10 +18,38 @@ export interface ClaudeStoredSession {
18
18
  export interface StoredAgentTimelineItem {
19
19
  id: string;
20
20
  conversationId: string;
21
- type: "message";
22
- role: "user" | "assistant" | "system";
23
- content: Array<{ type: "text"; text: string }>;
24
- text: 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
+ };
25
53
  createdAt: number;
26
54
  updatedAt?: number;
27
55
  metadata?: Record<string, unknown>;
@@ -110,6 +138,35 @@ function extractHistoryText(value: unknown): string | undefined {
110
138
  return extractHistoryText(record.content ?? record.text ?? record.message);
111
139
  }
112
140
 
141
+ function extractVisibleClaudeText(value: unknown): string | undefined {
142
+ if (!Array.isArray(value)) return extractHistoryText(value);
143
+ const text = value
144
+ .map((part) => {
145
+ if (typeof part === "string") return part;
146
+ const record = asRecord(part);
147
+ if (!record || record.type === "tool_use" || record.type === "tool_result") return "";
148
+ return typeof record.text === "string"
149
+ ? record.text
150
+ : typeof record.content === "string"
151
+ ? record.content
152
+ : "";
153
+ })
154
+ .filter(Boolean)
155
+ .join("\n")
156
+ .trim();
157
+ return text || undefined;
158
+ }
159
+
160
+ function stringifyHistoryValue(value: unknown): string | undefined {
161
+ if (value === undefined || value === null || value === "") return undefined;
162
+ if (typeof value === "string") return value;
163
+ try {
164
+ return JSON.stringify(value, null, 2);
165
+ } catch {
166
+ return String(value);
167
+ }
168
+ }
169
+
113
170
  function guessCwdFromProjectDir(projectDirName: string, fallbackCwd: string): string {
114
171
  const trimmed = projectDirName.replace(/^-+/, "");
115
172
  if (!trimmed) return resolve(fallbackCwd);
@@ -292,6 +349,127 @@ function historyMessage(
292
349
  };
293
350
  }
294
351
 
352
+ function claudeToolName(name: string | undefined): string {
353
+ if (!name) return "工具";
354
+ if (name === "Bash") return "命令";
355
+ if (isClaudeFileTool(name)) return "文件修改";
356
+ return name;
357
+ }
358
+
359
+ function isClaudeFileTool(name: string | undefined): boolean {
360
+ return name === "Edit" || name === "MultiEdit" || name === "Write" || name === "NotebookEdit";
361
+ }
362
+
363
+ function claudeToolKind(name: string | undefined): "tool_activity" | "command_execution" | "file_change" {
364
+ if (name === "Bash") return "command_execution";
365
+ if (isClaudeFileTool(name)) return "file_change";
366
+ return "tool_activity";
367
+ }
368
+
369
+ function claudeFileEntry(name: string | undefined, input: Record<string, unknown> | undefined): StoredAgentTimelineItem["fileChange"] | undefined {
370
+ if (!isClaudeFileTool(name) || !input) return undefined;
371
+ const rawPath = input.file_path ?? input.path ?? input.notebook_path;
372
+ if (typeof rawPath !== "string" || !rawPath.trim()) return undefined;
373
+ const path = rawPath.trim();
374
+ const kind = name === "Write" ? "create" : "update";
375
+ const added = typeof input.new_string === "string"
376
+ ? input.new_string.split(/\r?\n/).filter((line) => line.length > 0).length
377
+ : typeof input.content === "string"
378
+ ? input.content.split(/\r?\n/).filter((line) => line.length > 0).length
379
+ : Array.isArray(input.edits)
380
+ ? input.edits.length
381
+ : undefined;
382
+ const removed = typeof input.old_string === "string"
383
+ ? input.old_string.split(/\r?\n/).filter((line) => line.length > 0).length
384
+ : undefined;
385
+ const entry: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] = { path, kind };
386
+ if (added && added > 0) entry.added = added;
387
+ if (removed && removed > 0) entry.removed = removed;
388
+ return {
389
+ entries: [entry],
390
+ summary: [kind, path].filter(Boolean).join(" "),
391
+ status: "running",
392
+ };
393
+ }
394
+
395
+ function claudeCommandExecution(input: Record<string, unknown> | undefined): StoredAgentTimelineItem["commandExecution"] | undefined {
396
+ if (!input) return undefined;
397
+ const command = typeof input.command === "string" ? input.command : undefined;
398
+ const cwd = typeof input.cwd === "string" ? input.cwd : undefined;
399
+ if (!command && !cwd) return undefined;
400
+ return { command, cwd, status: "running" };
401
+ }
402
+
403
+ function createClaudeToolItem(
404
+ conversationId: string,
405
+ toolUseId: string,
406
+ name: string | undefined,
407
+ input: Record<string, unknown> | undefined,
408
+ createdAt: number,
409
+ ): StoredAgentTimelineItem {
410
+ const kind = claudeToolKind(name);
411
+ const inputText = stringifyHistoryValue(input);
412
+ const fileChange = claudeFileEntry(name, input);
413
+ return {
414
+ id: `history-tool:${toolUseId}`,
415
+ conversationId,
416
+ type: "tool_call",
417
+ kind,
418
+ itemId: toolUseId,
419
+ toolCall: {
420
+ id: toolUseId,
421
+ name: claudeToolName(name),
422
+ input: inputText,
423
+ createdAt,
424
+ status: "running",
425
+ },
426
+ commandExecution: name === "Bash" ? claudeCommandExecution(input) : undefined,
427
+ fileChange,
428
+ text: fileChange ? `已编辑 ${fileChange.entries.length} 个文件` : undefined,
429
+ createdAt,
430
+ updatedAt: createdAt,
431
+ metadata: { source: "device-history", provider: "claude", toolName: name },
432
+ };
433
+ }
434
+
435
+ function completeClaudeToolItem(
436
+ itemsById: Map<string, StoredAgentTimelineItem>,
437
+ conversationId: string,
438
+ toolUseId: string,
439
+ output: string | undefined,
440
+ failed: boolean,
441
+ createdAt: number,
442
+ ): void {
443
+ const id = `history-tool:${toolUseId}`;
444
+ const existing = itemsById.get(id);
445
+ const status = failed ? "failed" : "completed";
446
+ itemsById.set(id, {
447
+ id,
448
+ conversationId,
449
+ type: "tool_call",
450
+ kind: existing?.kind ?? "tool_activity",
451
+ itemId: toolUseId,
452
+ toolCall: {
453
+ id: toolUseId,
454
+ name: existing?.toolCall?.name ?? "工具",
455
+ input: existing?.toolCall?.input,
456
+ output,
457
+ createdAt: existing?.toolCall?.createdAt ?? createdAt,
458
+ status,
459
+ },
460
+ commandExecution: existing?.commandExecution
461
+ ? { ...existing.commandExecution, output, status }
462
+ : undefined,
463
+ fileChange: existing?.fileChange
464
+ ? { ...existing.fileChange, summary: output ?? existing.fileChange.summary, status }
465
+ : undefined,
466
+ text: existing?.text,
467
+ createdAt: existing?.createdAt ?? createdAt,
468
+ updatedAt: createdAt,
469
+ metadata: existing?.metadata ?? { source: "device-history", provider: "claude" },
470
+ });
471
+ }
472
+
295
473
  export function loadClaudeStoredTimeline(
296
474
  sessionId: string,
297
475
  conversationId: string,
@@ -308,7 +486,7 @@ export function loadClaudeStoredTimeline(
308
486
  return { items: [] };
309
487
  }
310
488
 
311
- const items: StoredAgentTimelineItem[] = [];
489
+ const itemsById = new Map<string, StoredAgentTimelineItem>();
312
490
  let index = 0;
313
491
  for (const line of readHistorySample(filePath, statSize).split(/\r?\n/)) {
314
492
  const trimmed = line.trim();
@@ -326,20 +504,50 @@ export function loadClaudeStoredTimeline(
326
504
  : undefined;
327
505
  const role = rawRole === "assistant" ? "assistant" : rawRole === "user" ? "user" : undefined;
328
506
  if (!role) continue;
329
- const text = extractHistoryText(message?.content ?? record.content ?? record.text);
330
- if (!text) continue;
331
507
  const createdAt =
332
508
  parseTimestamp(record.timestamp ?? record.createdAt ?? record.created_at) ??
333
509
  statMtime + index;
334
- items.push(historyMessage(conversationId, index, role, text, createdAt, sessionId));
335
- index += 1;
510
+ const content = message?.content ?? record.content ?? record.text;
511
+ const text = extractVisibleClaudeText(content);
512
+ if (text) {
513
+ itemsById.set(
514
+ `history:${sessionId}:${index}`,
515
+ historyMessage(conversationId, index, role, text, createdAt, sessionId),
516
+ );
517
+ index += 1;
518
+ }
519
+ if (Array.isArray(content)) {
520
+ for (const part of content) {
521
+ const block = asRecord(part);
522
+ if (!block) continue;
523
+ if (role === "assistant" && block.type === "tool_use") {
524
+ const toolUseId = typeof block.id === "string" ? block.id : undefined;
525
+ if (!toolUseId) continue;
526
+ const name = typeof block.name === "string" ? block.name : undefined;
527
+ const input = asRecord(block.input);
528
+ const item = createClaudeToolItem(conversationId, toolUseId, name, input, createdAt);
529
+ itemsById.set(item.id, item);
530
+ } else if (role === "user" && block.type === "tool_result") {
531
+ const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
532
+ if (!toolUseId) continue;
533
+ completeClaudeToolItem(
534
+ itemsById,
535
+ conversationId,
536
+ toolUseId,
537
+ extractHistoryText(block.content),
538
+ block.is_error === true,
539
+ createdAt,
540
+ );
541
+ }
542
+ }
543
+ }
336
544
  } catch {
337
545
  // Ignore malformed or partial JSONL lines in the history window.
338
546
  }
339
547
  }
340
548
 
341
549
  return {
342
- items: items
550
+ items: [...itemsById.values()]
343
551
  .sort((a, b) => a.createdAt - b.createdAt)
344
552
  .slice(-MAX_HISTORY_ITEMS),
345
553
  };