Compare commits
10 Commits
1778231a2d
...
26cc61d26f
| Author | SHA1 | Date |
|---|---|---|
|
|
26cc61d26f | |
|
|
542f72d449 | |
|
|
f261e1361c | |
|
|
1d4e7e6469 | |
|
|
34f90534d2 | |
|
|
a829e0b114 | |
|
|
6dc6cf41c4 | |
|
|
729dee2c11 | |
|
|
53e77e0608 | |
|
|
28992f8657 |
267
index.ts
267
index.ts
|
|
@ -1,6 +1,8 @@
|
||||||
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"
|
||||||
|
|
@ -131,6 +133,222 @@ 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
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -208,6 +426,26 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,6 +570,34 @@ 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`)
|
||||||
|
|
@ -343,6 +609,7 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
{
|
{
|
||||||
"name": "opencode-matrix-plugin",
|
"name": "matrix-plugin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "opencode-matrix-plugin",
|
"name": "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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1660,12 +1662,12 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "26.6.2",
|
"version": "20.19.43",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||||
"integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==",
|
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~8.9.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
|
|
@ -1762,6 +1764,30 @@
|
||||||
"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",
|
||||||
|
|
@ -5532,9 +5558,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "8.9.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -10,11 +10,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode/plugin": "^2.0.16",
|
"@opencode/plugin": "^2.0.16",
|
||||||
"matrix-bot-sdk": "^0.8.0",
|
"acorn": "^8.18.0",
|
||||||
"marked": "^15.0.0"
|
"acorn-walk": "^8.3.5",
|
||||||
|
"marked": "^15.0.0",
|
||||||
|
"matrix-bot-sdk": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/node": "^20.0.0"
|
"typescript": "^5.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue