diff --git a/index.ts b/index.ts index f7e3d3d..a2a3bca 100644 --- a/index.ts +++ b/index.ts @@ -137,18 +137,77 @@ async function runPlugin(ctx: any, options: any, dataDir: string) { // e.g. tools.duckduckgo.search({ query: "..." }) // ============================================================================= - function parseCodeModeTools(code: string): string[] { - const tools: string[] = [] - // Match tools..( — nested tool calls in CodeMode + interface ChildToolInfo { + name: string + args: string + } + + function parseCodeModeTools(code: string): ChildToolInfo[] { + const results: ChildToolInfo[] = [] + const seen = new Set() const regex = /tools\.(\w+)\.(\w+)\s*\(/g let match + while ((match = regex.exec(code)) !== null) { const toolName = `${match[1]}_${match[2]}` - if (!tools.includes(toolName)) { - tools.push(toolName) + if (seen.has(toolName)) continue + seen.add(toolName) + + // Extract the full argument block starting from the ( + const openParenPos = match.index + match[0].indexOf("(") + const args = extractArgumentBlock(code, openParenPos) + results.push({ name: toolName, args }) + } + + return results + } + + /** + * Extract argument block from code starting at the opening parenthesis. + * Handles nested parentheses, brackets, and braces. + * e.g. for tools.duckduckgo.fetch_content({ url: url, max_length: 5000 }) + * returns "{ url: url, max_length: 5000 }" + */ + function extractArgumentBlock(code: string, openParenPos: number): string { + let depth = 0 + let start = openParenPos + let end = openParenPos + + // Find the opening character: (, {, [, or " + while (start < code.length) { + const ch = code[start] + if (ch === "(" || ch === "{" || ch === "[" || ch === '"') break + start++ + } + + if (start >= code.length) return "(variable)" + + const openCh = code[start] + const closeCh = openCh === "(" ? ")" : openCh === "{" ? "}" : openCh === "[" ? "]" : '"' + + for (let i = start; i < code.length; i++) { + const ch = code[i] + if (ch === openCh) depth++ + else if (ch === closeCh) { + depth-- + if (depth === 0) { + end = i + 1 + break + } + } + // Skip string literals to avoid counting quotes inside strings + if (ch === '"' || ch === "'") { + let j = i + 1 + while (j < code.length && code[j] !== ch) { + if (code[j] === "\\") j++ + j++ + } + i = j } } - return tools + + const result = code.slice(start, end).trim() + return result || "(variable)" } // ============================================================================= @@ -235,9 +294,15 @@ async function runPlugin(ctx: any, options: any, dataDir: string) { const code = parsedArgs?.code || "" if (code) { const childTools = parseCodeModeTools(code) - for (const name of childTools) { - if (!step.tools.find(t => t.name === name)) { - step.tools.push({ toolID: `${toolID}/${name}`, name, args: "", result: "", failed: false }) + for (const child of childTools) { + if (!step.tools.find(t => t.name === child.name)) { + step.tools.push({ + toolID: `${toolID}/${child.name}`, + name: child.name, + args: child.args, + result: "", + failed: false, + }) } } } @@ -366,28 +431,29 @@ async function runPlugin(ctx: any, options: any, dataDir: string) { // Tool calls if (step.tools.length > 0) { - for (const tool of step.tools) { - // Check if this is an execute tool with child tool calls - // Child tools have toolID like "parentID/toolName" - const isExecuteWithChildren = tool.name === "execute" && tool.toolID.includes("/") - const childTools = isExecuteWithChildren ? step.tools.filter(t => t.toolID.startsWith(tool.toolID + "/")) : [] + // Check if any tool is an execute with child tool calls + const executeTool = step.tools.find( + (t) => t.name === "execute" && t.toolID.includes("/"), + ) + const childTools = executeTool + ? step.tools.filter((t) => t.toolID.startsWith(executeTool.toolID + "/")) + : [] - if (isExecuteWithChildren && childTools.length > 0) { - // Expand execute → individual spoilers for each child tool - for (const child of childTools) { - // Extract args from the code for this child tool - const childArgs = extractChildToolArgs(tool.args, child.name) - parts.push(`
\n🔧 ${child.name}\n\n`) - parts.push(`Args:\n\`\`\`\n${childArgs}\n\`\`\`\n\n`) - if (child.failed) { - parts.push(`Failed: ${child.result}\n\n`) - } else if (child.result) { - parts.push(`Result:\n\`\`\`\n${child.result}\n\`\`\`\n\n`) - } - parts.push(`
`) + if (childTools.length > 0) { + // Execute has child tool calls — show only the children, skip execute + for (const child of childTools) { + parts.push(`
\n🔧 ${child.name}\n\n`) + parts.push(`Args:\n\`\`\`\n${child.args}\n\`\`\`\n\n`) + if (child.failed) { + parts.push(`Failed: ${child.result}\n\n`) + } else if (child.result) { + parts.push(`Result:\n\`\`\`\n${child.result}\n\`\`\`\n\n`) } - } else { - // Normal tool (or execute without children) + parts.push(`
`) + } + } else { + // No child tool calls — show tools normally (including execute as-is) + for (const tool of step.tools) { parts.push(`
\n🔧 ${tool.name}\n\n`) parts.push(`Args:\n\`\`\`\n${tool.args}\n\`\`\`\n\n`) if (tool.failed) { @@ -409,28 +475,6 @@ async function runPlugin(ctx: any, options: any, dataDir: string) { return parts.join("\n") } - // Extract args for a child tool from the execute code - function extractChildToolArgs(code: string, toolName: string): string { - try { - const args = JSON.parse(code) - const codeStr = args?.code || "" - // Match tools..(...) and extract the arguments - const regex = new RegExp(`tools\\.\\w+\\.${escapeRegex(toolName.replace("_", "."))}\\s*\\(([^)]*)\\)`, "g") - const matches: string[] = [] - let match - while ((match = regex.exec(codeStr)) !== null) { - matches.push(match[1].trim()) - } - return matches.join("\n") || "(parsed from code)" - } catch { - return "(parsed from code)" - } - } - - function escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") - } - // ============================================================================= // Send message to Matrix (reuses existing sendReply/sendNotice) // =============================================================================