diff --git a/bun.lock b/bun.lock index 273f7fa..c343eef 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ }, "packages/cli": { "name": "@utcp/cli", - "version": "1.0.6", + "version": "1.0.12", "dependencies": { "@utcp/sdk": "^1.0.4", }, @@ -27,9 +27,22 @@ "typescript": "^5.0.0", }, }, + "packages/code-mode": { + "name": "@utcp/code-mode", + "version": "1.0.0", + "dependencies": { + "@utcp/sdk": "^1.0.15", + "vm2": "^3.9.19", + }, + "devDependencies": { + "@types/node": "^20.0.0", + "bun-types": "latest", + "typescript": "^5.0.0", + }, + }, "packages/core": { "name": "@utcp/sdk", - "version": "1.0.8", + "version": "1.0.16", "dependencies": { "dotenv": "^17.2.1", "zod": "^3.23.8", @@ -41,7 +54,7 @@ }, "packages/direct-call": { "name": "@utcp/direct-call", - "version": "1.0.1", + "version": "1.0.12", "dependencies": { "@utcp/sdk": "^1.0.4", }, @@ -53,7 +66,7 @@ }, "packages/dotenv-loader": { "name": "@utcp/dotenv-loader", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "dotenv": "^17.2.1", "zod": "^3.23.8", @@ -68,7 +81,7 @@ }, "packages/file": { "name": "@utcp/file", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@utcp/http": "^1.0.7", "@utcp/sdk": "^1.0.8", @@ -82,7 +95,7 @@ }, "packages/http": { "name": "@utcp/http", - "version": "1.0.8", + "version": "1.0.13", "dependencies": { "@utcp/sdk": "^1.0.6", "axios": "^1.11.0", @@ -96,7 +109,7 @@ }, "packages/mcp": { "name": "@utcp/mcp", - "version": "1.0.6", + "version": "1.0.12", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.4", "@utcp/sdk": "^1.0.4", @@ -111,7 +124,7 @@ }, "packages/text": { "name": "@utcp/text", - "version": "1.0.11", + "version": "1.0.12", "dependencies": { "@utcp/http": "^1.0.8", "@utcp/sdk": "^1.0.8", @@ -251,12 +264,14 @@ "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "@types/node": ["@types/node@20.19.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA=="], "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], "@utcp/cli": ["@utcp/cli@workspace:packages/cli"], + "@utcp/code-mode": ["@utcp/code-mode@workspace:packages/code-mode"], + "@utcp/direct-call": ["@utcp/direct-call@workspace:packages/direct-call"], "@utcp/dotenv-loader": ["@utcp/dotenv-loader@workspace:packages/dotenv-loader"], @@ -275,6 +290,8 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -595,7 +612,7 @@ "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], @@ -603,6 +620,8 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vm2": ["vm2@3.10.0", "", { "dependencies": { "acorn": "^8.14.1", "acorn-walk": "^8.3.4" }, "bin": { "vm2": "bin/vm2" } }, "sha512-3ggF4Bs0cw4M7Rxn19/Cv3nJi04xrgHwt4uLto+zkcZocaKwP/nKP9wPx6ggN2X0DSXxOOIc63BV1jvES19wXQ=="], + "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], "whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], @@ -623,6 +642,8 @@ "@utcp/cli/bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + "@utcp/code-mode/bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "@utcp/direct-call/bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], "@utcp/dotenv-loader/bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], @@ -639,6 +660,8 @@ "accepts/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], @@ -657,8 +680,30 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@types/bun/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@utcp/cli/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@utcp/code-mode/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@utcp/direct-call/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@utcp/dotenv-loader/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@utcp/file/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@utcp/http/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@utcp/mcp/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@utcp/sdk/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@utcp/text/bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -670,5 +715,25 @@ "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@utcp/cli/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@utcp/code-mode/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@utcp/direct-call/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@utcp/dotenv-loader/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@utcp/file/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@utcp/http/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@utcp/mcp/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@utcp/sdk/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@utcp/text/bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], } } diff --git a/package.json b/package.json index b682b5f..20b5409 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "scripts": { "clean": "rm -rf packages/*/dist || rmdir /s /q packages\\*\\dist || true", - "build": "bun run build:core && bun run build:http && bun run build:mcp && bun run build:text && bun run build:file && bun run build:cli && bun run build:direct-call && bun run build:dotenv-loader", + "build": "bun run build:core && bun run build:http && bun run build:mcp && bun run build:text && bun run build:file && bun run build:cli && bun run build:direct-call && bun run build:dotenv-loader && bun run build:code-mode", "build:core": "cd packages/core && bun run build", "build:http": "cd packages/http && bun run build", "build:mcp": "cd packages/mcp && bun run build", @@ -27,6 +27,7 @@ "build:cli": "cd packages/cli && bun run build", "build:direct-call": "cd packages/direct-call && bun run build", "build:dotenv-loader": "cd packages/dotenv-loader && bun run build", + "build:code-mode": "cd packages/code-mode && bun run build", "rebuild": "bun run clean && bun run build", "test": "bun test --config tsconfig.test.json", "version:patch": "node scripts/update-versions.js patch", @@ -41,7 +42,8 @@ "publish:cli": "cd packages/cli && npm publish", "publish:direct-call": "cd packages/direct-call && npm publish", "publish:dotenv-loader": "cd packages/dotenv-loader && npm publish", - "publish:all": "bun run build && bun run publish:core && bun run publish:text && bun run publish:file && bun run publish:http && bun run publish:mcp && bun run publish:cli && bun run publish:direct-call && bun run publish:dotenv-loader" + "publish:code-mode": "cd packages/code-mode && npm publish", + "publish:all": "bun run build && bun run publish:core && bun run publish:text && bun run publish:file && bun run publish:http && bun run publish:mcp && bun run publish:cli && bun run publish:direct-call && bun run publish:dotenv-loader && bun run publish:code-mode" }, "workspaces": [ "packages/*" diff --git a/packages/code-mode/.gitignore b/packages/code-mode/.gitignore new file mode 100644 index 0000000..3c25e1e --- /dev/null +++ b/packages/code-mode/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.log diff --git a/packages/code-mode/README.md b/packages/code-mode/README.md new file mode 100644 index 0000000..6e35c4b --- /dev/null +++ b/packages/code-mode/README.md @@ -0,0 +1,337 @@ +# @utcp/code-mode + +A powerful extension for UTCP that enables executing TypeScript code with direct access to all registered tools as native TypeScript functions. + +## Features + +- **TypeScript Code Execution**: Execute TypeScript code snippets with full access to registered tools +- **Hierarchical Tool Access**: Tools organized by manual namespace (e.g., `math_tools.add()`) +- **Hierarchical Type Definitions**: TypeScript interfaces organized in namespaces matching tool structure +- **Runtime Interface Access**: Access TypeScript interfaces at runtime for introspection +- **Type Safety**: Generates proper TypeScript interfaces for all tool inputs and outputs +- **Secure Execution**: Uses Node.js VM module for safe code execution with timeout support +- **Chain Tool Calls**: Combine multiple tool calls in a single TypeScript code block + +## Installation + +```bash +npm install @utcp/code-mode +``` + +## Basic Usage + +```typescript +import { CodeModeUtcpClient } from '@utcp/code-mode'; + +const client = await CodeModeUtcpClient.create(); + +// Register some tools first (example) +await client.registerManual({ + name: 'math_tools', + call_template_type: 'text', + content: ` + name: add + description: Adds two numbers + inputs: + type: object + properties: + a: { type: number } + b: { type: number } + required: [a, b] + outputs: + type: object + properties: + result: { type: number } + ` +}); + +// Now execute TypeScript code that uses the tools +const { result, logs } = await client.callToolChain(` + // Call the add tool using hierarchical access + const sum1 = await math_tools.add({ a: 5, b: 3 }); + console.log('First sum:', sum1.result); + + // Chain multiple tool calls + const sum2 = await math_tools.add({ a: sum1.result, b: 10 }); + console.log('Second sum:', sum2.result); + + // Access TypeScript interfaces at runtime + const addInterface = __getToolInterface('math_tools.add'); + console.log('Add tool interface:', addInterface); + + // Return final result + return sum2.result; +`); + +console.log('Final result:', result); // 18 +console.log('Console output:', logs); // ['First sum: 8', 'Second sum: 18', 'Add tool interface: ...'] +``` + +## Advanced Usage + +### Console Output Capture + +All console output is automatically captured and returned alongside execution results: + +```typescript +const { result, logs } = await client.callToolChain(` + console.log('Starting calculation...'); + console.error('This will show as [ERROR]'); + console.warn('This will show as [WARN]'); + + const sum1 = await math_tools.add({ a: 5, b: 3 }); + console.log('First sum:', sum1.result); + + const sum2 = await math_tools.add({ a: sum1.result, b: 10 }); + console.log('Final sum:', sum2.result); + + return sum2.result; +`); + +console.log('Result:', result); // 18 +console.log('Captured logs:'); +logs.forEach((log, i) => console.log(`${i + 1}: ${log}`)); +// Output: +// 1: Starting calculation... +// 2: [ERROR] This will show as [ERROR] +// 3: [WARN] This will show as [WARN] +// 4: First sum: 8 +// 5: Final sum: 18 +``` + +### Getting TypeScript Interfaces + +You can generate TypeScript interfaces for all your tools to get better IDE support: + +```typescript +const interfaces = await client.getAllToolsTypeScriptInterfaces(); +console.log(interfaces); +``` + +This will output something like: + +```typescript +// Auto-generated TypeScript interfaces for UTCP tools + +namespace math_tools { + interface addInput { + /** First number */ + a: number; + /** Second number */ + b: number; + } + + interface addOutput { + /** The sum result */ + result: number; + } +} + +/** + * Adds two numbers + * Tags: math, arithmetic + * Access as: math_tools.add(args) + */ +``` + +### Complex Tool Chains + +Execute complex logic with multiple tools using hierarchical access: + +```typescript +const result = await client.callToolChain(` + // Get user data (assuming 'user_service' manual) + const user = await user_service.getUserData({ userId: "123" }); + + // Process the data (assuming 'data_processing' manual) + const processedData = await data_processing.processUserData({ + userData: user, + options: { normalize: true, validate: true } + }); + + // Generate report (assuming 'reporting' manual) + const report = await reporting.generateReport({ + data: processedData, + format: "json", + includeMetrics: true + }); + + // Send notification (assuming 'notifications' manual) + await notifications.sendNotification({ + recipient: user.email, + subject: "Your report is ready", + body: \`Report generated with \${report.metrics.totalItems} items\` + }); + + return { + reportId: report.id, + itemCount: report.metrics.totalItems, + notificationSent: true + }; +`); +``` + +### Error Handling + +The code execution includes proper error handling: + +```typescript +try { + const result = await client.callToolChain(` + const result = await someToolThatMightFail({ input: "test" }); + return result; + `); +} catch (error) { + console.error('Code execution failed:', error.message); +} +``` + +### Timeout Configuration + +You can set custom timeouts for code execution: + +```typescript +const result = await client.callToolChain(` + // Long running operation + const result = await processLargeDataset({ data: largeArray }); + return result; +`, 60000); // 60 second timeout +``` + +### Runtime Interface Access + +The code execution context provides access to TypeScript interfaces at runtime: + +```typescript +const result = await client.callToolChain(` + // Access all interfaces + console.log('All interfaces:', __interfaces); + + // Get interface for a specific tool + const addInterface = __getToolInterface('math_tools.add'); + console.log('Add tool interface:', addInterface); + + // Parse interface information + const hasNamespaces = __interfaces.includes('namespace math_tools'); + const availableNamespaces = __interfaces.match(/namespace \\w+/g) || []; + + // Use this for dynamic validation, documentation, or debugging + return { + hasInterfaces: typeof __interfaces === 'string', + namespaceCount: availableNamespaces.length, + canIntrospect: typeof __getToolInterface === 'function', + specificToolInterface: !!addInterface + }; +`); +``` + +#### Available Context Variables + +- **`__interfaces`**: String containing all TypeScript interface definitions +- **`__getToolInterface(toolName: string)`**: Function to get interface for a specific tool + +## AI Agent Integration + +For AI agents that will use CodeModeUtcpClient, include the built-in prompt template in your system prompt: + +```typescript +import { CodeModeUtcpClient } from '@utcp/code-mode'; + +// Add this to your AI agent's system prompt +const systemPrompt = ` +You are an AI assistant with access to tools via UTCP CodeMode. + +${CodeModeUtcpClient.AGENT_PROMPT_TEMPLATE} + +Additional instructions... +`; +``` + +This template provides essential guidance on: +- **Tool Discovery Workflow**: How to explore available tools before coding +- **Hierarchical Access Patterns**: Using `manual.tool()` syntax correctly +- **Interface Introspection**: Leveraging `__interfaces` and `__getToolInterface()` +- **Best Practices**: Error handling, data flow, and proper code structure +- **Runtime Context**: Available variables and functions in the execution environment + +## API Reference + +### CodeModeUtcpClient + +Extends `UtcpClient` with additional code execution capabilities. + +#### Methods + +##### `callToolChain(code: string, timeout?: number): Promise<{result: any, logs: string[]}>` + +Executes TypeScript code with access to all registered tools and captures console output. + +- **code**: TypeScript code to execute +- **timeout**: Optional timeout in milliseconds (default: 30000) +- **Returns**: Object containing both the execution result and captured console logs (`console.log`, `console.error`, `console.warn`, `console.info`) + +##### `toolToTypeScriptInterface(tool: Tool): string` + +Converts a single tool to its TypeScript interface definition. + +- **tool**: The Tool object to convert +- **Returns**: TypeScript interface as a string + +##### `getAllToolsTypeScriptInterfaces(): Promise` + +Generates TypeScript interfaces for all registered tools. + +- **Returns**: Complete TypeScript interface definitions + +### Static Properties + +##### `CodeModeUtcpClient.AGENT_PROMPT_TEMPLATE: string` + +A comprehensive prompt template designed for AI agents using CodeModeUtcpClient. Contains detailed guidance on tool discovery, hierarchical access patterns, interface introspection, and best practices for code execution. + +### Static Methods + +##### `CodeModeUtcpClient.create(root_dir?: string, config?: UtcpClientConfig): Promise` + +Creates a new CodeModeUtcpClient instance. + +- **root_dir**: Root directory for relative path resolution +- **config**: UTCP client configuration +- **Returns**: New CodeModeUtcpClient instance + +## Security Considerations + +- Code execution happens in a secure Node.js VM context +- No access to Node.js modules or filesystem by default +- Timeout protection prevents infinite loops +- Only registered tools are accessible in the execution context + +## Type Safety + +The code mode client generates hierarchical TypeScript interfaces for all tools, providing: + +- **Namespace Organization**: Tools grouped by manual (e.g., `namespace math_tools`) +- **Hierarchical Access**: Clean dot notation (`math_tools.add()`) prevents naming conflicts +- **Compile-time Type Checking**: Full type safety for tool parameters and return values +- **IntelliSense Support**: Enhanced IDE autocompletion with organized namespaces +- **Runtime Introspection**: Access interface definitions during code execution +- **Self-Documenting Code**: Generated interfaces include descriptions and access patterns + +## Integration with IDEs + +For the best development experience: + +1. Generate TypeScript interfaces for your tools +2. Save them to a `.d.ts` file in your project +3. Reference the file in your TypeScript configuration +4. Enjoy full IntelliSense support for tool functions + +```typescript +// Generate and save interfaces +const interfaces = await client.getAllToolsTypeScriptInterfaces(); +await fs.writeFile('tools.d.ts', interfaces); +``` + +## License + +MPL-2.0 diff --git a/packages/code-mode/package.json b/packages/code-mode/package.json new file mode 100644 index 0000000..3f627df --- /dev/null +++ b/packages/code-mode/package.json @@ -0,0 +1,51 @@ +{ + "name": "@utcp/code-mode", + "version": "1.0.4", + "description": "Code execution mode for UTCP - enables executing TypeScript code chains with tool access", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "license": "MPL-2.0", + "author": "UTCP Contributors", + "repository": { + "type": "git", + "url": "https://github.com/universal-tool-calling-protocol/typescript-utcp.git", + "directory": "packages/code-mode" + }, + "keywords": [ + "utcp", + "code", + "typescript", + "execution", + "tool calling", + "chain" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsup", + "test": "bun test" + }, + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "default": "./dist/index.js" + } + }, + "dependencies": { + "@utcp/sdk": "^1.0.17" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@utcp/direct-call": "^1.0.12", + "bun-types": "latest", + "typescript": "^5.0.0" + } +} diff --git a/packages/code-mode/src/code_mode_utcp_client.ts b/packages/code-mode/src/code_mode_utcp_client.ts new file mode 100644 index 0000000..98f1a43 --- /dev/null +++ b/packages/code-mode/src/code_mode_utcp_client.ts @@ -0,0 +1,496 @@ +import { UtcpClient, Tool, JsonSchema, UtcpClientConfig } from '@utcp/sdk'; +import { createContext, runInContext } from 'vm'; + +/** + * CodeModeUtcpClient extends UtcpClient to provide TypeScript code execution capabilities. + * This allows executing TypeScript code that can directly call registered tools as functions. + */ +export class CodeModeUtcpClient extends UtcpClient { + private toolFunctionCache: Map = new Map(); + + /** + * Standard prompt template for AI agents using CodeModeUtcpClient. + * This provides guidance on how to properly discover and use tools within code execution. + */ + public static readonly AGENT_PROMPT_TEMPLATE = ` +## UTCP CodeMode Tool Usage Guide + +You have access to a CodeModeUtcpClient that allows you to execute TypeScript code with access to registered tools. Follow this workflow: + +### 1. Tool Discovery Phase +**Always start by discovering available tools:** +- Tools are organized by manual namespace (e.g., \`manual_name.tool_name\`) +- Use hierarchical access patterns: \`await manual.tool({ param: value })\` +- Multiple manuals can contain tools with the same name - namespaces prevent conflicts + +### 2. Interface Introspection +**Understand tool contracts before using them:** +- Access \`__interfaces\` to see all available TypeScript interface definitions +- Use \`__getToolInterface('manual.tool')\` to get specific tool interfaces +- Interfaces show required inputs, expected outputs, and descriptions +- Look for "Access as: manual.tool(args)" comments for usage patterns + +### 3. Code Execution Guidelines +**When writing code for \`callToolChain\`:** +- Use \`await manual.tool({ param: value })\` syntax for all tool calls +- Tools are async functions that return promises +- You have access to standard JavaScript globals: \`console\`, \`JSON\`, \`Math\`, \`Date\`, etc. +- All console output (\`console.log\`, \`console.error\`, etc.) is automatically captured and returned +- Build properly structured input objects based on interface definitions +- Handle errors appropriately with try/catch blocks +- Chain tool calls by using results from previous calls + +### 4. Best Practices +- **Discover first, code second**: Always explore available tools before writing execution code +- **Respect namespaces**: Use full \`manual.tool\` names to avoid conflicts +- **Parse interfaces**: Use interface information to construct proper input objects +- **Error handling**: Wrap tool calls in try/catch for robustness +- **Data flow**: Chain tools by passing outputs as inputs to subsequent tools + +### 5. Available Runtime Context +- \`__interfaces\`: String containing all TypeScript interface definitions +- \`__getToolInterface(toolName)\`: Function to get specific tool interface +- All registered tools as \`manual.tool\` functions +- Standard JavaScript built-ins for data processing + +Remember: Always discover and understand available tools before attempting to use them in code execution. +`.trim(); + + /** + * Creates a new CodeModeUtcpClient instance. + * This creates a regular UtcpClient and then upgrades it to a CodeModeUtcpClient + * with all the same configuration and additional code execution capabilities. + * + * @param root_dir The root directory for the client to resolve relative paths from + * @param config The configuration for the client + * @returns A new CodeModeUtcpClient instance + */ + public static async create( + root_dir: string = process.cwd(), + config: UtcpClientConfig | null = null + ): Promise { + // Create a regular UtcpClient first + const baseClient = await UtcpClient.create(root_dir, config); + + // Create a CodeModeUtcpClient using the same configuration + const codeModeClient = Object.setPrototypeOf(baseClient, CodeModeUtcpClient.prototype) as CodeModeUtcpClient; + + // Initialize the cache + (codeModeClient as any).toolFunctionCache = new Map(); + + return codeModeClient; + } + + /** + * Converts a Tool object into a TypeScript function interface string. + * This generates the function signature that can be used in TypeScript code. + * + * @param tool The Tool object to convert + * @returns TypeScript function interface as a string + */ + public toolToTypeScriptInterface(tool: Tool): string { + if (this.toolFunctionCache.has(tool.name)) { + return this.toolFunctionCache.get(tool.name)!; + } + + // Generate hierarchical interface structure + let interfaceContent: string; + let accessPattern: string; + + if (tool.name.includes('.')) { + const [manualName, ...toolParts] = tool.name.split('.'); + const toolName = toolParts.join('_'); + accessPattern = `${manualName}.${toolName}`; + + // Generate interfaces within namespace + const inputInterfaceContent = this.jsonSchemaToObjectContent(tool.inputs); + const outputInterfaceContent = this.jsonSchemaToObjectContent(tool.outputs); + + interfaceContent = ` +namespace ${manualName} { + interface ${toolName}Input { +${inputInterfaceContent} + } + + interface ${toolName}Output { +${outputInterfaceContent} + } +}`; + } else { + // No manual namespace, generate flat interfaces + accessPattern = tool.name; + const inputType = this.jsonSchemaToTypeScript(tool.inputs, `${tool.name}Input`); + const outputType = this.jsonSchemaToTypeScript(tool.outputs, `${tool.name}Output`); + interfaceContent = `${inputType}\n\n${outputType}`; + } + const interfaceString = ` +${interfaceContent} + +/** + * ${tool.description} + * Tags: ${tool.tags.join(', ')} + * Access as: ${accessPattern}(args) + */`; + + this.toolFunctionCache.set(tool.name, interfaceString); + return interfaceString; + } + + /** + * Converts all registered tools to TypeScript interface definitions. + * This provides the complete type definitions for all available tools. + * + * @returns A complete TypeScript interface definition string + */ + public async getAllToolsTypeScriptInterfaces(): Promise { + const tools = await this.getTools(); + const interfaces = tools.map(tool => this.toolToTypeScriptInterface(tool)); + + return `// Auto-generated TypeScript interfaces for UTCP tools +${interfaces.join('\n\n')}`; + } + + /** + * Executes TypeScript code with access to registered tools and captures console output. + * The code can call tools directly as functions and has access to standard JavaScript globals. + * + * @param code TypeScript code to execute + * @param timeout Optional timeout in milliseconds (default: 30000) + * @returns Object containing both the execution result and captured console logs + */ + public async callToolChain(code: string, timeout: number = 30000): Promise<{result: any, logs: string[]}> { + const tools = await this.getTools(); + + // Create the execution context with tool functions and log capture + const logs: string[] = []; + const context = await this.createExecutionContext(tools, logs); + + try { + // Create VM context + const vmContext = createContext(context); + + // Wrap the user code in an async function and execute it + const wrappedCode = ` + (async () => { + ${code} + })() + `; + + // Execute with timeout + const result = await this.runWithTimeout(wrappedCode, vmContext, timeout); + return { result, logs }; + } catch (error) { + throw new Error(`Code execution failed: ${error instanceof Error ? error.message : String(error)}`); + } + } + + /** + * Runs code in VM context with timeout support. + * + * @param code Code to execute + * @param context VM context + * @param timeout Timeout in milliseconds + * @returns Execution result + */ + private async runWithTimeout(code: string, context: any, timeout: number): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error(`Code execution timed out after ${timeout}ms`)); + }, timeout); + + try { + const result = runInContext(code, context); + + // Handle both sync and async results + Promise.resolve(result) + .then(finalResult => { + clearTimeout(timeoutId); + resolve(finalResult); + }) + .catch(error => { + clearTimeout(timeoutId); + reject(error); + }); + } catch (error) { + clearTimeout(timeoutId); + reject(error); + } + }); + } + + /** + * Creates the execution context for running TypeScript code. + * This context includes tool functions and basic JavaScript globals. + * + * @param tools Array of tools to make available + * @param logs Optional array to capture console.log output + * @returns Execution context object + */ + private async createExecutionContext(tools: Tool[], logs?: string[]): Promise> { + // Create console object (either capturing logs or using standard console) + const consoleObj = logs ? { + log: (...args: any[]) => { + logs.push(args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) + ).join(' ')); + }, + error: (...args: any[]) => { + logs.push('[ERROR] ' + args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) + ).join(' ')); + }, + warn: (...args: any[]) => { + logs.push('[WARN] ' + args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) + ).join(' ')); + }, + info: (...args: any[]) => { + logs.push('[INFO] ' + args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) + ).join(' ')); + } + } : console; + + const context: Record = { + // Add basic utilities + console: consoleObj, + JSON, + Promise, + Array, + Object, + String, + Number, + Boolean, + Math, + Date, + + // Add TypeScript interface definitions for reference + __interfaces: await this.getAllToolsTypeScriptInterfaces(), + __getToolInterface: (toolName: string) => { + const tool = tools.find(t => t.name === toolName); + return tool ? this.toolToTypeScriptInterface(tool) : null; + } + }; + + // Add tool functions to context organized by manual name + for (const tool of tools) { + if (tool.name.includes('.')) { + const [manualName, ...toolParts] = tool.name.split('.'); + const toolName = toolParts.join('_'); // Join remaining parts with underscore + + // Create manual namespace object if it doesn't exist + if (!context[manualName]) { + context[manualName] = {}; + } + + // Add the tool function to the manual namespace + context[manualName][toolName] = async (args: Record) => { + try { + return await this.callTool(tool.name, args); + } catch (error) { + throw new Error(`Error calling tool '${tool.name}': ${error instanceof Error ? error.message : String(error)}`); + } + }; + } else { + // If no dot, add directly to root context (no manual name) + context[tool.name] = async (args: Record) => { + try { + return await this.callTool(tool.name, args); + } catch (error) { + throw new Error(`Error calling tool '${tool.name}': ${error instanceof Error ? error.message : String(error)}`); + } + }; + } + } + + return context; + } + + /** + * Converts a JSON Schema to TypeScript object content (properties only, no interface wrapper). + * This generates the content inside an interface definition. + * + * @param schema JSON Schema to convert + * @returns TypeScript interface properties as string + */ + private jsonSchemaToObjectContent(schema: JsonSchema): string { + if (!schema || typeof schema !== 'object' || schema.type !== 'object') { + return ' [key: string]: any;'; + } + + const properties = schema.properties || {}; + const required = schema.required || []; + const lines: string[] = []; + + for (const [propName, propSchema] of Object.entries(properties)) { + const isRequired = required.includes(propName); + const optionalMarker = isRequired ? '' : '?'; + const description = (propSchema as any).description || ''; + const tsType = this.jsonSchemaToTypeScriptType(propSchema as JsonSchema); + + if (description) { + lines.push(` /** ${description} */`); + } + lines.push(` ${propName}${optionalMarker}: ${tsType};`); + } + + return lines.length > 0 ? lines.join('\n') : ' [key: string]: any;'; + } + + /** + * Converts a JSON Schema to TypeScript interface definition. + * This handles the most common JSON Schema patterns used in UTCP tools. + * + * @param schema JSON Schema to convert + * @param typeName Name for the generated TypeScript type + * @returns TypeScript type definition as string + */ + private jsonSchemaToTypeScript(schema: JsonSchema, typeName: string): string { + if (!schema || typeof schema !== 'object') { + return `type ${typeName} = any;`; + } + + // Handle different schema types + switch (schema.type) { + case 'object': + return this.objectSchemaToTypeScript(schema, typeName); + case 'array': + return this.arraySchemaToTypeScript(schema, typeName); + case 'string': + return this.primitiveSchemaToTypeScript(schema, typeName, 'string'); + case 'number': + case 'integer': + return this.primitiveSchemaToTypeScript(schema, typeName, 'number'); + case 'boolean': + return this.primitiveSchemaToTypeScript(schema, typeName, 'boolean'); + case 'null': + return `type ${typeName} = null;`; + default: + // Handle union types or fallback to any + if (Array.isArray(schema.type)) { + const types = schema.type.map(t => this.mapJsonTypeToTS(t)).join(' | '); + return `type ${typeName} = ${types};`; + } + return `type ${typeName} = any;`; + } + } + + /** + * Converts an object JSON Schema to TypeScript interface. + */ + private objectSchemaToTypeScript(schema: JsonSchema, typeName: string): string { + if (!schema.properties) { + return `interface ${typeName} { + [key: string]: any; +}`; + } + + const properties = Object.entries(schema.properties).map(([key, propSchema]) => { + const isRequired = schema.required?.includes(key) ?? false; + const optional = isRequired ? '' : '?'; + const propType = this.jsonSchemaToTypeScriptType(propSchema); + const description = propSchema.description ? ` /** ${propSchema.description} */\n` : ''; + + return `${description} ${key}${optional}: ${propType};`; + }).join('\n'); + + return `interface ${typeName} { +${properties} +}`; + } + + /** + * Converts an array JSON Schema to TypeScript type. + */ + private arraySchemaToTypeScript(schema: JsonSchema, typeName: string): string { + if (!schema.items) { + return `type ${typeName} = any[];`; + } + + const itemType = Array.isArray(schema.items) + ? schema.items.map(item => this.jsonSchemaToTypeScriptType(item)).join(' | ') + : this.jsonSchemaToTypeScriptType(schema.items); + + return `type ${typeName} = (${itemType})[];`; + } + + /** + * Converts a primitive JSON Schema to TypeScript type with enum support. + */ + private primitiveSchemaToTypeScript(schema: JsonSchema, typeName: string, baseType: string): string { + if (schema.enum) { + const enumValues = schema.enum.map(val => + typeof val === 'string' ? `"${val}"` : String(val) + ).join(' | '); + return `type ${typeName} = ${enumValues};`; + } + + return `type ${typeName} = ${baseType};`; + } + + /** + * Converts a JSON Schema to a TypeScript type (not a full type definition). + */ + private jsonSchemaToTypeScriptType(schema: JsonSchema): string { + if (!schema || typeof schema !== 'object') { + return 'any'; + } + + if (schema.enum) { + return schema.enum.map(val => + typeof val === 'string' ? `"${val}"` : String(val) + ).join(' | '); + } + + switch (schema.type) { + case 'object': + if (!schema.properties) return '{ [key: string]: any }'; + const props = Object.entries(schema.properties).map(([key, propSchema]) => { + const isRequired = schema.required?.includes(key) ?? false; + const optional = isRequired ? '' : '?'; + const propType = this.jsonSchemaToTypeScriptType(propSchema); + return `${key}${optional}: ${propType}`; + }).join('; '); + return `{ ${props} }`; + + case 'array': + if (!schema.items) return 'any[]'; + const itemType = Array.isArray(schema.items) + ? schema.items.map(item => this.jsonSchemaToTypeScriptType(item)).join(' | ') + : this.jsonSchemaToTypeScriptType(schema.items); + return `(${itemType})[]`; + + case 'string': + return 'string'; + case 'number': + case 'integer': + return 'number'; + case 'boolean': + return 'boolean'; + case 'null': + return 'null'; + + default: + if (Array.isArray(schema.type)) { + return schema.type.map(t => this.mapJsonTypeToTS(t)).join(' | '); + } + return 'any'; + } + } + + /** + * Maps basic JSON Schema types to TypeScript types. + */ + private mapJsonTypeToTS(type: string): string { + switch (type) { + case 'string': return 'string'; + case 'number': + case 'integer': return 'number'; + case 'boolean': return 'boolean'; + case 'null': return 'null'; + case 'object': return 'object'; + case 'array': return 'any[]'; + default: return 'any'; + } + } +} diff --git a/packages/code-mode/src/index.ts b/packages/code-mode/src/index.ts new file mode 100644 index 0000000..af84095 --- /dev/null +++ b/packages/code-mode/src/index.ts @@ -0,0 +1,14 @@ +/** + * Code Mode plugin for UTCP. + * Enables TypeScript code execution with direct access to registered tools. + */ + +// Export all public APIs +export { CodeModeUtcpClient } from './code_mode_utcp_client'; + +// Type exports for better TypeScript support +export type { + Tool, + JsonSchema, + UtcpClientConfig, +} from '@utcp/sdk'; diff --git a/packages/code-mode/tests/code_mode_utcp_client.test.ts b/packages/code-mode/tests/code_mode_utcp_client.test.ts new file mode 100644 index 0000000..e79d72b --- /dev/null +++ b/packages/code-mode/tests/code_mode_utcp_client.test.ts @@ -0,0 +1,640 @@ +/** + * Tests for CodeModeUtcpClient + * This validates the code mode functionality using direct-call tools + */ + +import { test, expect, describe, beforeAll, afterAll } from 'bun:test'; +import { CodeModeUtcpClient } from '../src/index'; +import { addFunctionToUtcpDirectCall } from '@utcp/direct-call'; + +// Test utility functions +const testResults: Record = {}; + +// Setup test tools using direct-call +beforeAll(async () => { + // Register test functions as direct-call tools + + // Simple math function + addFunctionToUtcpDirectCall('add', async (a: number, b: number) => { + testResults.addCalled = { a, b, timestamp: Date.now() }; + return { result: a + b, operation: 'addition' }; + }); + + // String manipulation function + addFunctionToUtcpDirectCall('greet', async (name: string, formal: boolean = false) => { + testResults.greetCalled = { name, formal, timestamp: Date.now() }; + const greeting = formal ? `Good day, ${name}` : `Hey ${name}!`; + return { greeting, isFormal: formal }; + }); + + // Complex object handling function + addFunctionToUtcpDirectCall('processData', async (data: any, options: any = {}) => { + testResults.processDataCalled = { data, options, timestamp: Date.now() }; + return { + processedData: { + ...data, + processed: true, + processedAt: new Date().toISOString(), + options + }, + metadata: { + itemCount: Array.isArray(data) ? data.length : 1, + hasOptions: Object.keys(options).length > 0 + } + }; + }); + + // Function that throws an error + addFunctionToUtcpDirectCall('throwError', async (message: string) => { + testResults.throwErrorCalled = { message, timestamp: Date.now() }; + throw new Error(message); + }); + + // Function with no parameters + addFunctionToUtcpDirectCall('getCurrentTime', async () => { + testResults.getCurrentTimeCalled = { timestamp: Date.now() }; + return { + timestamp: Date.now(), + iso: new Date().toISOString() + }; + }); + + // Array processing function + addFunctionToUtcpDirectCall('sumArray', async (numbers: number[]) => { + testResults.sumArrayCalled = { numbers, timestamp: Date.now() }; + return { + sum: numbers.reduce((a, b) => a + b, 0), + count: numbers.length, + average: numbers.length > 0 ? numbers.reduce((a, b) => a + b, 0) / numbers.length : 0 + }; + }); +}); + +describe('CodeModeUtcpClient', () => { + let client: CodeModeUtcpClient; + + beforeAll(async () => { + // Create client + client = await CodeModeUtcpClient.create(); + + // First register the manual provider function + addFunctionToUtcpDirectCall('getTestManual', async () => { + return { + utcp_version: '1.0.0', + manual_version: '1.0.0', + tools: [ + { + name: 'add', + description: 'Adds two numbers together', + inputs: { + type: 'object', + properties: { + a: { type: 'number', description: 'First number' }, + b: { type: 'number', description: 'Second number' } + }, + required: ['a', 'b'] + }, + outputs: { + type: 'object', + properties: { + result: { type: 'number', description: 'Sum of the numbers' }, + operation: { type: 'string', description: 'Type of operation' } + }, + required: ['result', 'operation'] + }, + tags: ['math', 'arithmetic'], + tool_call_template: { + call_template_type: 'direct-call', + callable_name: 'add' + } + }, + { + name: 'greet', + description: 'Generates a greeting message', + inputs: { + type: 'object', + properties: { + name: { type: 'string', description: 'Name to greet' }, + formal: { type: 'boolean', description: 'Whether to use formal greeting', default: false } + }, + required: ['name'] + }, + outputs: { + type: 'object', + properties: { + greeting: { type: 'string', description: 'The greeting message' }, + isFormal: { type: 'boolean', description: 'Whether the greeting was formal' } + }, + required: ['greeting', 'isFormal'] + }, + tags: ['text', 'greeting'], + tool_call_template: { + call_template_type: 'direct-call', + callable_name: 'greet' + } + }, + { + name: 'processData', + description: 'Processes data with optional configuration', + inputs: { + type: 'object', + properties: { + data: { description: 'Data to process' }, + options: { type: 'object', description: 'Processing options', default: {} } + }, + required: ['data'] + }, + outputs: { + type: 'object', + properties: { + processedData: { description: 'The processed data' }, + metadata: { type: 'object', description: 'Processing metadata' } + }, + required: ['processedData', 'metadata'] + }, + tags: ['processing', 'data'], + tool_call_template: { + call_template_type: 'direct-call', + callable_name: 'processData' + } + }, + { + name: 'throwError', + description: 'Throws an error for testing error handling', + inputs: { + type: 'object', + properties: { + message: { type: 'string', description: 'Error message' } + }, + required: ['message'] + }, + outputs: { + type: 'object', + properties: {} + }, + tags: ['testing', 'error'], + tool_call_template: { + call_template_type: 'direct-call', + callable_name: 'throwError' + } + }, + { + name: 'getCurrentTime', + description: 'Gets the current timestamp', + inputs: { + type: 'object', + properties: {}, + required: [] + }, + outputs: { + type: 'object', + properties: { + timestamp: { type: 'number', description: 'Unix timestamp' }, + iso: { type: 'string', description: 'ISO date string' } + }, + required: ['timestamp', 'iso'] + }, + tags: ['time', 'utility'], + tool_call_template: { + call_template_type: 'direct-call', + callable_name: 'getCurrentTime' + } + }, + { + name: 'sumArray', + description: 'Calculates sum and statistics of a number array', + inputs: { + type: 'object', + properties: { + numbers: { + type: 'array', + items: { type: 'number' }, + description: 'Array of numbers to sum' + } + }, + required: ['numbers'] + }, + outputs: { + type: 'object', + properties: { + sum: { type: 'number', description: 'Sum of all numbers' }, + count: { type: 'number', description: 'Count of numbers' }, + average: { type: 'number', description: 'Average of numbers' } + }, + required: ['sum', 'count', 'average'] + }, + tags: ['math', 'array', 'statistics'], + tool_call_template: { + call_template_type: 'direct-call', + callable_name: 'sumArray' + } + } + ] + }; + }); + + // Now register the manual that uses the getTestManual function + try { + const result = await client.registerManual({ + name: 'test_tools', + call_template_type: 'direct-call', + callable_name: 'getTestManual' + }); + + if (!result.success) { + console.error('Manual registration failed:', result.errors); + throw new Error(`Manual registration failed: ${result.errors.join(', ')}`); + } + } catch (error) { + console.error('Manual registration error:', error); + throw error; + } + }); + + afterAll(async () => { + if (client) { + await client.close(); + } + }); + + test('should create CodeModeUtcpClient instance', async () => { + expect(client).toBeDefined(); + expect(client).toBeInstanceOf(CodeModeUtcpClient); + }); + + test('should have registered tools', async () => { + const tools = await client.getTools(); + expect(tools.length).toBeGreaterThan(0); + + const toolNames = tools.map(t => t.name.split('.').pop()); + expect(toolNames).toContain('add'); + expect(toolNames).toContain('greet'); + expect(toolNames).toContain('processData'); + }); + + test('should convert tool to TypeScript interface', async () => { + const tools = await client.getTools(); + const addTool = tools.find(t => t.name.endsWith('.add')); + expect(addTool).toBeDefined(); + + const tsInterface = client.toolToTypeScriptInterface(addTool!); + expect(tsInterface).toContain('namespace test_tools'); + expect(tsInterface).toContain('interface addInput'); + expect(tsInterface).toContain('interface addOutput'); + expect(tsInterface).toContain('a: number'); + expect(tsInterface).toContain('b: number'); + expect(tsInterface).toContain('result: number'); + expect(tsInterface).toContain('operation: string'); + expect(tsInterface).toContain('Access as: test_tools.add(args)'); + }); + + test('should generate all tools TypeScript interfaces', async () => { + const interfaces = await client.getAllToolsTypeScriptInterfaces(); + expect(interfaces).toContain('// Auto-generated TypeScript interfaces for UTCP tools'); + expect(interfaces).toContain('namespace test_tools'); + expect(interfaces).toContain('interface addInput'); + expect(interfaces).toContain('interface greetInput'); + expect(interfaces).toContain('interface processDataInput'); + }); + + test('should execute simple code with basic operations', async () => { + const code = ` + const x = 5; + const y = 10; + return x + y; + `; + + const { result } = await client.callToolChain(code); + expect(result).toBe(15); + }); + + test('should execute code that calls a simple tool', async () => { + // Clear previous call records + delete testResults.addCalled; + + const code = ` + const result = await test_tools.add({ a: 15, b: 25 }); + return result; + `; + + const { result } = await client.callToolChain(code); + expect(result.result).toBe(40); + expect(result.operation).toBe('addition'); + + // Verify the tool was called with correct parameters + expect(testResults.addCalled).toBeDefined(); + expect(testResults.addCalled.a).toBe(15); + expect(testResults.addCalled.b).toBe(25); + }); + + test('should execute code with multiple tool calls', async () => { + // Clear previous call records + delete testResults.addCalled; + delete testResults.greetCalled; + + const code = ` + const mathResult = await test_tools.add({ a: 10, b: 5 }); + const greetResult = await test_tools.greet({ name: "Alice", formal: true }); + + return { + math: mathResult, + greeting: greetResult, + combined: \`\${greetResult.greeting} The sum is \${mathResult.result}\` + }; + `; + + const { result } = await client.callToolChain(code); + expect(result.math.result).toBe(15); + expect(result.greeting.greeting).toBe("Good day, Alice"); + expect(result.greeting.isFormal).toBe(true); + expect(result.combined).toBe("Good day, Alice The sum is 15"); + + // Verify both tools were called + expect(testResults.addCalled).toBeDefined(); + expect(testResults.greetCalled).toBeDefined(); + }); + + test('should handle complex data structures', async () => { + delete testResults.processDataCalled; + + const code = ` + const complexData = { + users: [ + { name: "John", age: 30 }, + { name: "Jane", age: 25 } + ], + settings: { theme: "dark", notifications: true } + }; + + const result = await test_tools.processData({ + data: complexData, + options: { validate: true, transform: "uppercase" } + }); + + return result; + `; + + const { result } = await client.callToolChain(code); + expect(result.processedData.processed).toBe(true); + expect(result.processedData.users).toBeDefined(); + expect(result.metadata.itemCount).toBe(1); + expect(result.metadata.hasOptions).toBe(true); + + // Verify the tool was called with the complex data + expect(testResults.processDataCalled).toBeDefined(); + expect(testResults.processDataCalled.data.users.length).toBe(2); + expect(testResults.processDataCalled.options.validate).toBe(true); + }); + + test('should handle arrays and array processing tools', async () => { + delete testResults.sumArrayCalled; + + const code = ` + const numbers = [1, 2, 3, 4, 5, 10]; + const stats = await test_tools.sumArray({ numbers }); + + return { + original: numbers, + statistics: stats, + doubled: numbers.map(n => n * 2) + }; + `; + + const { result } = await client.callToolChain(code); + expect(result.statistics.sum).toBe(25); + expect(result.statistics.count).toBe(6); + expect(result.statistics.average).toBe(25/6); + expect(result.doubled).toEqual([2, 4, 6, 8, 10, 20]); + + // Verify the tool was called correctly + expect(testResults.sumArrayCalled).toBeDefined(); + expect(testResults.sumArrayCalled.numbers).toEqual([1, 2, 3, 4, 5, 10]); + }); + + test('should handle tool calls with no parameters', async () => { + delete testResults.getCurrentTimeCalled; + + const code = ` + const timeResult = await test_tools.getCurrentTime({}); + return { + timeData: timeResult, + isRecent: timeResult.timestamp > Date.now() - 5000 + }; + `; + + const { result } = await client.callToolChain(code); + expect(result.timeData.timestamp).toBeDefined(); + expect(result.timeData.iso).toBeDefined(); + expect(result.isRecent).toBe(true); + + // Verify the tool was called + expect(testResults.getCurrentTimeCalled).toBeDefined(); + }); + + test('should handle tool errors correctly', async () => { + const code = ` + try { + await test_tools.throwError({ message: "Test error message" }); + return { error: false }; + } catch (error) { + return { + error: true, + message: error.message, + caught: true + }; + } + `; + + const { result } = await client.callToolChain(code); + expect(result.error).toBe(true); + expect(result.caught).toBe(true); + expect(result.message).toContain("Test error message"); + }); + + test('should handle code execution timeout', async () => { + const code = ` + // Infinite loop to test timeout + while (true) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return { completed: true }; + `; + + await expect(client.callToolChain(code, 1000)).rejects.toThrow(); + }); + + test('should handle code syntax errors', async () => { + const invalidCode = ` + const invalid syntax here + return result; + `; + + await expect(client.callToolChain(invalidCode)).rejects.toThrow(); + }); + + test('should have access to basic JavaScript globals', async () => { + const code = ` + return { + mathPi: Math.PI, + dateNow: Date.now(), + arrayMethods: Array.isArray([1, 2, 3]), + jsonStringify: JSON.stringify({ test: true }), + objectKeys: Object.keys({ a: 1, b: 2 }) + }; + `; + + const { result } = await client.callToolChain(code); + expect(result.mathPi).toBe(Math.PI); + expect(typeof result.dateNow).toBe('number'); + expect(result.arrayMethods).toBe(true); + expect(result.jsonStringify).toBe('{"test":true}'); + expect(result.objectKeys).toEqual(['a', 'b']); + }); + + test('should have access to TypeScript interfaces in execution context', async () => { + const code = ` + return { + hasInterfaces: typeof __interfaces === 'string', + interfacesContainNamespace: __interfaces.includes('namespace test_tools'), + canGetSpecificInterface: typeof __getToolInterface === 'function', + addToolInterface: __getToolInterface('test_tools.add'), + interfaceIsString: typeof __getToolInterface('test_tools.add') === 'string' + }; + `; + + const { result } = await client.callToolChain(code); + expect(result.hasInterfaces).toBe(true); + expect(result.interfacesContainNamespace).toBe(true); + expect(result.canGetSpecificInterface).toBe(true); + expect(result.addToolInterface).toBeTruthy(); + expect(result.interfaceIsString).toBe(true); + }); + + test('should execute complex chained operations', async () => { + // Clear call records + Object.keys(testResults).forEach(key => delete testResults[key]); + + const code = ` + // Step 1: Get some numbers and process them + const numbers = [5, 10, 15, 20]; + const arrayStats = await test_tools.sumArray({ numbers }); + + // Step 2: Use the sum in another calculation + const addResult = await test_tools.add({ a: arrayStats.sum, b: 100 }); + + // Step 3: Create a greeting with the result + const greeting = await test_tools.greet({ name: "CodeMode", formal: false }); + + // Step 4: Process all the data together + const finalData = await test_tools.processData({ + data: { + arrayStats, + addResult, + greeting, + executionTime: Date.now() + }, + options: { + includeMetadata: true, + format: "enhanced" + } + }); + + return { + steps: { + arrayProcessing: arrayStats, + addition: addResult, + greeting: greeting, + finalProcessing: finalData + }, + summary: { + originalSum: arrayStats.sum, + finalSum: addResult.result, + greetingMessage: greeting.greeting, + chainCompleted: true + } + }; + `; + + const result = await client.callToolChain(code, 15000); + + // Verify the chain worked correctly + expect(result.result.steps.arrayProcessing.sum).toBe(50); + expect(result.result.steps.addition.result).toBe(150); + expect(result.result.steps.greeting.greeting).toBe("Hey CodeMode!"); + expect(result.result.steps.finalProcessing.processedData.processed).toBe(true); + expect(result.result.summary.chainCompleted).toBe(true); + + // Verify all tools were called in the correct order + expect(testResults.sumArrayCalled).toBeDefined(); + expect(testResults.addCalled).toBeDefined(); + expect(testResults.greetCalled).toBeDefined(); + expect(testResults.processDataCalled).toBeDefined(); + + // Verify the parameters were passed correctly through the chain + expect(testResults.addCalled.a).toBe(50); // Sum from array + expect(testResults.addCalled.b).toBe(100); + expect(testResults.greetCalled.name).toBe("CodeMode"); + expect(testResults.greetCalled.formal).toBe(false); + }); + + test('should provide agent prompt template', () => { + const promptTemplate = CodeModeUtcpClient.AGENT_PROMPT_TEMPLATE; + + expect(typeof promptTemplate).toBe('string'); + expect(promptTemplate.length).toBeGreaterThan(0); + expect(promptTemplate).toContain('Tool Discovery Phase'); + expect(promptTemplate).toContain('Interface Introspection'); + expect(promptTemplate).toContain('Code Execution Guidelines'); + expect(promptTemplate).toContain('await manual.tool'); + expect(promptTemplate).toContain('__interfaces'); + expect(promptTemplate).toContain('__getToolInterface'); + expect(promptTemplate).toContain('Discover first, code second'); + }); + + test('should capture console.log output with callToolChain', async () => { + const code = ` + console.log('First log message'); + console.log('Number:', 42); + console.log('Object:', { name: 'test', value: 123 }); + + const addResult = await test_tools.add({ a: 10, b: 20 }); + console.log('Addition result:', addResult); + + return addResult.result; + `; + + const { result, logs } = await client.callToolChain(code); + + expect(result).toBe(30); + expect(logs).toHaveLength(4); + expect(logs[0]).toBe('First log message'); + expect(logs[1]).toBe('Number: 42'); + expect(logs[2]).toContain('"name": "test"'); + expect(logs[2]).toContain('"value": 123'); + expect(logs[3]).toContain('Addition result:'); + expect(logs[3]).toContain('"result": 30'); + }); + + test('should capture console error and warn with callToolChain', async () => { + const code = ` + console.log('Regular log'); + console.error('This is an error'); + console.warn('This is a warning'); + console.info('This is info'); + + return 'done'; + `; + + const { result, logs } = await client.callToolChain(code); + + expect(result).toBe('done'); + expect(logs).toHaveLength(4); + expect(logs[0]).toBe('Regular log'); + expect(logs[1]).toBe('[ERROR] This is an error'); + expect(logs[2]).toBe('[WARN] This is a warning'); + expect(logs[3]).toBe('[INFO] This is info'); + }); +}); + +// Export for potential manual testing +export { testResults }; diff --git a/packages/code-mode/tsconfig.json b/packages/code-mode/tsconfig.json new file mode 100644 index 0000000..861acff --- /dev/null +++ b/packages/code-mode/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" } + ] +} diff --git a/packages/code-mode/tsup.config.ts b/packages/code-mode/tsup.config.ts new file mode 100644 index 0000000..165c8e4 --- /dev/null +++ b/packages/code-mode/tsup.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: { + compilerOptions: { + composite: false, + paths: {}, + }, + }, + splitting: false, + sourcemap: true, + clean: true, + outDir: 'dist', + external: ['@utcp/sdk'], +}); diff --git a/packages/core/package.json b/packages/core/package.json index cbe848d..bcb413f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@utcp/sdk", - "version": "1.0.16", + "version": "1.0.17", "description": "Universal Tool Calling Protocol SDK", "main": "dist/index.cjs", "module": "dist/index.js", diff --git a/packages/core/src/client/utcp_client.ts b/packages/core/src/client/utcp_client.ts index 60610e6..afaf7c0 100644 --- a/packages/core/src/client/utcp_client.ts +++ b/packages/core/src/client/utcp_client.ts @@ -32,7 +32,7 @@ export class UtcpClient implements IUtcpClient { private _registeredCommProtocols: Map = new Map(); public readonly postProcessors: ToolPostProcessor[]; - private constructor( + protected constructor( public readonly config: UtcpClientConfig, public readonly variableSubstitutor: VariableSubstitutor, public readonly root_dir: string | null = null,