diff --git a/agent.ts b/agent.ts index e3675c9..b0ba2ad 100755 --- a/agent.ts +++ b/agent.ts @@ -1,5 +1,7 @@ #!/usr/bin/env bun +import React from "react"; +import { render } from "ink"; import { program } from "@commander-js/extra-typings"; import * as pkg from "./package.json"; import { Config } from "./classes/config"; @@ -9,6 +11,7 @@ import { setOpenAIClient } from "./utils/client"; import { createMcpServer } from "./utils/tools"; import { GeneralAgent } from "./agents/general"; +import { App } from "./components/App"; const config = new Config(); const logger = new Logger(config); @@ -25,13 +28,11 @@ async function cleanup() { } process.on("SIGINT", async () => { - console.log("SIGINT: ๐Ÿ‘‹ Bye!"); await cleanup(); process.exit(0); }); process.on("SIGTERM", async () => { - console.log("SIGTERM: ๐Ÿ‘‹ Bye!"); await cleanup(); process.exit(0); }); @@ -60,15 +61,19 @@ program await mcpServer.connect(); const agent = new GeneralAgent(config, logger); - await agent.interactiveChat( - async (input: string) => { - await agent.chat(input, [mcpServer!]); - }, - message, - async () => { - await cleanup(); - process.exit(0); - }, + + render( + React.createElement(App, { + agent, + mcpServer: mcpServer!, + logger, + config, + initialMessage: message, + onExit: async () => { + await cleanup(); + process.exit(0); + }, + }), ); }); diff --git a/agents/general.ts b/agents/general.ts index 58a701c..6f9a839 100644 --- a/agents/general.ts +++ b/agents/general.ts @@ -2,7 +2,6 @@ import { WrappedAgent } from "../classes/wrappedAgent"; import type { Config } from "../classes/config"; import type { Logger } from "../classes/logger"; import type { MCPServerStreamableHttp } from "@openai/agents"; -import chalk from "chalk"; export class GeneralAgent extends WrappedAgent { constructor(config: Config, logger: Logger) { @@ -17,9 +16,6 @@ You are in a terminal window, and the size of the terminal is ${Bun.env.COLUMNS} } async chat(prompt: string, mcpServers: MCPServerStreamableHttp[] = []) { - this.logger.startSpan(chalk.gray(`Thinking...`)); - const stream = await this.run(prompt, mcpServers); - this.logger.endSpan(); - return stream; + return await this.run(prompt, mcpServers); } } diff --git a/bun.lock b/bun.lock index 08e3dc7..9869c79 100644 --- a/bun.lock +++ b/bun.lock @@ -9,11 +9,16 @@ "@openai/agents": "^0.4.11", "chalk": "^5.4.1", "commander": "^14.0.0", + "ink": "^6.7.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", "openai": "^6.20.0", "ora": "^8.2.0", + "react": "^19.2.4", }, "devDependencies": { "@types/bun": "latest", + "@types/react": "^19.2.14", "prettier": "^3.6.2", }, "peerDependencies": { @@ -22,6 +27,8 @@ }, }, "packages": { + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], + "@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="], "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], @@ -40,7 +47,7 @@ "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], - "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -50,8 +57,14 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], @@ -64,16 +77,24 @@ "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="], + + "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -82,7 +103,7 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], @@ -96,14 +117,20 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], @@ -126,7 +153,7 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -144,12 +171,24 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ink": ["ink@6.7.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-dhB16KfdTO8yYwF2K0E4wPXpL88tdrjjB6w44AZ0ljSktYoUQQcxccq9KL1vpRhk8JIa0A7B7zvjajHqI42teA=="], + + "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], + + "ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="], + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], @@ -176,6 +215,8 @@ "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -190,7 +231,7 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], "openai": ["openai@6.22.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw=="], @@ -198,6 +239,8 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], @@ -214,9 +257,13 @@ "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -224,6 +271,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], @@ -242,18 +291,28 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="], "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], @@ -266,10 +325,16 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], @@ -282,10 +347,32 @@ "body-parser/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "ink/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "ink-text-input/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "ink-text-input/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "ora/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "body-parser/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "ora/string-width/get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "wrap-ansi/string-width/get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "body-parser/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], } } diff --git a/classes/config.ts b/classes/config.ts index ff20946..5e7ad24 100644 --- a/classes/config.ts +++ b/classes/config.ts @@ -1,3 +1,4 @@ +import * as path from "path"; import type { LogLevel } from "./logger"; export class Config { @@ -7,6 +8,7 @@ export class Config { public readonly log_color: boolean = true; public readonly log_timestamps: boolean = true; public arcade_gateway_url: string | undefined; + public readonly context_dir: string; constructor() { const openai_api_key = Bun.env.OPENAI_API_KEY; @@ -28,5 +30,7 @@ export class Config { this.log_level = log_level as LogLevel; this.arcade_gateway_url = Bun.env.ARCADE_GATEWAY_URL; + this.context_dir = + Bun.env.CONTEXT_DIR || path.join(process.cwd(), ".context", "arcade"); } } diff --git a/classes/logger.ts b/classes/logger.ts index 6621a7e..3e726c6 100644 --- a/classes/logger.ts +++ b/classes/logger.ts @@ -1,7 +1,5 @@ -import chalk from "chalk"; -import ora, { type Ora } from "ora"; +import { EventEmitter } from "events"; import type { Config } from "./config"; -import { Writable } from "stream"; export enum LogLevel { DEBUG = "debug", @@ -10,48 +8,22 @@ export enum LogLevel { ERROR = "error", } -const SPAN_UPDATE_INTERVAL = 200; - -class LoggerStream extends Writable { - public appendedString: string = ""; - private logger: Logger; - private lastUpdatedAt: number = 0; - - constructor(logger: Logger) { - super(); - this.logger = logger; - } - - _write( - chunk: any, - encoding: string, - callback: (error?: Error | null) => void, - ) { - const stringChunk = String(chunk); - this.appendedString += stringChunk; - if (Date.now() - this.lastUpdatedAt > SPAN_UPDATE_INTERVAL) { - this.logger.streamToSpan(this.appendedString); - this.lastUpdatedAt = Date.now(); - } - callback(); - } +export interface LogEvent { + level: LogLevel; + message: string; + timestamp: string; } -export class Logger { +export class Logger extends EventEmitter { private level: LogLevel; private color: boolean; private includeTimestamps: boolean; - private spanStartTime: number | undefined = undefined; - private spinner: Ora | undefined = undefined; - private toolCallCount: number = 0; - private updateInterval: NodeJS.Timeout | undefined = undefined; - public stream: LoggerStream; constructor(config: Config) { + super(); this.includeTimestamps = config.log_timestamps; this.level = config.log_level; this.color = config.log_color; - this.stream = new LoggerStream(this); } public getTimestamp() { @@ -62,156 +34,59 @@ export class Logger { second: "2-digit", }); - return this.includeTimestamps - ? this.color - ? chalk.gray(`[${timestamp}]`) - : `[${timestamp}]` - : ""; + return this.includeTimestamps ? `[${timestamp}]` : ""; } - private getSpanMarker() { - return this.spanStartTime !== undefined ? " โ”œโ”€" : ""; - } - - private getDuration() { - return Math.round((Date.now() - (this.spanStartTime ?? Date.now())) / 1000); - } - - private getToolCallStats() { - const duration = this.getDuration(); - const toolCallText = ` ๐Ÿ• duration: ${duration}s | ๐Ÿ› ๏ธ tool calls: ${this.toolCallCount}`; - return this.color ? chalk.dim(toolCallText) : toolCallText; - } - - private formatMessage(message: string, color: (text: string) => string) { - return this.color ? color(message) : message; - } - - private logToConsole( - message: string, - level: LogLevel, - color: (text: string) => string, - skipTimestamp: boolean = false, - ) { - // Check if we should skip logging based on current log level - const shouldSkip = + private shouldSkip(level: LogLevel) { + return ( (this.level === LogLevel.ERROR && level !== LogLevel.ERROR) || (this.level === LogLevel.WARN && (level === LogLevel.INFO || level === LogLevel.DEBUG)) || - (this.level === LogLevel.INFO && level === LogLevel.DEBUG); - if (shouldSkip) return; - - const timestamp = skipTimestamp ? "" : this.getTimestamp(); - const spanMarker = this.getSpanMarker(); - const formattedMessage = this.formatMessage(message, color); - const output = `${timestamp}${spanMarker} ${formattedMessage}`; - - if (level === LogLevel.ERROR || level === LogLevel.WARN) { - console.error(output); - } else if (level === LogLevel.DEBUG) { - console.debug(output); - } else { - console.log(output); - } + (this.level === LogLevel.INFO && level === LogLevel.DEBUG) + ); } - result(message: string | undefined) { - if (!message) return; - this.logToConsole(message, LogLevel.INFO, chalk.white, true); + private log(message: string, level: LogLevel) { + if (this.shouldSkip(level)) return; + const event: LogEvent = { + level, + message, + timestamp: this.getTimestamp(), + }; + this.emit("log", event); } info(message: string | undefined) { if (!message) return; - this.logToConsole(message, LogLevel.INFO, chalk.white); + this.log(message, LogLevel.INFO); } warn(message: string | undefined) { if (!message) return; - this.logToConsole(message, LogLevel.WARN, chalk.yellow); + this.log(message, LogLevel.WARN); } error(message: string | undefined) { if (!message) return; - this.logToConsole(message, LogLevel.ERROR, chalk.red); + this.log(message, LogLevel.ERROR); } debug(message: string | undefined) { if (!message) return; - this.logToConsole(message, LogLevel.DEBUG, chalk.gray); + this.log(message, LogLevel.DEBUG); } incrementToolCalls() { - this.toolCallCount++; - this.updateSpanDisplay(); + this.emit("toolCall"); } - private updateSpanDisplay() { - if (!this.spinner) return; - const mainMessage = this.spinner.text.split("\n")[0]; - this.spinner.text = `${mainMessage}\n${this.getToolCallStats()}`; + onLog(callback: (event: LogEvent) => void) { + this.on("log", callback); + return () => this.off("log", callback); } - startSpan(message: string) { - this.info(message); - this.spanStartTime = Date.now(); - this.toolCallCount = 0; - this.stream.appendedString = ""; - this.spinner = ora(this.formatMessage(message, chalk.cyan)).start(); - - this.updateInterval = setInterval(() => this.updateSpanDisplay(), 1000); - } - - updateSpan(message: string, emoji: string) { - if (!this.spinner) return; - - const originalText = this.spinner.text; - const timestamp = this.getTimestamp(); - const spanMarker = this.getSpanMarker(); - const formattedMessage = this.formatMessage(message, chalk.white); - - this.spinner.stopAndPersist({ - text: formattedMessage, - symbol: `${timestamp}${spanMarker} ${emoji}`, - }); - - this.spinner.start(this.formatMessage(originalText, chalk.cyan)); - this.updateSpanDisplay(); - } - - streamToSpan(message: string) { - if (!this.spinner) return; - - if (this.updateInterval) { - clearInterval(this.updateInterval); - this.updateInterval = undefined; - } - - this.spinner.text = `Streaming: \r\n` + message; - } - - endSpan(message?: string) { - if (this.updateInterval) { - clearInterval(this.updateInterval); - this.updateInterval = undefined; - } - - this.stream.appendedString = ""; - - const doneMessage = "Done!"; - const timestamp = this.getTimestamp(); - const duration = this.getDuration(); - - this.spinner?.stopAndPersist({ - text: this.formatMessage(`${doneMessage} (${duration}s)`, chalk.cyan), - symbol: `${timestamp} โœ…`, - }); - - this.spinner = undefined; - this.spanStartTime = undefined; - this.toolCallCount = 0; - - if (message) { - this.result(`\r\n${message}\r\n`); - } + onToolCall(callback: () => void) { + this.on("toolCall", callback); + return () => this.off("toolCall", callback); } } diff --git a/classes/wrappedAgent.ts b/classes/wrappedAgent.ts index fbfeb96..fdfae77 100644 --- a/classes/wrappedAgent.ts +++ b/classes/wrappedAgent.ts @@ -9,8 +9,6 @@ import { import { Config } from "./config"; import type { Logger } from "./logger"; -import * as readline from "readline"; -import chalk from "chalk"; export abstract class WrappedAgent { history: AgentInputItem[] = []; @@ -57,85 +55,17 @@ export abstract class WrappedAgent { stream: true, }); - stream - .toTextStream({ compatibleWithNodeStreams: true }) - .pipe(this.logger.stream); - await stream.completed; + return stream; + } - this.logger.endSpan(stream.finalOutput); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async finalizeStream(stream: any) { + await stream.completed; if (stream.history.length > 0) { this.history = stream.history; } - if (stream.finalOutput) { - this.logger.debug(stream.finalOutput); - } - - return stream; - } - - public async interactiveChat( - execMethod: (input: string) => Promise, - initialMessage?: string, - onExit: () => void = () => process.exit(0), - ) { - this.logger.info( - `๐Ÿค– Starting chat session with your agent (${this.config.openai_model})`, - ); - this.logger.info("๐Ÿ’ก Type 'quit', 'exit', or 'bye' to end the session"); - this.logger.info("๐Ÿ’ก Type 'clear' to clear the conversation history"); - - const askQuestion = async ( - questionText: string = `${this.logger.getTimestamp()} ` + - chalk.green("?> "), - ) => { - await new Promise((resolve) => { - // Create a custom output stream that colors user input green - const mutableStdout = new (require("stream").Writable)({ - write: (chunk: any, encoding: any, callback: any) => { - // Color the input text green - process.stdout.write(chalk.green(chunk.toString()), callback); - }, - }); - mutableStdout.columns = process.stdout.columns; - mutableStdout.rows = process.stdout.rows; - - const rl = readline.createInterface({ - input: process.stdin, - output: mutableStdout, - terminal: true, - }); - - rl.question(questionText, async (answer) => { - process.stdout.write("\n"); // Add newline after green input - await handleInput(answer.trim()); - rl.close(); - resolve(true); - }); - }); - - await askQuestion(); - }; - - const handleInput = async (input: string) => { - if (input.toLowerCase() === "quit" || input.toLowerCase() === "exit") { - console.log("๐Ÿ‘‹ Goodbye!"); - onExit?.(); - } - - if (input === "clear") { - this.history = []; - this.logger.info("๐Ÿงน Conversation history cleared!"); - return await execMethod("Hello - we are starting a new conversation."); - } - - return await execMethod(input); - }; - - if (initialMessage) { - await handleInput(initialMessage); - } - await askQuestion(); + return stream.finalOutput; } } diff --git a/components/App.tsx b/components/App.tsx new file mode 100644 index 0000000..c253d7d --- /dev/null +++ b/components/App.tsx @@ -0,0 +1,193 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { Box, useApp } from "ink"; +import type { MCPServerStreamableHttp } from "@openai/agents"; +import type { GeneralAgent } from "../agents/general.js"; +import type { Logger, LogEvent } from "../classes/logger.js"; +import type { Config } from "../classes/config.js"; +import { MessageArea } from "./MessageArea.js"; +import { InputBox } from "./InputBox.js"; +import type { MessageData } from "./Message.js"; + +interface AppProps { + agent: GeneralAgent; + mcpServer: MCPServerStreamableHttp; + logger: Logger; + config: Config; + initialMessage?: string; + onExit: () => Promise; +} + +export function App({ + agent, + mcpServer, + logger, + config, + initialMessage, + onExit, +}: AppProps) { + const { exit } = useApp(); + const [messages, setMessages] = useState([]); + const [streamingText, setStreamingText] = useState(""); + const [isStreaming, setIsStreaming] = useState(false); + const [toolCallCount, setToolCallCount] = useState(0); + const [startTime, setStartTime] = useState(null); + const processingRef = useRef(false); + const queueRef = useRef([]); + const [queueCount, setQueueCount] = useState(0); + const initialProcessed = useRef(false); + + // Subscribe to logger events + useEffect(() => { + const unsubLog = logger.onLog((event: LogEvent) => { + setMessages((prev) => [ + ...prev, + { + role: "system", + content: event.message, + timestamp: event.timestamp, + }, + ]); + }); + + const unsubTool = logger.onToolCall(() => { + setToolCallCount((prev) => prev + 1); + }); + + return () => { + unsubLog(); + unsubTool(); + }; + }, [logger]); + + const processOne = useCallback( + async (input: string) => { + // Add user message + setMessages((prev) => [ + ...prev, + { + role: "user", + content: input, + timestamp: logger.getTimestamp(), + }, + ]); + + setIsStreaming(true); + setStreamingText(""); + setToolCallCount(0); + setStartTime(Date.now()); + + try { + const stream = await agent.chat(input, [mcpServer]); + const textStream = stream.toTextStream({ + compatibleWithNodeStreams: true, + }); + + let accumulated = ""; + for await (const chunk of textStream) { + accumulated += chunk; + setStreamingText(accumulated); + } + + const finalOutput = await agent.finalizeStream(stream); + const content = finalOutput || accumulated; + + setMessages((prev) => [ + ...prev, + { + role: "assistant", + content, + timestamp: logger.getTimestamp(), + }, + ]); + } catch (err) { + setMessages((prev) => [ + ...prev, + { + role: "system", + content: `Error: ${err instanceof Error ? err.message : String(err)}`, + timestamp: logger.getTimestamp(), + }, + ]); + } finally { + setIsStreaming(false); + setStreamingText(""); + setStartTime(null); + setToolCallCount(0); + } + }, + [agent, mcpServer, logger], + ); + + const drainQueue = useCallback(async () => { + if (processingRef.current) return; + processingRef.current = true; + + while (queueRef.current.length > 0) { + const next = queueRef.current.shift()!; + setQueueCount(queueRef.current.length); + await processOne(next); + } + + processingRef.current = false; + }, [processOne]); + + const enqueueInput = useCallback( + (input: string) => { + queueRef.current.push(input); + setQueueCount(queueRef.current.length); + drainQueue(); + }, + [drainQueue], + ); + + const handleSubmit = useCallback( + (input: string) => { + const lower = input.toLowerCase(); + if (lower === "quit" || lower === "exit" || lower === "bye") { + onExit().then(() => exit()); + return; + } + + if (lower === "clear") { + setMessages([]); + agent.history = []; + setMessages([ + { + role: "system", + content: "Conversation history cleared!", + timestamp: logger.getTimestamp(), + }, + ]); + return; + } + + enqueueInput(input); + }, + [enqueueInput, onExit, exit, agent, logger], + ); + + // Handle initial message + useEffect(() => { + if (initialMessage && !initialProcessed.current) { + initialProcessed.current = true; + enqueueInput(initialMessage); + } + }, [initialMessage, enqueueInput]); + + return ( + + + + + ); +} diff --git a/components/InputBox.tsx b/components/InputBox.tsx new file mode 100644 index 0000000..bf6e565 --- /dev/null +++ b/components/InputBox.tsx @@ -0,0 +1,93 @@ +import { useState, useCallback, useRef } from "react"; +import { Box, Text, useInput } from "ink"; +import TextInput from "./TextInput.js"; +import * as fs from "fs"; +import * as path from "path"; + +interface InputBoxProps { + onSubmit: (input: string) => void; + contextDir: string; + queueCount: number; +} + +const DELIM = "\0"; + +function loadHistory(historyFile: string): string[] { + try { + const content = fs.readFileSync(historyFile, "utf-8"); + return content.split(DELIM).filter((entry) => entry.length > 0); + } catch { + return []; + } +} + +function saveToHistory(historyFile: string, entry: string) { + fs.mkdirSync(path.dirname(historyFile), { recursive: true }); + fs.appendFileSync(historyFile, entry + DELIM); +} + +export function InputBox({ onSubmit, contextDir, queueCount }: InputBoxProps) { + const [value, setValue] = useState(""); + const historyFile = path.join(contextDir, "chat_history.txt"); + const historyRef = useRef(loadHistory(historyFile)); + const indexRef = useRef(-1); + const draftRef = useRef(""); + + const handleSubmit = useCallback( + (input: string) => { + const trimmed = input.trim(); + if (!trimmed) return; + + historyRef.current.push(trimmed); + saveToHistory(historyFile, trimmed); + indexRef.current = -1; + draftRef.current = ""; + setValue(""); + onSubmit(trimmed); + }, + [onSubmit, historyFile], + ); + + useInput((_input, key) => { + const history = historyRef.current; + if (key.upArrow && history.length > 0) { + if (indexRef.current === -1) { + draftRef.current = value; + indexRef.current = history.length - 1; + } else if (indexRef.current > 0) { + indexRef.current--; + } + setValue(history[indexRef.current] ?? ""); + } else if (key.downArrow) { + if (indexRef.current === -1) return; + if (indexRef.current < history.length - 1) { + indexRef.current++; + setValue(history[indexRef.current] ?? ""); + } else { + indexRef.current = -1; + setValue(draftRef.current); + } + } + }); + + return ( + + + + + {queueCount > 0 && ( + + {" "} + {queueCount} message{queueCount > 1 ? "s" : ""} queued + + )} + + ); +} diff --git a/components/Message.tsx b/components/Message.tsx new file mode 100644 index 0000000..8b64082 --- /dev/null +++ b/components/Message.tsx @@ -0,0 +1,27 @@ +import { Box, Text } from "ink"; + +export type MessageRole = "user" | "assistant" | "system"; + +export interface MessageData { + role: MessageRole; + content: string; + timestamp: string; +} + +export function Message({ role, content, timestamp }: MessageData) { + const color = + role === "user" ? "green" : role === "system" ? "gray" : "white"; + const prefix = role === "user" ? "?> " : ""; + + return ( + + + {timestamp} + + {prefix} + {content} + + + + ); +} diff --git a/components/MessageArea.tsx b/components/MessageArea.tsx new file mode 100644 index 0000000..7164ae5 --- /dev/null +++ b/components/MessageArea.tsx @@ -0,0 +1,45 @@ +import { Box, Text } from "ink"; +import Spinner from "ink-spinner"; +import { Message, type MessageData } from "./Message.js"; + +interface MessageAreaProps { + messages: MessageData[]; + streamingText: string; + isStreaming: boolean; + toolCallCount: number; + startTime: number | null; +} + +export function MessageArea({ + messages, + streamingText, + isStreaming, + toolCallCount, + startTime, +}: MessageAreaProps) { + return ( + + {messages.map((msg, i) => ( + + ))} + + {isStreaming && ( + + + + {" "} + + + Thinking... + {startTime && + ` ๐Ÿ• ${Math.round((Date.now() - startTime) / 1000)}s`} + {toolCallCount > 0 && + ` | ๐Ÿ› ๏ธ ${toolCallCount} tool call${toolCallCount > 1 ? "s" : ""}`} + + + {streamingText && {streamingText}} + + )} + + ); +} diff --git a/components/TextInput.tsx b/components/TextInput.tsx new file mode 100644 index 0000000..5c0ea55 --- /dev/null +++ b/components/TextInput.tsx @@ -0,0 +1,178 @@ +import React, { useState, useEffect } from "react"; +import { Text, useInput } from "ink"; +import chalk from "chalk"; + +interface TextInputProps { + value: string; + onChange: (value: string) => void; + onSubmit?: (value: string) => void; + placeholder?: string; + focus?: boolean; + mask?: string; + showCursor?: boolean; + highlightPastedText?: boolean; +} + +export default function TextInput({ + value: originalValue, + placeholder = "", + focus = true, + mask, + highlightPastedText = false, + showCursor = true, + onChange, + onSubmit, +}: TextInputProps) { + const [state, setState] = useState({ + cursorOffset: (originalValue || "").length, + cursorWidth: 0, + }); + + const { cursorOffset, cursorWidth } = state; + + useEffect(() => { + setState((previousState) => { + if (!focus || !showCursor) { + return previousState; + } + const newValue = originalValue || ""; + if (previousState.cursorOffset > newValue.length - 1) { + return { + cursorOffset: newValue.length, + cursorWidth: 0, + }; + } + return previousState; + }); + }, [originalValue, focus, showCursor]); + + const cursorActualWidth = highlightPastedText ? cursorWidth : 0; + const value = mask ? mask.repeat(originalValue.length) : originalValue; + let renderedValue = value; + let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined; + + if (showCursor && focus) { + renderedPlaceholder = + placeholder.length > 0 + ? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1)) + : chalk.inverse(" "); + renderedValue = value.length > 0 ? "" : chalk.inverse(" "); + let i = 0; + for (const char of value) { + renderedValue += + i >= cursorOffset - cursorActualWidth && i <= cursorOffset + ? chalk.inverse(char) + : char; + i++; + } + if (value.length > 0 && cursorOffset === value.length) { + renderedValue += chalk.inverse(" "); + } + } + + useInput( + (input, key) => { + if ( + key.upArrow || + key.downArrow || + (key.ctrl && input === "c") || + key.tab || + (key.shift && key.tab) + ) { + return; + } + + if (key.return) { + if (onSubmit) { + onSubmit(originalValue); + } + return; + } + + let nextCursorOffset = cursorOffset; + let nextValue = originalValue; + let nextCursorWidth = 0; + + if (key.ctrl && input === "a") { + // Ctrl+A: move cursor to beginning + setState({ cursorOffset: 0, cursorWidth: 0 }); + return; + } else if (key.ctrl && input === "e") { + // Ctrl+E: move cursor to end + setState({ cursorOffset: originalValue.length, cursorWidth: 0 }); + return; + } else if (key.ctrl && input === "u") { + // Ctrl+U: delete from cursor to beginning + nextValue = originalValue.slice(cursorOffset); + nextCursorOffset = 0; + } else if (key.ctrl && input === "k") { + // Ctrl+K: delete from cursor to end + nextValue = originalValue.slice(0, cursorOffset); + } else if (key.ctrl && input === "w") { + // Ctrl+W: delete word before cursor + const before = originalValue.slice(0, cursorOffset); + const trimmed = before.replace(/\s+$/, ""); + const lastSpace = trimmed.lastIndexOf(" "); + const deleteFrom = lastSpace === -1 ? 0 : lastSpace + 1; + nextValue = + originalValue.slice(0, deleteFrom) + + originalValue.slice(cursorOffset); + nextCursorOffset = deleteFrom; + } else if (key.leftArrow) { + if (showCursor) { + nextCursorOffset--; + } + } else if (key.rightArrow) { + if (showCursor) { + nextCursorOffset++; + } + } else if (key.backspace || key.delete) { + if (cursorOffset > 0) { + nextValue = + originalValue.slice(0, cursorOffset - 1) + + originalValue.slice(cursorOffset, originalValue.length); + nextCursorOffset--; + } + } else if (key.ctrl) { + // Ignore other ctrl combos so they don't insert characters + return; + } else { + nextValue = + originalValue.slice(0, cursorOffset) + + input + + originalValue.slice(cursorOffset, originalValue.length); + nextCursorOffset += input.length; + if (input.length > 1) { + nextCursorWidth = input.length; + } + } + + if (nextCursorOffset < 0) { + nextCursorOffset = 0; + } + if (nextCursorOffset > nextValue.length) { + nextCursorOffset = nextValue.length; + } + + setState({ + cursorOffset: nextCursorOffset, + cursorWidth: nextCursorWidth, + }); + + if (nextValue !== originalValue) { + onChange(nextValue); + } + }, + { isActive: focus }, + ); + + return ( + + {placeholder + ? value.length > 0 + ? renderedValue + : renderedPlaceholder + : renderedValue} + + ); +} diff --git a/package.json b/package.json index 7bc7849..82d7cc2 100644 --- a/package.json +++ b/package.json @@ -14,19 +14,23 @@ "scripts": { "format": "prettier --write .", "lint": "prettier --check .", - "compile": "bun build agent.ts --compile --outfile agent", - "clear": "rm -rf .context/arcade" + "compile": "bun build agent.ts --compile --outfile agent --external react-devtools-core", + "clear": "rm -rf ${CONTEXT_DIR:-.context/arcade}" }, "dependencies": { "@commander-js/extra-typings": "^14.0.0", "@openai/agents": "^0.4.11", "chalk": "^5.4.1", "commander": "^14.0.0", + "ink": "^6.7.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", "openai": "^6.20.0", - "ora": "^8.2.0" + "react": "^19.2.4" }, "devDependencies": { "@types/bun": "latest", + "@types/react": "^19.2.14", "prettier": "^3.6.2" } } diff --git a/tests/config.test.ts b/tests/config.test.ts index b177487..714e51c 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -84,6 +84,27 @@ describe("Config", () => { expect(config.arcade_gateway_url).toBeUndefined(); }); + it("should default context_dir to .context/arcade", async () => { + Bun.env.OPENAI_API_KEY = "test-openai-key"; + Bun.env.OPENAI_MODEL = "gpt-4-turbo"; + Bun.env.LOG_LEVEL = "info"; + + const { Config } = await import("../classes/config"); + const config = new Config(); + expect(config.context_dir).toContain(".context/arcade"); + }); + + it("should allow CONTEXT_DIR override", async () => { + Bun.env.OPENAI_API_KEY = "test-openai-key"; + Bun.env.OPENAI_MODEL = "gpt-4-turbo"; + Bun.env.LOG_LEVEL = "info"; + Bun.env.CONTEXT_DIR = "/tmp/custom-context"; + + const { Config } = await import("../classes/config"); + const config = new Config(); + expect(config.context_dir).toBe("/tmp/custom-context"); + }); + it("should have default values for optional properties", async () => { // Set up all required environment variables Bun.env.OPENAI_API_KEY = "test-openai-key"; @@ -164,6 +185,7 @@ describe("Config", () => { expect(config).toHaveProperty("log_color"); expect(config).toHaveProperty("log_timestamps"); expect(config).toHaveProperty("arcade_gateway_url"); + expect(config).toHaveProperty("context_dir"); }); it("should have correct property types", () => { @@ -173,6 +195,7 @@ describe("Config", () => { expect(typeof config.log_color).toBe("boolean"); expect(typeof config.log_timestamps).toBe("boolean"); expect(typeof config.arcade_gateway_url).toBe("string"); + expect(typeof config.context_dir).toBe("string"); }); }); }); diff --git a/tests/logger.test.ts b/tests/logger.test.ts index 34c2424..318abf6 100644 --- a/tests/logger.test.ts +++ b/tests/logger.test.ts @@ -2,29 +2,19 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; describe("Logger", () => { let originalEnv: NodeJS.ProcessEnv; - let originalConsole: any; beforeEach(() => { originalEnv = { ...process.env }; - originalConsole = { ...console }; // Set up required environment variables process.env.OPENAI_API_KEY = "test-openai-key"; process.env.OPENAI_MODEL = "gpt-4-turbo"; process.env.LOG_LEVEL = "info"; process.env.ARCADE_API_KEY = "test-arcade-key"; - - // Mock console methods to capture output - console.log = () => {}; - console.error = () => {}; - console.debug = () => {}; }); afterEach(() => { process.env = originalEnv; - console.log = originalConsole.log; - console.error = originalConsole.error; - console.debug = originalConsole.debug; }); describe("constructor", () => { @@ -40,18 +30,6 @@ describe("Logger", () => { expect(logger).toHaveProperty("debug"); expect(logger).toHaveProperty("error"); expect(logger).toHaveProperty("warn"); - expect(logger).toHaveProperty("stream"); - }); - - it("should create LoggerStream", async () => { - const { Config } = await import("../classes/config"); - const { Logger } = await import("../classes/logger"); - - const config = new Config(); - const logger = new Logger(config); - - expect(logger.stream).toBeDefined(); - expect(typeof logger.stream.write).toBe("function"); }); }); @@ -92,6 +70,33 @@ describe("Logger", () => { expect(() => logger.error(undefined)).not.toThrow(); expect(() => logger.warn(undefined)).not.toThrow(); }); + + it("should emit log events", async () => { + const events: any[] = []; + logger.onLog((event: any) => events.push(event)); + + logger.info("test info"); + logger.warn("test warn"); + + expect(events).toHaveLength(2); + expect(events[0]?.message).toBe("test info"); + expect(events[0]?.level).toBe("info"); + expect(events[1]?.message).toBe("test warn"); + expect(events[1]?.level).toBe("warn"); + }); + + it("should emit events with correct structure", () => { + const events: any[] = []; + logger.onLog((event: any) => events.push(event)); + + logger.info("test message"); + + expect(events).toHaveLength(1); + expect(events[0]).toHaveProperty("level"); + expect(events[0]).toHaveProperty("message"); + expect(events[0]).toHaveProperty("timestamp"); + expect(events[0]?.message).toBe("test message"); + }); }); describe("getTimestamp method", () => { @@ -112,21 +117,16 @@ describe("Logger", () => { it("should return a timestamp string", () => { const timestamp = logger.getTimestamp(); expect(typeof timestamp).toBe("string"); - // Should match the format [HH:MM:SS] with optional ANSI color codes - expect(timestamp).toMatch( - /^(\u001b\[\d+m)?\[\d{2}:\d{2}:\d{2}\](\u001b\[\d+m)?$/, - ); + expect(timestamp).toMatch(/^\[\d{2}:\d{2}:\d{2}\]$/); }); it("should include timestamp when timestamps are enabled", () => { const timestamp = logger.getTimestamp(); - expect(timestamp).toMatch( - /^(\u001b\[\d+m)?\[\d{2}:\d{2}:\d{2}\](\u001b\[\d+m)?$/, - ); + expect(timestamp).toMatch(/^\[\d{2}:\d{2}:\d{2}\]$/); }); }); - describe("stream property", () => { + describe("tool call events", () => { let logger: any; beforeEach(async () => { @@ -137,54 +137,30 @@ describe("Logger", () => { logger = new Logger(config); }); - it("should have a stream property", () => { - expect(logger).toHaveProperty("stream"); - expect(logger.stream).toBeDefined(); - }); - - it("should have write method on stream", () => { - expect(typeof logger.stream.write).toBe("function"); - }); - - it("should handle stream writes", () => { - expect(() => logger.stream.write("test data")).not.toThrow(); + it("should have incrementToolCalls method", () => { + expect(typeof logger.incrementToolCalls).toBe("function"); + expect(() => logger.incrementToolCalls()).not.toThrow(); }); - }); - - describe("span methods", () => { - let logger: any; - - beforeEach(async () => { - const { Config } = await import("../classes/config"); - const { Logger } = await import("../classes/logger"); - const config = new Config(); - logger = new Logger(config); - }); + it("should emit toolCall events", () => { + let called = 0; + logger.onToolCall(() => called++); - it("should have startSpan method", () => { - expect(typeof logger.startSpan).toBe("function"); - expect(() => logger.startSpan("test span")).not.toThrow(); - }); + logger.incrementToolCalls(); + logger.incrementToolCalls(); - it("should have updateSpan method", () => { - expect(typeof logger.updateSpan).toBe("function"); - expect(() => logger.updateSpan("test update", "๐Ÿ”„")).not.toThrow(); + expect(called).toBe(2); }); - it("should have endSpan method", () => { - expect(typeof logger.endSpan).toBe("function"); - expect(() => logger.endSpan("test output")).not.toThrow(); - }); + it("should allow unsubscribing from events", () => { + let called = 0; + const unsub = logger.onToolCall(() => called++); - it("should have streamToSpan method", () => { - expect(typeof logger.streamToSpan).toBe("function"); - expect(() => logger.streamToSpan("test stream")).not.toThrow(); - }); + logger.incrementToolCalls(); + unsub(); + logger.incrementToolCalls(); - it("should have incrementToolCalls method", () => { - expect(typeof logger.incrementToolCalls).toBe("function"); - expect(() => logger.incrementToolCalls()).not.toThrow(); + expect(called).toBe(1); }); }); @@ -198,7 +174,6 @@ describe("Logger", () => { const config = new Config(); const logger = new Logger(config); - // Should not throw for any log level expect(() => logger.debug("debug message")).not.toThrow(); expect(() => logger.info("info message")).not.toThrow(); expect(() => logger.warn("warn message")).not.toThrow();