Compare commits

..

No commits in common. "26cc61d26f3558b6998d825fecc2b8665bacf183" and "1778231a2dab90849ed71603777d353044749855" have entirely different histories.

3 changed files with 22 additions and 317 deletions

267
index.ts
View File

@ -1,8 +1,6 @@
import fs from "fs" import fs from "fs"
import os from "os" import os from "os"
import path from "path" import path from "path"
import * as acorn from "acorn"
import * as acornWalk from "acorn-walk"
import { MatrixBotClient, type MatrixEventContext } from "./matrix-client.js" import { MatrixBotClient, type MatrixEventContext } from "./matrix-client.js"
import { SessionManager } from "./session-manager.js" import { SessionManager } from "./session-manager.js"
import { loadConfig } from "./config-loader.js" import { loadConfig } from "./config-loader.js"
@ -133,222 +131,6 @@ async function runPlugin(ctx: any, options: any, dataDir: string) {
const sessionsBySessionID = new Map<string, SessionState>() const sessionsBySessionID = new Map<string, SessionState>()
// =============================================================================
// Parse tool calls from CodeMode JS script using AST (same as OpenCode)
// CodeMode creates nested tools: tools.<server>.<tool>(...)
// e.g. tools.duckduckgo.search({ query: "..." })
// =============================================================================
interface ChildToolInfo {
name: string
args: string
}
// Minimal AST node type (acorn doesn't export AST namespace in usable way)
type AstNode = { type: string; [key: string]: any }
/**
* Parse the CodeMode JS script and extract all tool calls.
* Uses acorn AST parser + walk — same approach as OpenCode's CodeMode interpreter.
* Does NOT execute the code, only parses and analyzes the AST.
*/
function parseCodeModeTools(code: string): ChildToolInfo[] {
const results: ChildToolInfo[] = []
const seen = new Set<string>()
// Parse the code into an AST using acorn (same parser as CodeMode)
const ast = acorn.parse(code, {
ecmaVersion: "latest",
sourceType: "script",
allowReturnOutsideFunction: true,
allowAwaitOutsideFunction: true,
locations: true,
}) as AstNode
// Walk the AST and find all CallExpression nodes for tools.xxx.yyy(...)
acornWalk.simple(ast as any, {
CallExpression(node) {
// Check if callee is tools.<server>.<tool>(...)
const toolCall = extractToolCall(node)
if (!toolCall) return
const { path, args } = toolCall
const name = path.join("_")
if (seen.has(name)) return
seen.add(name)
// Evaluate the arguments to get their string representation
const argsStr = evaluateArgs(args)
results.push({ name, args: argsStr })
},
})
return results
}
/**
* Check if a CallExpression is a tool call: tools.<server>.<tool>(...)
* Returns { path, args } or null.
*/
function extractToolCall(node: AstNode): { path: string[]; args: AstNode[] } | null {
const callee = node.callee as AstNode
// Must be a MemberExpression: tools.xxx.yyy
if (callee.type !== "MemberExpression") return null
if (callee.computed) return null
const prop = callee.property as AstNode
if (prop.type !== "Identifier") return null
const toolName = prop.name
// Object must be tools.xxx (nested MemberExpression) or just "tools"
let obj: AstNode = callee.object
const path: string[] = []
// Walk up the nested MemberExpression chain
while (obj.type === "MemberExpression" && !obj.computed) {
const innerProp = obj.property as AstNode
if (innerProp.type !== "Identifier") return null
path.unshift(innerProp.name)
obj = obj.object
}
// The base must be the identifier "tools"
if (obj.type !== "Identifier" || obj.name !== "tools") return null
path.unshift(toolName)
return { path, args: (node.arguments ?? []) as AstNode[] }
}
/**
* Evaluate arguments to a string representation.
* Handles literals, identifiers, objects, arrays, and template literals.
* Matches how CodeMode's interpreter evaluates expression values.
*/
function evaluateArgs(args: AstNode[]): string {
if (!args || args.length === 0) return ""
const values: string[] = []
for (const arg of args) {
values.push(evaluateExpression(arg))
}
return values.join(", ")
}
/**
* Evaluate an AST expression node to its string value.
* Handles the expression types used in CodeMode tool calls.
*/
function evaluateExpression(node: AstNode): string {
switch (node.type) {
// Literals
case "Literal":
return String(node.value)
// String/number/boolean/null/regexp literals
case "Identifier": {
const name = node.name
// Known globals with known values
if (name === "undefined") return "undefined"
if (name === "true") return "true"
if (name === "false") return "false"
if (name === "null") return "null"
// For variables, show the variable name
return name
}
// Object literals: { key: value, ... }
case "ObjectExpression": {
const parts: string[] = []
for (const prop of node.properties ?? []) {
if (prop.type === "Property") {
const key = prop.key.type === "Identifier" ? prop.key.name : String((prop.key as AstNode).value)
const value = evaluateExpression(prop.value)
parts.push(`${key}: ${value}`)
}
}
return `{ ${parts.join(", ")} }`
}
// Array literals: [value1, value2, ...]
case "ArrayExpression": {
const elements = (node.elements ?? []).map((el: AstNode | null) =>
el ? evaluateExpression(el) : "",
)
return `[${elements.join(", ")}]`
}
// Template literals: `text ${expr} text`
case "TemplateLiteral": {
let result = "`"
const quasis = node.quasis ?? []
const expressions = node.expressions ?? []
for (let i = 0; i < quasis.length; i++) {
result += quasis[i]?.value?.raw ?? ""
if (expressions[i]) {
result += "${" + evaluateExpression(expressions[i]) + "}"
}
}
result += "`"
return result
}
// Binary expressions: a + b, a === b, etc.
case "BinaryExpression": {
const left = evaluateExpression(node.left)
const right = evaluateExpression(node.right)
return `${left} ${node.operator} ${right}`
}
// Conditional: a ? b : c
case "ConditionalExpression": {
return `${evaluateExpression(node.test)} ? ${evaluateExpression(node.consequent)} : ${evaluateExpression(node.alternate)}`
}
// Function expressions (arrow functions, etc.)
case "ArrowFunctionExpression":
case "FunctionExpression":
return "function"
// Call expressions: other().method()
case "CallExpression": {
if (node.callee?.type === "MemberExpression") {
const member = node.callee as AstNode
if (member.object?.type === "Identifier") {
return `${(member.object as AstNode).name}.${(member.property as AstNode)?.name}(${evaluateArgs(node.arguments ?? [])})`
}
}
return "call"
}
// Unary expressions: !a, -b, typeof c
case "UnaryExpression": {
const arg = evaluateExpression(node.argument)
return `${node.operator}${arg}`
}
// Logical expressions: a && b, a || b
case "LogicalExpression": {
return `${evaluateExpression(node.left)} ${node.operator} ${evaluateExpression(node.right)}`
}
// Await expressions
case "AwaitExpression": {
return `await ${evaluateExpression(node.argument)}`
}
// Spread: ...arr
case "SpreadElement": {
return `...${evaluateExpression(node.argument)}`
}
// Default: show "(expr)"
default:
return `(expr:${node.type})`
}
}
// ============================================================================= // =============================================================================
// Event subscription — collects step data and sends messages per step // Event subscription — collects step data and sends messages per step
// ============================================================================= // =============================================================================
@ -426,26 +208,6 @@ async function runPlugin(ctx: any, options: any, dataDir: string) {
const tool = step.tools.find(t => t.toolID === toolID) const tool = step.tools.find(t => t.toolID === toolID)
if (tool) tool.args = args if (tool) tool.args = args
} }
// Parse CodeMode execute tool — extract child tool calls from JS code
if (toolID && step && args) {
let parsedArgs: any
try { parsedArgs = JSON.parse(args) } catch { continue }
const code = parsedArgs?.code || ""
if (code) {
const childTools = parseCodeModeTools(code)
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,
})
}
}
}
}
continue continue
} }
@ -570,34 +332,6 @@ async function runPlugin(ctx: any, options: any, dataDir: string) {
// Tool calls // Tool calls
if (step.tools.length > 0) { if (step.tools.length > 0) {
// 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 (childTools.length > 0) {
// Execute has child tool calls — show children, then execute result
for (const child of childTools) {
parts.push(`<details>\n<summary>🔧 ${child.name}</summary>\n\n`)
parts.push(`<b>Args:</b>\n\`\`\`\n${child.args}\n\`\`\`\n\n`)
if (child.failed) {
parts.push(`<b>Failed:</b> ${child.result}\n\n`)
} else if (child.result) {
parts.push(`<b>Result:</b>\n\`\`\`\n${child.result}\n\`\`\`\n\n`)
}
parts.push(`</details>`)
}
// Execute result as a separate spoiler (like a regular tool result)
if (executeTool?.result) {
parts.push(`<details>\n<summary>🔧 execute</summary>\n\n`)
parts.push(`<b>Result:</b>\n\`\`\`\n${executeTool.result}\n\`\`\`\n\n`)
parts.push(`</details>`)
}
} else {
// No child tool calls — show tools normally (including execute as-is)
for (const tool of step.tools) { for (const tool of step.tools) {
parts.push(`<details>\n<summary>🔧 ${tool.name}</summary>\n\n`) parts.push(`<details>\n<summary>🔧 ${tool.name}</summary>\n\n`)
parts.push(`<b>Args:</b>\n\`\`\`\n${tool.args}\n\`\`\`\n\n`) parts.push(`<b>Args:</b>\n\`\`\`\n${tool.args}\n\`\`\`\n\n`)
@ -609,7 +343,6 @@ async function runPlugin(ctx: any, options: any, dataDir: string) {
parts.push(`</details>`) parts.push(`</details>`)
} }
} }
}
// Separator before text // Separator before text
if (step.text) { if (step.text) {

46
package-lock.json generated
View File

@ -1,21 +1,19 @@
{ {
"name": "matrix-plugin", "name": "opencode-matrix-plugin",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "matrix-plugin", "name": "opencode-matrix-plugin",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
"@opencode/plugin": "^2.0.16", "@opencode/plugin": "^2.0.16",
"acorn": "^8.18.0",
"acorn-walk": "^8.3.5",
"marked": "^15.0.0", "marked": "^15.0.0",
"matrix-bot-sdk": "^0.8.0" "matrix-bot-sdk": "^0.8.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.7.0" "typescript": "^5.7.0"
} }
}, },
@ -1662,12 +1660,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.43", "version": "26.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~8.9.0"
} }
}, },
"node_modules/@types/qs": { "node_modules/@types/qs": {
@ -1764,30 +1762,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/acorn": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": { "node_modules/agent-base": {
"version": "7.1.4", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@ -5558,9 +5532,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.21.0", "version": "8.9.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unpipe": { "node_modules/unpipe": {

View File

@ -10,13 +10,11 @@
}, },
"dependencies": { "dependencies": {
"@opencode/plugin": "^2.0.16", "@opencode/plugin": "^2.0.16",
"acorn": "^8.18.0", "matrix-bot-sdk": "^0.8.0",
"acorn-walk": "^8.3.5", "marked": "^15.0.0"
"marked": "^15.0.0",
"matrix-bot-sdk": "^0.8.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "typescript": "^5.7.0",
"typescript": "^5.7.0" "@types/node": "^20.0.0"
} }
} }