linkshell-cli 0.2.66 → 0.2.68

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.
@@ -25,6 +25,12 @@ interface AgentToolCall {
25
25
  status: "pending" | "running" | "completed" | "failed";
26
26
  }
27
27
 
28
+ interface AgentPlanStep {
29
+ id: string;
30
+ text: string;
31
+ status: "pending" | "in_progress" | "completed";
32
+ }
33
+
28
34
  interface AgentPermission {
29
35
  requestId: string;
30
36
  toolName?: string;
@@ -61,6 +67,118 @@ function firstString(value: Record<string, unknown>, keys: string[]): string | u
61
67
  return undefined;
62
68
  }
63
69
 
70
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
71
+ return typeof value === "object" && value ? value as Record<string, unknown> : undefined;
72
+ }
73
+
74
+ function extractItem(value: unknown): Record<string, unknown> | undefined {
75
+ const raw = asRecord(value);
76
+ if (!raw) return undefined;
77
+ return asRecord(raw.item) ?? raw;
78
+ }
79
+
80
+ function stringifyDefined(value: unknown): string | undefined {
81
+ if (value === undefined || value === null || value === "") return undefined;
82
+ return stringify(value);
83
+ }
84
+
85
+ function appendCapped(current: string | undefined, delta: string, maxLength: number): string {
86
+ const next = `${current ?? ""}${delta}`;
87
+ if (next.length <= maxLength) return next;
88
+ return next.slice(next.length - maxLength);
89
+ }
90
+
91
+ function decodeBase64(value: string | undefined): string | undefined {
92
+ if (!value) return undefined;
93
+ try {
94
+ return Buffer.from(value, "base64").toString("utf8");
95
+ } catch {
96
+ return undefined;
97
+ }
98
+ }
99
+
100
+ function normalizeToolStatus(value: unknown, completedFallback = false): AgentToolCall["status"] {
101
+ if (value === "completed" || value === "succeeded" || value === "success" || value === "applied") {
102
+ return "completed";
103
+ }
104
+ if (value === "failed" || value === "error" || value === "declined" || value === "cancelled") {
105
+ return "failed";
106
+ }
107
+ if (value === "pending" || value === "queued") return "pending";
108
+ if (value === "running" || value === "inProgress" || value === "executing") return "running";
109
+ return completedFallback ? "completed" : "running";
110
+ }
111
+
112
+ function normalizePlanStatus(value: unknown): AgentPlanStep["status"] {
113
+ if (value === "completed" || value === "done") return "completed";
114
+ if (value === "inProgress" || value === "running" || value === "active") return "in_progress";
115
+ return "pending";
116
+ }
117
+
118
+ function planStepFromItem(item: Record<string, unknown>): AgentPlanStep | undefined {
119
+ const text = firstString(item, ["text", "title", "description", "message"]);
120
+ if (!text) return undefined;
121
+ return {
122
+ id: firstString(item, ["id", "itemId"]) ?? id("plan"),
123
+ text,
124
+ status: normalizePlanStatus(item.status),
125
+ };
126
+ }
127
+
128
+ function nameFromToolMethod(method: string): string {
129
+ if (method.includes("commandExecution")) return "命令";
130
+ if (method.includes("fileChange")) return "文件修改";
131
+ if (method.includes("mcpToolCall")) return "MCP 工具";
132
+ return "工具";
133
+ }
134
+
135
+ function toolNameFromItem(item: Record<string, unknown>): string | undefined {
136
+ const itemType = firstString(item, ["type"]);
137
+ if (itemType === "commandExecution") return "命令";
138
+ if (itemType === "fileChange") return "文件修改";
139
+ if (itemType === "mcpToolCall") {
140
+ const server = firstString(item, ["server"]);
141
+ const tool = firstString(item, ["tool", "toolName", "name"]);
142
+ return [server, tool].filter(Boolean).join(" · ") || "MCP 工具";
143
+ }
144
+ if (itemType === "dynamicToolCall") {
145
+ const namespace = firstString(item, ["namespace"]);
146
+ const tool = firstString(item, ["tool", "toolName", "name"]);
147
+ return [namespace, tool].filter(Boolean).join(" · ") || "工具";
148
+ }
149
+ return firstString(item, ["toolName", "tool", "name", "title"]) ?? itemType;
150
+ }
151
+
152
+ function toolInputFromItem(item: Record<string, unknown>): string | undefined {
153
+ const itemType = firstString(item, ["type"]);
154
+ if (itemType === "commandExecution") {
155
+ const command = firstString(item, ["command"]);
156
+ const cwd = firstString(item, ["cwd"]);
157
+ if (command && cwd) return `${command}\n\ncwd: ${cwd}`;
158
+ return command ?? cwd;
159
+ }
160
+ if (itemType === "fileChange") {
161
+ const changes = Array.isArray(item.changes) ? item.changes : [];
162
+ return summarizeFileChanges(changes);
163
+ }
164
+ return stringifyDefined(item.arguments ?? item.input ?? item.toolInput);
165
+ }
166
+
167
+ function summarizeFileChanges(changes: unknown[]): string | undefined {
168
+ const lines = changes
169
+ .map((change) => {
170
+ const raw = asRecord(change);
171
+ if (!raw) return undefined;
172
+ const path =
173
+ firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
174
+ firstString(asRecord(raw.update) ?? {}, ["path", "file", "filePath"]);
175
+ const kind = firstString(raw, ["kind", "type", "operation", "action"]);
176
+ return [kind, path].filter(Boolean).join(" ");
177
+ })
178
+ .filter((line): line is string => Boolean(line));
179
+ return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
180
+ }
181
+
64
182
  export class AgentSessionProxy {
65
183
  private client: AcpClient | undefined;
66
184
  private agentSessionId: string | undefined;
@@ -70,6 +188,9 @@ export class AgentSessionProxy {
70
188
  private currentTurnId: string | undefined;
71
189
  private messages: AgentMessage[] = [];
72
190
  private toolCalls = new Map<string, AgentToolCall>();
191
+ private toolOutputBuffers = new Map<string, string>();
192
+ private plan: AgentPlanStep[] = [];
193
+ private planDeltaBuffers = new Map<string, string>();
73
194
  private pendingPermissions = new Map<string, AgentPermission>();
74
195
  private permissionWaiters = new Map<string, PendingPermissionWaiter>();
75
196
  private permissionSources = new Map<string, string>();
@@ -313,7 +434,12 @@ export class AgentSessionProxy {
313
434
  if (
314
435
  method === "initialized" ||
315
436
  method.startsWith("account/") ||
316
- method.startsWith("mcpServer/startupStatus/")
437
+ method.startsWith("mcpServer/startupStatus/") ||
438
+ method === "thread/status/changed" ||
439
+ method === "thread/tokenUsage/updated" ||
440
+ method === "turn/diff/updated" ||
441
+ method === "serverRequest/resolved" ||
442
+ method === "mcpServer/oauthLogin/completed"
317
443
  ) {
318
444
  return;
319
445
  }
@@ -339,11 +465,48 @@ export class AgentSessionProxy {
339
465
  this.handlePermission(params, false, method);
340
466
  return;
341
467
  }
342
- if (method === "session/update" || method.startsWith("item/")) {
468
+
469
+ switch (method) {
470
+ case "item/agentMessage/delta":
471
+ this.handleAgentMessageDelta(params);
472
+ return;
473
+ case "turn/plan/updated":
474
+ this.handlePlanUpdated(params);
475
+ return;
476
+ case "item/plan/delta":
477
+ this.handlePlanDelta(params);
478
+ return;
479
+ case "item/started":
480
+ this.handleItemStarted(params);
481
+ return;
482
+ case "item/completed":
483
+ this.handleItemCompleted(params);
484
+ return;
485
+ case "item/commandExecution/outputDelta":
486
+ case "item/fileChange/outputDelta":
487
+ case "item/mcpToolCall/progress":
488
+ this.handleToolDelta(method, params);
489
+ return;
490
+ case "item/fileChange/patchUpdated":
491
+ this.handleFilePatchUpdated(params);
492
+ return;
493
+ case "command/exec/outputDelta":
494
+ this.handleCommandExecDelta(params);
495
+ return;
496
+ case "item/autoApprovalReview/started":
497
+ case "item/autoApprovalReview/completed":
498
+ case "item/commandExecution/terminalInteraction":
499
+ return;
500
+ }
501
+
502
+ if (method === "session/update") {
343
503
  this.handleUpdate(params);
344
504
  return;
345
505
  }
346
- this.handleUpdate({ method, params });
506
+
507
+ if (this.input.verbose) {
508
+ process.stderr.write(`[agent] ignored ${method}\n`);
509
+ }
347
510
  }
348
511
 
349
512
  private handlePermission(
@@ -386,13 +549,226 @@ export class AgentSessionProxy {
386
549
  });
387
550
  }
388
551
 
552
+ private handleAgentMessageDelta(params: unknown): void {
553
+ const raw = asRecord(params);
554
+ if (!raw) return;
555
+ const itemId = firstString(raw, ["itemId", "id", "messageId"]) ?? id("msg");
556
+ const delta = firstString(raw, ["delta", "text", "content"]);
557
+ if (!delta) return;
558
+ const current = this.messages.find((message) => message.id === itemId);
559
+ const message: AgentMessage = {
560
+ id: itemId,
561
+ role: "assistant",
562
+ content: `${current?.content ?? ""}${delta}`,
563
+ createdAt: current?.createdAt ?? Date.now(),
564
+ isStreaming: true,
565
+ };
566
+ this.upsertMessage(message);
567
+ this.status = "running";
568
+ this.sendUpdate({ kind: "message_delta", message, delta, status: "running" });
569
+ }
570
+
571
+ private handlePlanUpdated(params: unknown): void {
572
+ const raw = asRecord(params);
573
+ const plan = Array.isArray(raw?.plan) ? raw.plan : [];
574
+ this.plan = plan
575
+ .map((entry, index) => {
576
+ const step = asRecord(entry);
577
+ const text = firstString(step ?? {}, ["text", "title", "description", "message"]);
578
+ if (!text) return undefined;
579
+ return {
580
+ id: firstString(step ?? {}, ["id"]) ?? `plan-${index + 1}`,
581
+ text,
582
+ status: normalizePlanStatus(step?.status),
583
+ } satisfies AgentPlanStep;
584
+ })
585
+ .filter((step): step is AgentPlanStep => Boolean(step));
586
+ if (this.plan.length > 0) {
587
+ this.sendUpdate({ kind: "plan", plan: this.plan, status: "running" });
588
+ }
589
+ }
590
+
591
+ private handlePlanDelta(params: unknown): void {
592
+ const raw = asRecord(params);
593
+ if (!raw) return;
594
+ const itemId = firstString(raw, ["itemId", "id"]) ?? id("plan");
595
+ const delta = firstString(raw, ["delta", "text"]);
596
+ if (!delta) return;
597
+ const text = `${this.planDeltaBuffers.get(itemId) ?? ""}${delta}`;
598
+ this.planDeltaBuffers.set(itemId, text);
599
+ const existing = this.plan.findIndex((step) => step.id === itemId);
600
+ const step: AgentPlanStep = { id: itemId, text, status: "in_progress" };
601
+ if (existing >= 0) this.plan[existing] = step;
602
+ else this.plan.push(step);
603
+ this.sendUpdate({ kind: "plan", plan: this.plan, status: "running" });
604
+ }
605
+
606
+ private handleItemStarted(params: unknown): void {
607
+ const item = extractItem(params);
608
+ if (!item) return;
609
+ const itemType = firstString(item, ["type"]);
610
+
611
+ if (itemType === "agentMessage" || itemType === "assistantMessage") {
612
+ this.handleCompletedMessageItem(item, true);
613
+ return;
614
+ }
615
+
616
+ if (itemType === "plan") {
617
+ const planStep = planStepFromItem(item);
618
+ if (planStep) {
619
+ const existing = this.plan.findIndex((step) => step.id === planStep.id);
620
+ if (existing >= 0) this.plan[existing] = planStep;
621
+ else this.plan.push(planStep);
622
+ this.sendUpdate({ kind: "plan", plan: this.plan, status: "running" });
623
+ }
624
+ return;
625
+ }
626
+
627
+ const toolCall = this.toolCallFromItem(item, "running");
628
+ if (!toolCall) return;
629
+ this.toolCalls.set(toolCall.id, toolCall);
630
+ this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
631
+ }
632
+
633
+ private handleItemCompleted(params: unknown): void {
634
+ const item = extractItem(params);
635
+ if (!item) return;
636
+ const itemType = firstString(item, ["type"]);
637
+
638
+ if (itemType === "agentMessage" || itemType === "assistantMessage") {
639
+ this.handleCompletedMessageItem(item, false);
640
+ return;
641
+ }
642
+
643
+ if (itemType === "plan") {
644
+ const planStep = planStepFromItem(item);
645
+ if (planStep) {
646
+ const existing = this.plan.findIndex((step) => step.id === planStep.id);
647
+ const completed = { ...planStep, status: "completed" as const };
648
+ if (existing >= 0) this.plan[existing] = completed;
649
+ else this.plan.push(completed);
650
+ this.sendUpdate({ kind: "plan", plan: this.plan, status: this.status === "running" ? "running" : "idle" });
651
+ }
652
+ return;
653
+ }
654
+
655
+ const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
656
+ if (!toolCall) return;
657
+ const bufferedOutput = this.toolOutputBuffers.get(toolCall.id);
658
+ if (bufferedOutput && !toolCall.output) toolCall.output = bufferedOutput;
659
+ this.toolCalls.set(toolCall.id, toolCall);
660
+ this.sendUpdate({ kind: "tool_result", toolCall, status: this.status === "running" ? "running" : "idle" });
661
+ }
662
+
663
+ private handleToolDelta(method: string, params: unknown): void {
664
+ const raw = asRecord(params);
665
+ if (!raw) return;
666
+ const itemId = firstString(raw, ["itemId", "id", "toolCallId"]) ?? id("tool");
667
+ const delta = firstString(raw, ["delta", "message", "text"]);
668
+ if (!delta) return;
669
+ const output = appendCapped(this.toolOutputBuffers.get(itemId), delta, 6000);
670
+ this.toolOutputBuffers.set(itemId, output);
671
+ const existing = this.toolCalls.get(itemId);
672
+ const toolCall: AgentToolCall = {
673
+ id: itemId,
674
+ name: existing?.name ?? nameFromToolMethod(method),
675
+ input: existing?.input,
676
+ output,
677
+ status: "running",
678
+ };
679
+ this.toolCalls.set(itemId, toolCall);
680
+ this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
681
+ }
682
+
683
+ private handleFilePatchUpdated(params: unknown): void {
684
+ const raw = asRecord(params);
685
+ if (!raw) return;
686
+ const itemId = firstString(raw, ["itemId", "id"]) ?? id("file");
687
+ const changes = Array.isArray(raw.changes) ? raw.changes : [];
688
+ const output = summarizeFileChanges(changes);
689
+ const existing = this.toolCalls.get(itemId);
690
+ const toolCall: AgentToolCall = {
691
+ id: itemId,
692
+ name: existing?.name ?? "文件修改",
693
+ input: existing?.input,
694
+ output: output || existing?.output,
695
+ status: existing?.status ?? "running",
696
+ };
697
+ this.toolCalls.set(itemId, toolCall);
698
+ this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
699
+ }
700
+
701
+ private handleCommandExecDelta(params: unknown): void {
702
+ const raw = asRecord(params);
703
+ if (!raw) return;
704
+ const processId = firstString(raw, ["processId", "id"]) ?? id("exec");
705
+ const delta =
706
+ firstString(raw, ["delta", "text"]) ??
707
+ decodeBase64(firstString(raw, ["deltaBase64"]));
708
+ if (!delta) return;
709
+ const output = appendCapped(this.toolOutputBuffers.get(processId), delta, 6000);
710
+ this.toolOutputBuffers.set(processId, output);
711
+ const existing = this.toolCalls.get(processId);
712
+ const toolCall: AgentToolCall = {
713
+ id: processId,
714
+ name: existing?.name ?? "命令输出",
715
+ input: existing?.input,
716
+ output,
717
+ status: "running",
718
+ };
719
+ this.toolCalls.set(processId, toolCall);
720
+ this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
721
+ }
722
+
723
+ private handleCompletedMessageItem(item: Record<string, unknown>, streaming: boolean): void {
724
+ const itemId = firstString(item, ["id"]) ?? id("msg");
725
+ const existing = this.messages.find((message) => message.id === itemId);
726
+ const content = firstString(item, ["text", "content", "message"]) ?? existing?.content;
727
+ if (!content) return;
728
+ const message: AgentMessage = {
729
+ id: itemId,
730
+ role: "assistant",
731
+ content,
732
+ createdAt: existing?.createdAt ?? Date.now(),
733
+ isStreaming: streaming,
734
+ };
735
+ this.upsertMessage(message);
736
+ this.sendUpdate({
737
+ kind: streaming ? "message_delta" : "message",
738
+ message,
739
+ status: this.status === "running" ? "running" : "idle",
740
+ });
741
+ }
742
+
743
+ private toolCallFromItem(
744
+ item: Record<string, unknown>,
745
+ fallbackStatus: AgentToolCall["status"],
746
+ ): AgentToolCall | undefined {
747
+ const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
748
+ if (!itemId) return undefined;
749
+ const itemType = firstString(item, ["type"]);
750
+ const name = toolNameFromItem(item);
751
+ const output =
752
+ firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
753
+ stringifyDefined(item.result ?? item.error ?? item.contentItems);
754
+ const bufferedOutput = this.toolOutputBuffers.get(itemId);
755
+ return {
756
+ id: itemId,
757
+ name: name ?? itemType ?? "tool",
758
+ input: toolInputFromItem(item),
759
+ output: output ?? bufferedOutput,
760
+ status: normalizeToolStatus(item.status, fallbackStatus === "completed"),
761
+ };
762
+ }
763
+
389
764
  private handleUpdate(params: unknown): void {
390
765
  const raw = typeof params === "object" && params ? params as Record<string, unknown> : {};
391
766
  const nested = typeof raw.params === "object" && raw.params ? raw.params as Record<string, unknown> : {};
392
767
  const text =
393
768
  firstString(raw, ["delta", "text", "content", "message"]) ??
394
769
  firstString(nested, ["delta", "text", "content", "message"]) ??
395
- stringify(raw.update ?? raw.params ?? params);
770
+ undefined;
771
+ if (!text) return;
396
772
  const role = raw.role === "user" || raw.role === "system" ? raw.role : "assistant";
397
773
 
398
774
  if (firstString(raw, ["toolName", "tool", "name"])) {
@@ -417,8 +793,7 @@ export class AgentSessionProxy {
417
793
  createdAt: Date.now(),
418
794
  isStreaming: raw.done === false || raw.isStreaming === true,
419
795
  };
420
- this.messages.push(message);
421
- if (this.messages.length > 100) this.messages.shift();
796
+ this.upsertMessage(message);
422
797
  this.status = raw.done === true ? "idle" : "running";
423
798
  this.sendUpdate({
424
799
  kind: "message",
@@ -449,6 +824,13 @@ export class AgentSessionProxy {
449
824
  this.permissionWaiters.clear();
450
825
  }
451
826
 
827
+ private upsertMessage(message: AgentMessage): void {
828
+ const existing = this.messages.findIndex((entry) => entry.id === message.id);
829
+ if (existing >= 0) this.messages[existing] = message;
830
+ else this.messages.push(message);
831
+ if (this.messages.length > 100) this.messages.shift();
832
+ }
833
+
452
834
  private sendCapabilities(): void {
453
835
  const enabled = Boolean(this.client && this.initialized && !this.error);
454
836
  this.input.send(createEnvelope({
@@ -464,7 +846,7 @@ export class AgentSessionProxy {
464
846
  supportsImages: false,
465
847
  supportsAudio: false,
466
848
  supportsPermission: enabled,
467
- supportsPlan: false,
849
+ supportsPlan: enabled,
468
850
  supportsCancel: enabled,
469
851
  },
470
852
  }));
@@ -490,6 +872,7 @@ export class AgentSessionProxy {
490
872
  message?: AgentMessage;
491
873
  delta?: string;
492
874
  toolCall?: AgentToolCall;
875
+ plan?: AgentPlanStep[];
493
876
  status?: "idle" | "running" | "waiting_permission" | "error";
494
877
  error?: string;
495
878
  }): void {