Compare commits
No commits in common. "26cc61d26f3558b6998d825fecc2b8665bacf183" and "1778231a2dab90849ed71603777d353044749855" have entirely different histories.
26cc61d26f
...
1778231a2d
267
index.ts
267
index.ts
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue