@exreve/exk 1.0.48 → 1.0.50

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.
@@ -99,6 +99,12 @@ function extractToolName(toolResult) {
99
99
  return 'Edit';
100
100
  return (toolResult.content !== undefined || toolResult.type === 'create') ? 'Write' : 'Read';
101
101
  }
102
+ // Grep: {mode, numFiles, filenames, content, numLines}
103
+ if (toolResult.numFiles !== undefined && toolResult.filenames !== undefined && toolResult.mode !== undefined)
104
+ return 'Grep';
105
+ // Glob: {type, pattern, files}
106
+ if (toolResult.files !== undefined && toolResult.pattern !== undefined && !toolResult.stdout)
107
+ return 'Glob';
102
108
  if (toolResult.stdout !== undefined || toolResult.stderr !== undefined)
103
109
  return 'Bash';
104
110
  // send_file tool: content is JSON with _type marker — detect before Bash fallback
@@ -122,7 +128,12 @@ function lookupToolNameFromHistory(messages, toolUseId) {
122
128
  const msg = messages[i];
123
129
  if (msg.role !== 'assistant')
124
130
  continue;
125
- const content = typeof msg.content === 'string' ? null : msg.content;
131
+ // msg.content is the SDK message object: {role: 'assistant', content: [{type: 'text',...}, {type: 'tool_use',...}]}
132
+ let content = typeof msg.content === 'string' ? null : msg.content;
133
+ // Unwrap nested content: {content: [...]} → [...]
134
+ if (content && !Array.isArray(content) && Array.isArray(content.content)) {
135
+ content = content.content;
136
+ }
126
137
  if (!Array.isArray(content))
127
138
  continue;
128
139
  const toolUse = content.find((c) => c.type === 'tool_use' && c.id === toolUseId);
@@ -960,10 +971,14 @@ export class AgentSessionManager {
960
971
  }
961
972
  else if (message.type === 'user') {
962
973
  const msg = message;
963
- // SDK 0.2.x: tool results appear in message.content as tool_result blocks.
964
- // The tool_use_id is ALWAYS in message.content[].tool_use_id never in
965
- // the top-level tool_use_result field. We must extract it from message.content
966
- // first, then use either parsed content blocks or tool_use_result as the data source.
974
+ // SDK sends tool results with TWO data sources:
975
+ // message.content[0] — {type:'tool_result', tool_use_id:'...', content: <raw text>}
976
+ // msg.tool_use_result structured object with {stdout,stderr} for Bash,
977
+ // {type,file} for Read, or [{type:'text',text:'...'}] for MCP tools
978
+ //
979
+ // The tool_use_id lives ONLY in message.content[].tool_use_id.
980
+ // The structured result lives in tool_use_result for built-in tools.
981
+ // For MCP tools, tool_use_result is a content-block array we need to parse.
967
982
  let toolResult = null;
968
983
  let toolUseId = msg.parent_tool_use_id;
969
984
  // STEP 1: Always extract tool_use_id from message.content (authoritative source)
@@ -974,8 +989,41 @@ export class AgentSessionManager {
974
989
  toolUseId = toolResultBlock.tool_use_id;
975
990
  }
976
991
  }
977
- // STEP 2: Try to parse the actual result from message.content tool_result block
978
- if (Array.isArray(msg.message?.content)) {
992
+ // STEP 2: Use tool_use_result as the primary data source.
993
+ // For built-in tools (Bash, Read, Edit, Write, Glob, Grep) it's a structured
994
+ // object like {stdout, stderr} or {type:'text', file:{...}} — use directly.
995
+ // For MCP tools it's a content-block array [{type:'text', text:'...'}] — parse text.
996
+ if (msg.tool_use_result) {
997
+ const raw = msg.tool_use_result;
998
+ if (Array.isArray(raw)) {
999
+ // MCP tool result: [{type:'text', text:'...'}] — extract and parse
1000
+ const textParts = raw
1001
+ .filter((c) => c.type === 'text')
1002
+ .map((c) => c.text);
1003
+ const rawContent = textParts.join('\n');
1004
+ try {
1005
+ toolResult = JSON.parse(rawContent);
1006
+ }
1007
+ catch {
1008
+ toolResult = { content: rawContent, type: 'text' };
1009
+ }
1010
+ }
1011
+ else if (typeof raw === 'object' && raw !== null) {
1012
+ // Built-in tool result: {stdout, stderr, ...} or {type, file, ...} — use directly
1013
+ toolResult = raw;
1014
+ }
1015
+ else if (typeof raw === 'string') {
1016
+ try {
1017
+ toolResult = JSON.parse(raw);
1018
+ }
1019
+ catch {
1020
+ toolResult = { content: raw, type: 'text' };
1021
+ }
1022
+ }
1023
+ }
1024
+ // STEP 3: Fallback to parsing message.content if tool_use_result wasn't available
1025
+ // (e.g. subagent calls where tool_use_result may be absent)
1026
+ if (!toolResult && Array.isArray(msg.message?.content)) {
979
1027
  const contentBlocks = msg.message.content;
980
1028
  const toolResultBlock = contentBlocks.find((c) => c.type === 'tool_result');
981
1029
  if (toolResultBlock) {
@@ -988,7 +1036,6 @@ export class AgentSessionManager {
988
1036
  }
989
1037
  }
990
1038
  else if (Array.isArray(toolResultBlock.content)) {
991
- // Extract text from content blocks and try to parse as JSON
992
1039
  const textParts = toolResultBlock.content
993
1040
  .filter((c) => c.type === 'text')
994
1041
  .map((c) => c.text);
@@ -1005,40 +1052,12 @@ export class AgentSessionManager {
1005
1052
  }
1006
1053
  }
1007
1054
  }
1008
- // STEP 3: Fallback to top-level tool_use_result if message.content didn't yield a result
1009
- // (present in SDK 0.1.x and some 0.2.x messages). Parse the raw content blocks.
1010
- if (!toolResult && msg.tool_use_result) {
1011
- const raw = msg.tool_use_result;
1012
- if (Array.isArray(raw)) {
1013
- // tool_use_result is [{type:"text", text:"..."}] — extract and parse text
1014
- const textParts = raw
1015
- .filter((c) => c.type === 'text')
1016
- .map((c) => c.text);
1017
- const rawContent = textParts.join('\n');
1018
- try {
1019
- toolResult = JSON.parse(rawContent);
1020
- }
1021
- catch {
1022
- toolResult = { content: rawContent, type: 'text' };
1023
- }
1024
- }
1025
- else if (typeof raw === 'string') {
1026
- try {
1027
- toolResult = JSON.parse(raw);
1028
- }
1029
- catch {
1030
- toolResult = { content: raw, type: 'text' };
1031
- }
1032
- }
1033
- else {
1034
- toolResult = raw;
1035
- }
1036
- }
1037
1055
  if (toolResult) {
1056
+ // History lookup is authoritative: the assistant's tool_use block names the tool.
1057
+ // Heuristic detection can't distinguish Bash/Grep/Glob/etc (all have {stdout,stderr}).
1058
+ const historyName = lookupToolNameFromHistory(session.messages, toolUseId);
1038
1059
  const detectedName = extractToolName(toolResult);
1039
- const resolvedName = detectedName !== 'unknown'
1040
- ? detectedName
1041
- : (lookupToolNameFromHistory(session.messages, toolUseId) || detectedName);
1060
+ const resolvedName = historyName || detectedName;
1042
1061
  onOutput({
1043
1062
  type: 'tool_result',
1044
1063
  data: toolResult,
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.48",
3
+ "version": "1.0.50",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,4 +61,4 @@
61
61
  "engines": {
62
62
  "node": ">=20.0.0"
63
63
  }
64
- }
64
+ }