@botcord/daemon 0.2.76 → 0.2.77

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.
@@ -57,11 +57,12 @@ function defaultClientFactory(input) {
57
57
  function isOwnerTrust(msg) {
58
58
  if (msg.room_id?.startsWith(OWNER_CHAT_PREFIX))
59
59
  return true;
60
- if (msg.source_type === "dashboard_user_chat")
60
+ const sourceType = msg.source_type;
61
+ if (sourceType === "dashboard_user_chat")
61
62
  return true;
62
63
  // Cloud Agent run tasks are Hub-issued on the user's behalf, same
63
64
  // trust posture as owner chat.
64
- if (msg.source_type === "cloud_agent_run")
65
+ if (sourceType === "cloud_agent_run")
65
66
  return true;
66
67
  return false;
67
68
  }
@@ -100,9 +101,10 @@ function normalizeInbox(msg, options) {
100
101
  // run completes). All other envelope types (notification, system,
101
102
  // contact_added/removed, …) are still filtered out — they belong in
102
103
  // a separate push-notification path that daemon does not yet implement.
103
- if (env.type !== "message" &&
104
- env.type !== "contact_request" &&
105
- env.type !== "cloud_run")
104
+ const envType = env.type;
105
+ if (envType !== "message" &&
106
+ envType !== "contact_request" &&
107
+ envType !== "cloud_run")
106
108
  return null;
107
109
  if (!msg.room_id)
108
110
  return null;
@@ -113,7 +115,8 @@ function normalizeInbox(msg, options) {
113
115
  const text = ownerTrust ? rawText : sanitizeUntrustedContent(rawText);
114
116
  const isDm = msg.room_id.startsWith(DM_ROOM_PREFIX);
115
117
  const isOwnerChat = msg.room_id.startsWith(OWNER_CHAT_PREFIX);
116
- const senderKind = ownerTrust || msg.source_type === "dashboard_human_room" ? "user" : "agent";
118
+ const sourceType = msg.source_type;
119
+ const senderKind = ownerTrust || sourceType === "dashboard_human_room" ? "user" : "agent";
117
120
  const senderName = msg.source_user_name ?? undefined;
118
121
  const threadId = msg.topic_id ?? msg.topic ?? null;
119
122
  const streamable = isOwnerChat;
@@ -860,55 +863,29 @@ function normalizeBlockForHub(block, seq) {
860
863
  return { kind: "assistant", seq, payload: { text } };
861
864
  }
862
865
  if (kind === "tool_use") {
863
- // Claude Code: assistant message w/ content[].type === "tool_use" {id,name,input}
864
- // Codex: item.started for command_execution, file_change, mcp_tool_call, web_search
865
- const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
866
- const tu = contents.find((c) => c?.type === "tool_use");
867
- if (tu) {
868
- payload.name = typeof tu.name === "string" ? tu.name : "tool";
869
- if (tu.input && typeof tu.input === "object")
870
- payload.params = tu.input;
871
- if (typeof tu.id === "string")
872
- payload.id = tu.id;
873
- }
874
- else if (raw?.item && typeof raw.item === "object") {
875
- payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
876
- const params = codexToolParams(raw.item);
877
- if (Object.keys(params).length > 0)
878
- payload.params = params;
879
- if (typeof raw.item.id === "string")
880
- payload.id = raw.item.id;
881
- if (typeof raw.item.status === "string")
882
- payload.status = raw.item.status;
866
+ // Claude Code, Codex, DeepSeek TUI, Kimi, and ACP all expose tool calls
867
+ // with slightly different field names. Preserve the real invocation input
868
+ // so the dashboard can show more than a bare "tool" label.
869
+ const call = extractToolCall(raw);
870
+ if (call) {
871
+ payload.name = call.name;
872
+ if (call.params !== undefined && !isEmptyRecord(call.params))
873
+ payload.params = call.params;
874
+ if (call.id)
875
+ payload.id = call.id;
876
+ if (call.status)
877
+ payload.status = call.status;
883
878
  }
884
879
  return { kind: "tool_call", seq, payload };
885
880
  }
886
881
  if (kind === "tool_result") {
887
- // Claude Code: {type:"user", message:{content:[{type:"tool_result",tool_use_id,content}]}}
888
- // Codex: item.completed for command_execution, file_change, mcp_tool_call, web_search
889
- const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
890
- const tr = contents.find((c) => c?.type === "tool_result");
891
- if (tr) {
892
- let resultStr = "";
893
- if (typeof tr.content === "string") {
894
- resultStr = tr.content;
895
- }
896
- else if (Array.isArray(tr.content)) {
897
- resultStr = tr.content
898
- .map((c) => (typeof c?.text === "string" ? c.text : JSON.stringify(c)))
899
- .join("\n");
900
- }
901
- payload.result = resultStr;
902
- if (typeof tr.tool_use_id === "string")
903
- payload.tool_use_id = tr.tool_use_id;
904
- }
905
- else if (raw?.item && typeof raw.item === "object") {
906
- payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
907
- if (typeof raw.item.id === "string")
908
- payload.tool_use_id = raw.item.id;
909
- const result = codexToolResult(raw.item);
910
- if (result)
911
- payload.result = result;
882
+ const result = extractToolResult(raw);
883
+ if (result) {
884
+ if (result.name)
885
+ payload.name = result.name;
886
+ payload.result = result.result;
887
+ if (result.id)
888
+ payload.tool_use_id = result.id;
912
889
  }
913
890
  return { kind: "tool_result", seq, payload };
914
891
  }
@@ -965,6 +942,168 @@ function isTerminalRuntimeBlock(raw) {
965
942
  terminal === "turn.done" ||
966
943
  terminal === "done");
967
944
  }
945
+ function extractToolCall(raw) {
946
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
947
+ const tu = contents.find((c) => c?.type === "tool_use");
948
+ if (tu) {
949
+ return {
950
+ name: stringField(tu, "name") ?? "tool",
951
+ params: parseMaybeJson(tu.input ?? tu.arguments),
952
+ id: stringField(tu, "id"),
953
+ };
954
+ }
955
+ const deepseek = extractDeepseekToolCall(raw);
956
+ if (deepseek)
957
+ return deepseek;
958
+ const item = raw?.item;
959
+ if (item && typeof item === "object") {
960
+ const params = codexToolParams(item);
961
+ return {
962
+ name: stringField(item, "type") ?? stringField(item, "name") ?? "tool",
963
+ params,
964
+ id: stringField(item, "id"),
965
+ status: stringField(item, "status"),
966
+ };
967
+ }
968
+ const toolCalls = Array.isArray(raw?.tool_calls) ? raw.tool_calls : [];
969
+ const toolCall = toolCalls.find((t) => t && typeof t === "object");
970
+ if (toolCall) {
971
+ const fn = toolCall.function && typeof toolCall.function === "object" ? toolCall.function : undefined;
972
+ return {
973
+ name: stringField(fn, "name") ?? stringField(toolCall, "name") ?? "tool",
974
+ params: parseMaybeJson(fn?.arguments ?? toolCall.arguments ?? toolCall.input ?? toolCall.rawInput),
975
+ id: stringField(toolCall, "id"),
976
+ };
977
+ }
978
+ const update = raw?.params?.update ?? raw?.update;
979
+ const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
980
+ if (acpTool && typeof acpTool === "object") {
981
+ return {
982
+ name: stringField(acpTool, "name") ?? stringField(update, "name") ?? "tool",
983
+ params: parseMaybeJson(acpTool.rawInput ??
984
+ acpTool.raw_input ??
985
+ acpTool.input ??
986
+ acpTool.arguments ??
987
+ acpTool.args ??
988
+ acpTool.params) ?? acpTool,
989
+ id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
990
+ };
991
+ }
992
+ return null;
993
+ }
994
+ function extractToolResult(raw) {
995
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
996
+ const tr = contents.find((c) => c?.type === "tool_result");
997
+ if (tr) {
998
+ return {
999
+ result: stringifyToolResult(tr.content),
1000
+ id: stringField(tr, "tool_use_id"),
1001
+ };
1002
+ }
1003
+ const deepseek = extractDeepseekToolResult(raw);
1004
+ if (deepseek)
1005
+ return deepseek;
1006
+ const item = raw?.item;
1007
+ if (item && typeof item === "object") {
1008
+ const result = codexToolResult(item);
1009
+ return {
1010
+ name: stringField(item, "type") ?? stringField(item, "name"),
1011
+ result: result || stringifyToolResult(item),
1012
+ id: stringField(item, "id"),
1013
+ };
1014
+ }
1015
+ if (raw?.role === "tool") {
1016
+ return {
1017
+ result: stringifyToolResult(raw.content),
1018
+ id: stringField(raw, "tool_call_id"),
1019
+ };
1020
+ }
1021
+ const update = raw?.params?.update ?? raw?.update;
1022
+ const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
1023
+ if (acpTool && typeof acpTool === "object") {
1024
+ const result = acpTool.output ??
1025
+ acpTool.result ??
1026
+ acpTool.content ??
1027
+ acpTool.error ??
1028
+ update.content ??
1029
+ update;
1030
+ return {
1031
+ name: stringField(acpTool, "name") ?? stringField(update, "name"),
1032
+ result: stringifyToolResult(result),
1033
+ id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
1034
+ };
1035
+ }
1036
+ return null;
1037
+ }
1038
+ function extractDeepseekToolCall(raw) {
1039
+ const payload = raw?.payload;
1040
+ if (!payload || typeof payload !== "object")
1041
+ return null;
1042
+ if (raw?.event === "tool.started") {
1043
+ const tool = payload.tool && typeof payload.tool === "object" ? payload.tool : undefined;
1044
+ return {
1045
+ name: stringField(payload, "name") ?? stringField(tool, "name") ?? "tool",
1046
+ params: parseMaybeJson(payload.input ?? payload.arguments ?? payload.params ?? tool?.input ?? tool?.rawInput),
1047
+ id: stringField(payload, "id") ?? stringField(tool, "id"),
1048
+ status: stringField(payload, "status") ?? stringField(tool, "status"),
1049
+ };
1050
+ }
1051
+ if (payload.event === "item.started") {
1052
+ const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1053
+ const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1054
+ const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
1055
+ return {
1056
+ name: stringField(tool, "name") ??
1057
+ stringField(inner, "name") ??
1058
+ stringField(item, "name") ??
1059
+ stringField(item, "type") ??
1060
+ "tool",
1061
+ params: parseMaybeJson(tool?.input ??
1062
+ tool?.rawInput ??
1063
+ tool?.arguments ??
1064
+ inner.input ??
1065
+ item?.input ??
1066
+ item?.arguments) ?? tool ?? item,
1067
+ id: stringField(tool, "id") ?? stringField(inner, "id") ?? stringField(item, "id"),
1068
+ status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
1069
+ };
1070
+ }
1071
+ return null;
1072
+ }
1073
+ function extractDeepseekToolResult(raw) {
1074
+ const payload = raw?.payload;
1075
+ if (!payload || typeof payload !== "object")
1076
+ return null;
1077
+ if (raw?.event === "tool.completed") {
1078
+ const result = payload.output ?? payload.result ?? payload.content ?? payload.error ?? payload;
1079
+ return {
1080
+ name: stringField(payload, "name"),
1081
+ result: stringifyToolResult(result),
1082
+ id: stringField(payload, "id"),
1083
+ };
1084
+ }
1085
+ if (payload.event === "item.completed" || payload.event === "item.failed") {
1086
+ const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1087
+ const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1088
+ const result = item?.output ??
1089
+ item?.result ??
1090
+ item?.content ??
1091
+ item?.detail ??
1092
+ item?.summary ??
1093
+ item?.error ??
1094
+ inner.output ??
1095
+ inner.result ??
1096
+ inner.error ??
1097
+ item ??
1098
+ inner;
1099
+ return {
1100
+ name: stringField(item, "name") ?? stringField(item, "type") ?? stringField(inner, "name"),
1101
+ result: stringifyToolResult(result),
1102
+ id: stringField(item, "id") ?? stringField(inner, "id"),
1103
+ };
1104
+ }
1105
+ return null;
1106
+ }
968
1107
  function formatBlockDetails(raw) {
969
1108
  if (!raw || typeof raw !== "object")
970
1109
  return "";
@@ -1034,6 +1173,52 @@ function codexToolResult(item) {
1034
1173
  }
1035
1174
  return parts.join("\n");
1036
1175
  }
1176
+ function stringifyToolResult(value) {
1177
+ if (value == null)
1178
+ return "";
1179
+ if (typeof value === "string")
1180
+ return value;
1181
+ if (Array.isArray(value)) {
1182
+ return value
1183
+ .map((c) => {
1184
+ if (typeof c === "string")
1185
+ return c;
1186
+ if (typeof c?.text === "string")
1187
+ return c.text;
1188
+ return stringifyToolResult(c);
1189
+ })
1190
+ .filter(Boolean)
1191
+ .join("\n");
1192
+ }
1193
+ try {
1194
+ return JSON.stringify(value, null, 2);
1195
+ }
1196
+ catch {
1197
+ return String(value);
1198
+ }
1199
+ }
1200
+ function parseMaybeJson(value) {
1201
+ if (typeof value !== "string")
1202
+ return value;
1203
+ const trimmed = value.trim();
1204
+ if (!trimmed)
1205
+ return value;
1206
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
1207
+ return value;
1208
+ try {
1209
+ return JSON.parse(trimmed);
1210
+ }
1211
+ catch {
1212
+ return value;
1213
+ }
1214
+ }
1215
+ function isEmptyRecord(value) {
1216
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
1217
+ }
1218
+ function stringField(obj, key) {
1219
+ const value = obj?.[key];
1220
+ return typeof value === "string" && value.length > 0 ? value : undefined;
1221
+ }
1037
1222
  function extractContentText(content) {
1038
1223
  if (!content)
1039
1224
  return "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.76",
3
+ "version": "0.2.77",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@botcord/cli": "^0.1.7",
31
- "@botcord/protocol-core": "file:../protocol-core",
31
+ "@botcord/protocol-core": "^0.2.9",
32
32
  "@larksuiteoapi/node-sdk": "^1.63.1",
33
33
  "ws": "^8.20.1"
34
34
  },
@@ -769,6 +769,30 @@ describe("createBotCordChannel — streamBlock()", () => {
769
769
  });
770
770
  });
771
771
 
772
+ it("normalizes DeepSeek tool input so the dashboard can expand it", () => {
773
+ expect(
774
+ __normalizeBlockForHubForTests(
775
+ {
776
+ kind: "tool_use",
777
+ seq: 4,
778
+ raw: {
779
+ event: "tool.started",
780
+ payload: { id: "tool_1", name: "exec_shell", input: { cmd: "pwd" } },
781
+ },
782
+ },
783
+ 4,
784
+ ),
785
+ ).toEqual({
786
+ kind: "tool_call",
787
+ seq: 4,
788
+ payload: {
789
+ id: "tool_1",
790
+ name: "exec_shell",
791
+ params: { cmd: "pwd" },
792
+ },
793
+ });
794
+ });
795
+
772
796
  it("POSTs to /hub/stream-block with the right trace_id + block", async () => {
773
797
  const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
774
798
  const realFetch = globalThis.fetch;
@@ -153,10 +153,11 @@ function defaultClientFactory(input: {
153
153
  */
154
154
  function isOwnerTrust(msg: InboxMessage): boolean {
155
155
  if (msg.room_id?.startsWith(OWNER_CHAT_PREFIX)) return true;
156
- if (msg.source_type === "dashboard_user_chat") return true;
156
+ const sourceType = msg.source_type as string | undefined;
157
+ if (sourceType === "dashboard_user_chat") return true;
157
158
  // Cloud Agent run tasks are Hub-issued on the user's behalf, same
158
159
  // trust posture as owner chat.
159
- if (msg.source_type === "cloud_agent_run") return true;
160
+ if (sourceType === "cloud_agent_run") return true;
160
161
  return false;
161
162
  }
162
163
 
@@ -197,10 +198,11 @@ function normalizeInbox(
197
198
  // run completes). All other envelope types (notification, system,
198
199
  // contact_added/removed, …) are still filtered out — they belong in
199
200
  // a separate push-notification path that daemon does not yet implement.
201
+ const envType = env.type as string;
200
202
  if (
201
- env.type !== "message" &&
202
- env.type !== "contact_request" &&
203
- env.type !== "cloud_run"
203
+ envType !== "message" &&
204
+ envType !== "contact_request" &&
205
+ envType !== "cloud_run"
204
206
  )
205
207
  return null;
206
208
  if (!msg.room_id) return null;
@@ -214,8 +216,9 @@ function normalizeInbox(
214
216
 
215
217
  const isDm = msg.room_id.startsWith(DM_ROOM_PREFIX);
216
218
  const isOwnerChat = msg.room_id.startsWith(OWNER_CHAT_PREFIX);
219
+ const sourceType = msg.source_type as string | undefined;
217
220
  const senderKind: "user" | "agent" =
218
- ownerTrust || msg.source_type === "dashboard_human_room" ? "user" : "agent";
221
+ ownerTrust || sourceType === "dashboard_human_room" ? "user" : "agent";
219
222
 
220
223
  const senderName = msg.source_user_name ?? undefined;
221
224
  const threadId = msg.topic_id ?? msg.topic ?? null;
@@ -1005,45 +1008,25 @@ function normalizeBlockForHub(
1005
1008
  }
1006
1009
 
1007
1010
  if (kind === "tool_use") {
1008
- // Claude Code: assistant message w/ content[].type === "tool_use" {id,name,input}
1009
- // Codex: item.started for command_execution, file_change, mcp_tool_call, web_search
1010
- const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
1011
- const tu = contents.find((c: any) => c?.type === "tool_use");
1012
- if (tu) {
1013
- payload.name = typeof tu.name === "string" ? tu.name : "tool";
1014
- if (tu.input && typeof tu.input === "object") payload.params = tu.input;
1015
- if (typeof tu.id === "string") payload.id = tu.id;
1016
- } else if (raw?.item && typeof raw.item === "object") {
1017
- payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
1018
- const params = codexToolParams(raw.item);
1019
- if (Object.keys(params).length > 0) payload.params = params;
1020
- if (typeof raw.item.id === "string") payload.id = raw.item.id;
1021
- if (typeof raw.item.status === "string") payload.status = raw.item.status;
1011
+ // Claude Code, Codex, DeepSeek TUI, Kimi, and ACP all expose tool calls
1012
+ // with slightly different field names. Preserve the real invocation input
1013
+ // so the dashboard can show more than a bare "tool" label.
1014
+ const call = extractToolCall(raw);
1015
+ if (call) {
1016
+ payload.name = call.name;
1017
+ if (call.params !== undefined && !isEmptyRecord(call.params)) payload.params = call.params;
1018
+ if (call.id) payload.id = call.id;
1019
+ if (call.status) payload.status = call.status;
1022
1020
  }
1023
1021
  return { kind: "tool_call", seq, payload };
1024
1022
  }
1025
1023
 
1026
1024
  if (kind === "tool_result") {
1027
- // Claude Code: {type:"user", message:{content:[{type:"tool_result",tool_use_id,content}]}}
1028
- // Codex: item.completed for command_execution, file_change, mcp_tool_call, web_search
1029
- const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
1030
- const tr = contents.find((c: any) => c?.type === "tool_result");
1031
- if (tr) {
1032
- let resultStr = "";
1033
- if (typeof tr.content === "string") {
1034
- resultStr = tr.content;
1035
- } else if (Array.isArray(tr.content)) {
1036
- resultStr = tr.content
1037
- .map((c: any) => (typeof c?.text === "string" ? c.text : JSON.stringify(c)))
1038
- .join("\n");
1039
- }
1040
- payload.result = resultStr;
1041
- if (typeof tr.tool_use_id === "string") payload.tool_use_id = tr.tool_use_id;
1042
- } else if (raw?.item && typeof raw.item === "object") {
1043
- payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
1044
- if (typeof raw.item.id === "string") payload.tool_use_id = raw.item.id;
1045
- const result = codexToolResult(raw.item);
1046
- if (result) payload.result = result;
1025
+ const result = extractToolResult(raw);
1026
+ if (result) {
1027
+ if (result.name) payload.name = result.name;
1028
+ payload.result = result.result;
1029
+ if (result.id) payload.tool_use_id = result.id;
1047
1030
  }
1048
1031
  return { kind: "tool_result", seq, payload };
1049
1032
  }
@@ -1097,6 +1080,191 @@ function isTerminalRuntimeBlock(raw: any): boolean {
1097
1080
  );
1098
1081
  }
1099
1082
 
1083
+ function extractToolCall(raw: any): { name: string; params?: unknown; id?: string; status?: string } | null {
1084
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
1085
+ const tu = contents.find((c: any) => c?.type === "tool_use");
1086
+ if (tu) {
1087
+ return {
1088
+ name: stringField(tu, "name") ?? "tool",
1089
+ params: parseMaybeJson(tu.input ?? tu.arguments),
1090
+ id: stringField(tu, "id"),
1091
+ };
1092
+ }
1093
+
1094
+ const deepseek = extractDeepseekToolCall(raw);
1095
+ if (deepseek) return deepseek;
1096
+
1097
+ const item = raw?.item;
1098
+ if (item && typeof item === "object") {
1099
+ const params = codexToolParams(item);
1100
+ return {
1101
+ name: stringField(item, "type") ?? stringField(item, "name") ?? "tool",
1102
+ params,
1103
+ id: stringField(item, "id"),
1104
+ status: stringField(item, "status"),
1105
+ };
1106
+ }
1107
+
1108
+ const toolCalls = Array.isArray(raw?.tool_calls) ? raw.tool_calls : [];
1109
+ const toolCall = toolCalls.find((t: any) => t && typeof t === "object");
1110
+ if (toolCall) {
1111
+ const fn = toolCall.function && typeof toolCall.function === "object" ? toolCall.function : undefined;
1112
+ return {
1113
+ name: stringField(fn, "name") ?? stringField(toolCall, "name") ?? "tool",
1114
+ params: parseMaybeJson(fn?.arguments ?? toolCall.arguments ?? toolCall.input ?? toolCall.rawInput),
1115
+ id: stringField(toolCall, "id"),
1116
+ };
1117
+ }
1118
+
1119
+ const update = raw?.params?.update ?? raw?.update;
1120
+ const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
1121
+ if (acpTool && typeof acpTool === "object") {
1122
+ return {
1123
+ name: stringField(acpTool, "name") ?? stringField(update, "name") ?? "tool",
1124
+ params: parseMaybeJson(
1125
+ acpTool.rawInput ??
1126
+ acpTool.raw_input ??
1127
+ acpTool.input ??
1128
+ acpTool.arguments ??
1129
+ acpTool.args ??
1130
+ acpTool.params,
1131
+ ) ?? acpTool,
1132
+ id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
1133
+ };
1134
+ }
1135
+
1136
+ return null;
1137
+ }
1138
+
1139
+ function extractToolResult(raw: any): { name?: string; result: string; id?: string } | null {
1140
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
1141
+ const tr = contents.find((c: any) => c?.type === "tool_result");
1142
+ if (tr) {
1143
+ return {
1144
+ result: stringifyToolResult(tr.content),
1145
+ id: stringField(tr, "tool_use_id"),
1146
+ };
1147
+ }
1148
+
1149
+ const deepseek = extractDeepseekToolResult(raw);
1150
+ if (deepseek) return deepseek;
1151
+
1152
+ const item = raw?.item;
1153
+ if (item && typeof item === "object") {
1154
+ const result = codexToolResult(item);
1155
+ return {
1156
+ name: stringField(item, "type") ?? stringField(item, "name"),
1157
+ result: result || stringifyToolResult(item),
1158
+ id: stringField(item, "id"),
1159
+ };
1160
+ }
1161
+
1162
+ if (raw?.role === "tool") {
1163
+ return {
1164
+ result: stringifyToolResult(raw.content),
1165
+ id: stringField(raw, "tool_call_id"),
1166
+ };
1167
+ }
1168
+
1169
+ const update = raw?.params?.update ?? raw?.update;
1170
+ const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
1171
+ if (acpTool && typeof acpTool === "object") {
1172
+ const result =
1173
+ acpTool.output ??
1174
+ acpTool.result ??
1175
+ acpTool.content ??
1176
+ acpTool.error ??
1177
+ update.content ??
1178
+ update;
1179
+ return {
1180
+ name: stringField(acpTool, "name") ?? stringField(update, "name"),
1181
+ result: stringifyToolResult(result),
1182
+ id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
1183
+ };
1184
+ }
1185
+
1186
+ return null;
1187
+ }
1188
+
1189
+ function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id?: string; status?: string } | null {
1190
+ const payload = raw?.payload;
1191
+ if (!payload || typeof payload !== "object") return null;
1192
+
1193
+ if (raw?.event === "tool.started") {
1194
+ const tool = payload.tool && typeof payload.tool === "object" ? payload.tool : undefined;
1195
+ return {
1196
+ name: stringField(payload, "name") ?? stringField(tool, "name") ?? "tool",
1197
+ params: parseMaybeJson(payload.input ?? payload.arguments ?? payload.params ?? tool?.input ?? tool?.rawInput),
1198
+ id: stringField(payload, "id") ?? stringField(tool, "id"),
1199
+ status: stringField(payload, "status") ?? stringField(tool, "status"),
1200
+ };
1201
+ }
1202
+
1203
+ if (payload.event === "item.started") {
1204
+ const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1205
+ const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1206
+ const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
1207
+ return {
1208
+ name:
1209
+ stringField(tool, "name") ??
1210
+ stringField(inner, "name") ??
1211
+ stringField(item, "name") ??
1212
+ stringField(item, "type") ??
1213
+ "tool",
1214
+ params: parseMaybeJson(
1215
+ tool?.input ??
1216
+ tool?.rawInput ??
1217
+ tool?.arguments ??
1218
+ inner.input ??
1219
+ item?.input ??
1220
+ item?.arguments,
1221
+ ) ?? tool ?? item,
1222
+ id: stringField(tool, "id") ?? stringField(inner, "id") ?? stringField(item, "id"),
1223
+ status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
1224
+ };
1225
+ }
1226
+
1227
+ return null;
1228
+ }
1229
+
1230
+ function extractDeepseekToolResult(raw: any): { name?: string; result: string; id?: string } | null {
1231
+ const payload = raw?.payload;
1232
+ if (!payload || typeof payload !== "object") return null;
1233
+
1234
+ if (raw?.event === "tool.completed") {
1235
+ const result = payload.output ?? payload.result ?? payload.content ?? payload.error ?? payload;
1236
+ return {
1237
+ name: stringField(payload, "name"),
1238
+ result: stringifyToolResult(result),
1239
+ id: stringField(payload, "id"),
1240
+ };
1241
+ }
1242
+
1243
+ if (payload.event === "item.completed" || payload.event === "item.failed") {
1244
+ const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1245
+ const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1246
+ const result =
1247
+ item?.output ??
1248
+ item?.result ??
1249
+ item?.content ??
1250
+ item?.detail ??
1251
+ item?.summary ??
1252
+ item?.error ??
1253
+ inner.output ??
1254
+ inner.result ??
1255
+ inner.error ??
1256
+ item ??
1257
+ inner;
1258
+ return {
1259
+ name: stringField(item, "name") ?? stringField(item, "type") ?? stringField(inner, "name"),
1260
+ result: stringifyToolResult(result),
1261
+ id: stringField(item, "id") ?? stringField(inner, "id"),
1262
+ };
1263
+ }
1264
+
1265
+ return null;
1266
+ }
1267
+
1100
1268
  function formatBlockDetails(raw: unknown): string {
1101
1269
  if (!raw || typeof raw !== "object") return "";
1102
1270
  const r = raw as any;
@@ -1168,6 +1336,47 @@ function codexToolResult(item: Record<string, unknown>): string {
1168
1336
  return parts.join("\n");
1169
1337
  }
1170
1338
 
1339
+ function stringifyToolResult(value: unknown): string {
1340
+ if (value == null) return "";
1341
+ if (typeof value === "string") return value;
1342
+ if (Array.isArray(value)) {
1343
+ return value
1344
+ .map((c: any) => {
1345
+ if (typeof c === "string") return c;
1346
+ if (typeof c?.text === "string") return c.text;
1347
+ return stringifyToolResult(c);
1348
+ })
1349
+ .filter(Boolean)
1350
+ .join("\n");
1351
+ }
1352
+ try {
1353
+ return JSON.stringify(value, null, 2);
1354
+ } catch {
1355
+ return String(value);
1356
+ }
1357
+ }
1358
+
1359
+ function parseMaybeJson(value: unknown): unknown {
1360
+ if (typeof value !== "string") return value;
1361
+ const trimmed = value.trim();
1362
+ if (!trimmed) return value;
1363
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
1364
+ try {
1365
+ return JSON.parse(trimmed);
1366
+ } catch {
1367
+ return value;
1368
+ }
1369
+ }
1370
+
1371
+ function isEmptyRecord(value: unknown): boolean {
1372
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
1373
+ }
1374
+
1375
+ function stringField(obj: any, key: string): string | undefined {
1376
+ const value = obj?.[key];
1377
+ return typeof value === "string" && value.length > 0 ? value : undefined;
1378
+ }
1379
+
1171
1380
  function extractContentText(content: unknown): string {
1172
1381
  if (!content) return "";
1173
1382
  if (typeof content === "string") return content;