u
This commit is contained in:
@@ -7,8 +7,14 @@ OPENROUTER_API_KEY=sk-or-...
|
||||
# Optional: Model to use (default: xiaomi/mimo-v2.6-flash)
|
||||
MODEL=xiaomi/mimo-v2.6-flash
|
||||
|
||||
# Optional: Tavily API key — enables the websearch and browse_url tools
|
||||
TAVILY_API_KEY=tvly-dev-...
|
||||
|
||||
# Optional: File to persist the undo log (relative to workspace root)
|
||||
UNDO_FILE=.fido-undo.json
|
||||
|
||||
# Optional: File to persist /suspend snapshots (relative to workspace root)
|
||||
SUSPEND_FILE=.fido-suspends.json
|
||||
|
||||
# Optional: Workspace root directory (defaults to cwd when running fido)
|
||||
# ROOT_DIR=.
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[
|
||||
{
|
||||
"op": "CREATE",
|
||||
"path": "tmp-cfg-test.mjs",
|
||||
"content": "import { ROOT_DIR } from './src/config.js';\nconsole.log('ROOT_DIR =', ROOT_DIR);\n"
|
||||
},
|
||||
{
|
||||
"op": "DELETE",
|
||||
"path": "tmp-cfg-test.mjs"
|
||||
}
|
||||
]
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
.fido-*
|
||||
801
package-lock.json
generated
801
package-lock.json
generated
@@ -9,11 +9,15 @@
|
||||
"version": "1.0.0",
|
||||
"license": "0BSD",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/prompts": "^7.5.1",
|
||||
"@tavily/core": "^0.7.13",
|
||||
"chalk": "^5.4.1",
|
||||
"dotenv": "^18.0.2",
|
||||
"ignore": "^7.0.10",
|
||||
"llmorbiter": "file:../llmorbiter"
|
||||
"llmorbiter": "file:../llmorbiter",
|
||||
"marked": "^15.0.12",
|
||||
"marked-terminal": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"../llmorbiter": {
|
||||
@@ -23,6 +27,16 @@
|
||||
"dotenv": "^17.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/ansi": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
|
||||
@@ -357,6 +371,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tavily/core": {
|
||||
"version": "0.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@tavily/core/-/core-0.7.13.tgz",
|
||||
"integrity": "sha512-6qVMzYrWPd31dIWqLk/c3F8yGedXw1cSsfbtsDqvdrkpos5vzjfo8RYWCAOGb93QI1E7wsBCTYm+dre5Rv7wvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-tiktoken": "^1.0.14"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
|
||||
"integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"environment": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@@ -381,6 +442,88 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
|
||||
"integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.6",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
@@ -393,12 +536,73 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/char-regex": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
|
||||
"integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/chardet": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz",
|
||||
"integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cli-highlight": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz",
|
||||
"integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"highlight.js": "^10.7.1",
|
||||
"mz": "^2.4.0",
|
||||
"parse5": "^5.1.1",
|
||||
"parse5-htmlparser2-tree-adapter": "^6.0.0",
|
||||
"yargs": "^16.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"highlight": "bin/highlight"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0",
|
||||
"npm": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-table3": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
|
||||
"integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10.* || >= 12.*"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@colors/colors": "1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-width": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
|
||||
@@ -408,6 +612,34 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -426,6 +658,44 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "18.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.2.tgz",
|
||||
@@ -441,12 +711,271 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emojilib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz",
|
||||
"integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/environment": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
|
||||
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
@@ -481,10 +1010,100 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tiktoken": {
|
||||
"version": "1.0.21",
|
||||
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
|
||||
"integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/llmorbiter": {
|
||||
"resolved": "../llmorbiter",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/marked-terminal": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz",
|
||||
"integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"ansi-regex": "^6.1.0",
|
||||
"chalk": "^5.4.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"cli-table3": "^0.6.5",
|
||||
"node-emoji": "^2.2.0",
|
||||
"supports-hyperlinks": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"marked": ">=1 <16"
|
||||
}
|
||||
},
|
||||
"node_modules/marked-terminal/node_modules/ansi-regex": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
|
||||
"integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
|
||||
@@ -494,6 +1113,80 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0",
|
||||
"object-assign": "^4.0.1",
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-emoji": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz",
|
||||
"integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.6.0",
|
||||
"char-regex": "^1.0.2",
|
||||
"emojilib": "^2.4.0",
|
||||
"skin-tone": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
|
||||
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse5": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -512,6 +1205,18 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/skin-tone": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz",
|
||||
"integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unicode-emoji-modifier-base": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
@@ -538,6 +1243,64 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-hyperlinks": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz",
|
||||
"integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0",
|
||||
"supports-color": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-hyperlinks?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/thenify-all": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-emoji-modifier-base": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz",
|
||||
"integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
@@ -552,6 +1315,42 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "16.2.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz",
|
||||
"integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "20.2.9",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors-cjs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
|
||||
|
||||
@@ -13,9 +13,12 @@
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/prompts": "^7.5.1",
|
||||
"@tavily/core": "^0.7.13",
|
||||
"chalk": "^5.4.1",
|
||||
"dotenv": "^18.0.2",
|
||||
"ignore": "^7.0.10",
|
||||
"llmorbiter": "file:../llmorbiter"
|
||||
"llmorbiter": "file:../llmorbiter",
|
||||
"marked": "^15.0.12",
|
||||
"marked-terminal": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,10 @@ import {
|
||||
makeTheme,
|
||||
} from '@inquirer/core';
|
||||
|
||||
import { filterCommands } from './slash-commands.js';
|
||||
import { COMMANDS, filterCommands } from './slash-commands.js';
|
||||
|
||||
const MAX_SUGGESTIONS = 6;
|
||||
const NAME_WIDTH = Math.max(...COMMANDS.map((c) => c.name.length));
|
||||
|
||||
/** Show the inline menu only while the first word is being typed (no space yet). */
|
||||
function termFrom(line) {
|
||||
@@ -43,8 +44,8 @@ function termFrom(line) {
|
||||
/**
|
||||
* Replace readline's actual edit buffer, not just the visible terminal line.
|
||||
* `rl.clearLine()` only erases terminal output; using it before `rl.write()`
|
||||
* leaves the old buffer in place and turns a completion such as `/h` into
|
||||
* `/h/help` internally.
|
||||
* leaves the old buffer in place and turns a completion such as `/p` into
|
||||
* `/p/paste` internally.
|
||||
*/
|
||||
function replaceBuffer(rl, nextValue) {
|
||||
rl.write(null, { ctrl: true, name: 'a' });
|
||||
@@ -104,7 +105,7 @@ export const autocompleteInput = createPrompt((config, done) => {
|
||||
|
||||
const lines = suggestions.map((c, i) => {
|
||||
const marker = i === active % count ? chalk.cyan('❯') : ' ';
|
||||
return ` ${chalk.cyan(marker)} ${chalk.bold(c.name.padEnd(7))} ${chalk.dim(c.description)}`;
|
||||
return ` ${chalk.cyan(marker)} ${chalk.bold(c.name.padEnd(NAME_WIDTH + 2))} ${chalk.dim(c.description)}`;
|
||||
});
|
||||
if (count > 1) {
|
||||
lines.push(chalk.dim(' ↑/↓ choose · Tab complete'));
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
* OPENROUTER_API_KEY=sk-or-...
|
||||
* MODEL=xiaomi/mimo-v2.6-flash
|
||||
* UNDO_FILE=.fido-undo.json
|
||||
* SUSPEND_FILE=.fido-suspends.json (optional, stores /suspend snapshots)
|
||||
* TAVILY_API_KEY=tvly-dev-... (optional, enables the websearch and browse_url tools)
|
||||
* ROOT_DIR=. (optional, defaults to cwd)
|
||||
*
|
||||
* The workspace root can also be given as the first command-line argument:
|
||||
* node src/fido.js /path/to/project
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
@@ -24,10 +29,11 @@ dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||||
export const API_KEY = process.env.OPENROUTER_API_KEY;
|
||||
export const MODEL = process.env.MODEL || 'xiaomi/mimo-v2.6-flash';
|
||||
export const UNDO_FILE = process.env.UNDO_FILE || '.fido-undo.json';
|
||||
export const SUSPEND_FILE = process.env.SUSPEND_FILE || '.fido-suspends.json';
|
||||
export const TAVILY_API_KEY = process.env.TAVILY_API_KEY;
|
||||
|
||||
// Root dir: --dir flag wins, then env ROOT_DIR, then cwd
|
||||
// Root dir: first positional arg wins, then env ROOT_DIR, then cwd
|
||||
const args = process.argv.slice(2);
|
||||
const dirFlagIdx = args.indexOf('--dir');
|
||||
export const ROOT_DIR = path.resolve(
|
||||
(dirFlagIdx !== -1 && args[dirFlagIdx + 1]) || process.env.ROOT_DIR || process.cwd()
|
||||
args[0] || process.env.ROOT_DIR || process.cwd()
|
||||
);
|
||||
|
||||
214
src/fido.js
214
src/fido.js
@@ -17,22 +17,70 @@
|
||||
*
|
||||
* Usage:
|
||||
* node src/fido.js
|
||||
* node src/fido.js --dir /path/to/project
|
||||
* node src/fido.js /path/to/project
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { editor } from '@inquirer/prompts';
|
||||
import { editor, input, select } from '@inquirer/prompts';
|
||||
import { marked } from 'marked';
|
||||
import { markedTerminal } from 'marked-terminal';
|
||||
|
||||
import { LLMOrbiter, OpenRouterProvider } from 'llmorbiter';
|
||||
|
||||
import { API_KEY, MODEL, UNDO_FILE, ROOT_DIR } from './config.js';
|
||||
import { API_KEY, MODEL, UNDO_FILE, SUSPEND_FILE, ROOT_DIR } from './config.js';
|
||||
import { listDir } from './dir-listing.js';
|
||||
import { loadUndoLog, getUndoLog } from './undo.js';
|
||||
import { tools } from './tools/index.js';
|
||||
import { loadSuspends, addSuspend, getSuspends } from './suspend.js';
|
||||
import { tools, toolCallNotifications, toolResultNotifications } from './tools/index.js';
|
||||
import { printToolCall, emitGenericToolResult } from './tools/notify.js';
|
||||
import { autocompleteInput } from './autocomplete-input.js';
|
||||
import { COMMANDS, findCommand } from './slash-commands.js';
|
||||
|
||||
// ─── Markdown → console renderer ───────────────────────────────────────────────
|
||||
|
||||
// Configure marked to emit ANSI-styled terminal output. Width accounts for the
|
||||
// 2-space UI indent applied when printing the assistant's response.
|
||||
marked.use(markedTerminal({ reflowText: true, width: 76, tab: 2 }));
|
||||
|
||||
// ─── Background-color blocks ───────────────────────────────────────────────────
|
||||
//
|
||||
// marked-terminal (and chalk) end every styled span with a full reset
|
||||
// (\x1b[0m). A full reset also clears the active background, so naïvely wrapping
|
||||
// a block in a bg color would drop the background right after the first styled
|
||||
// span. To keep the background alive across a block we:
|
||||
// 1. re-emit the background open code immediately after every reset, and
|
||||
// 2. right-pad each line to a fixed width so the block reads as a rectangle.
|
||||
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
const RESET = '\x1b[0m';
|
||||
const BG_CLOSE = '\x1b[49m';
|
||||
|
||||
const ASSISTANT_BG_OPEN = '\x1b[48;2;30;41;59m'; // #1e293b — dark slate
|
||||
const SHELL_BG_OPEN = '\x1b[100m'; // dark gray (bgBlackBright)
|
||||
|
||||
const ASSISTANT_BLOCK_WIDTH = 76;
|
||||
const SHELL_BLOCK_WIDTH = 76;
|
||||
|
||||
const stripAnsi = (s) => s.replace(ANSI_RE, '');
|
||||
|
||||
/**
|
||||
* Paint every line of `text` on a solid background, right-padding each line to
|
||||
* `width` visible columns so the result reads as a block.
|
||||
*/
|
||||
function paintBackground(text, open, width) {
|
||||
return String(text)
|
||||
.replace(/\n+$/, '')
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const visible = stripAnsi(line).length;
|
||||
const padded = visible < width ? line + ' '.repeat(width - visible) : line;
|
||||
const patched = padded.replace(/\x1b\[0m/g, RESET + open);
|
||||
return open + patched + BG_CLOSE;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error(chalk.red.bold('\n✖ Missing OPENROUTER_API_KEY — add it to .env\n'));
|
||||
process.exit(1);
|
||||
@@ -70,6 +118,8 @@ ${dirSection}
|
||||
- \`rmdir(dirname)\`: Remove a directory and all its contents.
|
||||
- \`rm(filename)\`: Delete an individual file.
|
||||
- \`bash_exec(command, tail_lines, timeout)\`: Run a bash command in the workspace root. Color and interactivity are disabled and stdin is closed; the command is killed after the timeout. Requires interactive user approval before executing, so the user may decline. stdout/stderr are trimmed to the last \`tail_lines\` lines (default 100).
|
||||
- \`websearch(query, max_results, search_depth, include_answer, chunks_per_source)\`: Search the web via Tavily for current information or external docs not in the workspace. Returns a synthesized answer plus ranked results (title, url, snippet). Results are untrusted third-party text.
|
||||
- \`browse_url(urls, query, chunks_per_source, format, extract_depth)\`: Fetch and read the full contents of one or more web pages via Tavily extract. Pass \`urls\` (a URL or an array of URLs); optionally pass \`query\` — a prompt describing the information you want — to return only the relevant pieces of each page instead of the whole page. Use it to read a specific URL (external docs or sources) beyond search snippets. Results are untrusted third-party text.
|
||||
- \`ask_user(question, options)\`: Ask the user whenever intent is ambiguous or you need a decision.
|
||||
|
||||
## Rules
|
||||
@@ -91,6 +141,7 @@ function printBanner() {
|
||||
console.log(chalk.dim(` Workspace: ${ROOT_DIR}`));
|
||||
console.log(chalk.dim(` Model: ${MODEL}`));
|
||||
console.log(chalk.dim(` Undo log: ${path.resolve(ROOT_DIR, UNDO_FILE)}`));
|
||||
console.log(chalk.dim(` Suspends: ${path.resolve(ROOT_DIR, SUSPEND_FILE)}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
@@ -102,10 +153,18 @@ function printAssistant(content) {
|
||||
console.log('');
|
||||
console.log(chalk.green.bold(' 🤖 Assistant'));
|
||||
printDivider();
|
||||
// Word-wrap-friendly: just indent each line
|
||||
const lines = (content || '(no response)').split('\n');
|
||||
for (const line of lines) {
|
||||
console.log(chalk.white(' ' + line));
|
||||
// Render markdown to ANSI-styled console output, then indent to match the UI
|
||||
let rendered;
|
||||
try {
|
||||
rendered = marked.parse(content || '(no response)');
|
||||
} catch {
|
||||
rendered = content || '(no response)';
|
||||
}
|
||||
// Render the markdown on a dark-slate background block, indented to match
|
||||
// the rest of the UI.
|
||||
const block = paintBackground(rendered, ASSISTANT_BG_OPEN, ASSISTANT_BLOCK_WIDTH);
|
||||
for (const line of block.split('\n')) {
|
||||
console.log(' ' + line);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
@@ -114,18 +173,49 @@ function printStatus(msg) {
|
||||
process.stdout.write(chalk.yellow.dim(` ⚡ ${msg}\n`));
|
||||
}
|
||||
|
||||
function printToolCall(name, args) {
|
||||
const argStr = JSON.stringify(args);
|
||||
const display = argStr.length > 120 ? argStr.slice(0, 117) + '...' : argStr;
|
||||
console.log(chalk.cyan(` ⚙ Tool: ${chalk.bold(name)}(${display})`));
|
||||
}
|
||||
|
||||
function printToolResult(name, result) {
|
||||
// Shell output gets a dedicated render: full stdout/stderr on a gray block.
|
||||
if (name === 'bash_exec') {
|
||||
let r = result;
|
||||
if (typeof r === 'string') {
|
||||
try { r = JSON.parse(r); } catch { /* not JSON, fall through */ }
|
||||
}
|
||||
if (r && typeof r === 'object' && ('stdout' in r || 'stderr' in r)) {
|
||||
printShellResult(r);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const str = typeof result === 'string' ? result : JSON.stringify(result);
|
||||
const display = str.length > 200 ? str.slice(0, 197) + '...' : str;
|
||||
console.log(chalk.dim(` ↳ ${display}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a bash_exec result: a one-line summary, then stdout/stderr on a
|
||||
* dark-gray background block (indented to match the rest of the UI).
|
||||
*/
|
||||
function printShellResult(r) {
|
||||
const meta = [];
|
||||
if (r.exitCode !== undefined && r.exitCode !== null) meta.push(`exit ${r.exitCode}`);
|
||||
if (r.signal) meta.push(`signal ${r.signal}`);
|
||||
if (r.timedOut) meta.push('timed out');
|
||||
console.log(chalk.dim(` ↳ bash_exec${meta.length ? ' (' + meta.join(', ') + ')' : ''}`));
|
||||
|
||||
const body = [];
|
||||
if (r.stdout) body.push(r.stdout.replace(/\n+$/, ''));
|
||||
if (r.stderr) body.push(r.stderr.replace(/\n+$/, ''));
|
||||
const text = body.join('\n').replace(/\n/g, '\n ');
|
||||
if (text.trim()) {
|
||||
const block = paintBackground(text, SHELL_BG_OPEN, SHELL_BLOCK_WIDTH);
|
||||
for (const line of block.split('\n')) {
|
||||
console.log(' ' + line);
|
||||
}
|
||||
}
|
||||
|
||||
if (r.truncationNote) console.log(chalk.dim(` ↳ ${r.truncationNote}`));
|
||||
if (r.note) console.log(chalk.dim(` ↳ ${r.note}`));
|
||||
}
|
||||
|
||||
function printToolError(name, error) {
|
||||
console.log(chalk.red(` ✖ Tool error [${name}]: ${error}`));
|
||||
}
|
||||
@@ -137,6 +227,12 @@ function printUsage(u) {
|
||||
console.log(chalk.dim(` 📊 ${parts.join(' ')}`));
|
||||
}
|
||||
|
||||
function formatWhen(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return Number.isNaN(d.getTime()) ? '' : d.toLocaleString();
|
||||
}
|
||||
|
||||
function printCommandHelp() {
|
||||
const width = Math.max(...COMMANDS.map((c) => c.name.length));
|
||||
console.log('');
|
||||
@@ -171,14 +267,24 @@ async function promptMultiline(message = 'Compose or paste your text, then save
|
||||
|
||||
async function main() {
|
||||
loadUndoLog();
|
||||
loadSuspends();
|
||||
printBanner();
|
||||
|
||||
const provider = new OpenRouterProvider({ apiKey: API_KEY });
|
||||
|
||||
const orbiter = new LLMOrbiter(provider, {
|
||||
onStatus: (s) => printStatus(s),
|
||||
onToolCall: (c) => printToolCall(c.name, c.args),
|
||||
onToolResult: (r) => printToolResult(r.name, r.result),
|
||||
onToolCall: (c) => {
|
||||
const notify = toolCallNotifications[c.name];
|
||||
if (notify) notify(c.args);
|
||||
else printToolCall(c.name, c.args);
|
||||
},
|
||||
onToolResult: (r) => {
|
||||
const notifyResult = toolResultNotifications[r.name];
|
||||
if (notifyResult) notifyResult(r.result);
|
||||
else emitGenericToolResult(r.name, r.result);
|
||||
printToolResult(r.name, r.result);
|
||||
},
|
||||
onToolError: (e) => printToolError(e.name, e.error),
|
||||
onUsage: (u) => printUsage(u),
|
||||
});
|
||||
@@ -224,10 +330,6 @@ async function main() {
|
||||
console.log(chalk.dim('\n Goodbye! 🐕\n'));
|
||||
break;
|
||||
}
|
||||
if (command === '/help') {
|
||||
printCommandHelp();
|
||||
continue;
|
||||
}
|
||||
if (command === '/paste') {
|
||||
try {
|
||||
const pasted = await promptMultiline();
|
||||
@@ -259,6 +361,78 @@ async function main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === '/suspend') {
|
||||
// Description is the text after the command; ask for one if omitted.
|
||||
let description = userInput.trim().slice('/suspend'.length).trim();
|
||||
if (!description) {
|
||||
try {
|
||||
description = (
|
||||
await input({
|
||||
message: chalk.bold.cyan('Short description for this suspend'),
|
||||
theme: {
|
||||
prefix: chalk.cyan('›'),
|
||||
style: { answer: chalk.white, message: chalk.bold.cyan },
|
||||
},
|
||||
})
|
||||
).trim();
|
||||
} catch {
|
||||
console.log(chalk.yellow('\n Suspend cancelled.\n'));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!description) {
|
||||
console.log(chalk.yellow('\n No description given — suspend cancelled.\n'));
|
||||
continue;
|
||||
}
|
||||
if (messages.length <= 1) {
|
||||
console.log(chalk.yellow('\n Nothing to suspend yet — say something first.\n'));
|
||||
continue;
|
||||
}
|
||||
addSuspend(description, messages);
|
||||
const count = getSuspends().length;
|
||||
console.log(chalk.green(`\n ⏸ Suspended: ${description}`));
|
||||
console.log(chalk.dim(` ${count} snapshot${count === 1 ? '' : 's'} in ${SUSPEND_FILE}\n`));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === '/recall') {
|
||||
const suspends = getSuspends();
|
||||
if (suspends.length === 0) {
|
||||
console.log(chalk.yellow('\n Nothing to recall.\n'));
|
||||
continue;
|
||||
}
|
||||
let choice;
|
||||
try {
|
||||
choice = await select({
|
||||
message: chalk.bold.cyan('Recall a suspended conversation'),
|
||||
choices: suspends.map((s, i) => ({
|
||||
name: `${s.description} ${chalk.dim(formatWhen(s.createdAt))}`,
|
||||
value: i,
|
||||
short: s.description,
|
||||
})),
|
||||
theme: {
|
||||
prefix: chalk.cyan('›'),
|
||||
style: { message: chalk.bold.cyan, highlight: chalk.cyan.bold },
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
console.log(chalk.yellow('\n Recall cancelled.\n'));
|
||||
continue;
|
||||
}
|
||||
const snap = suspends[choice];
|
||||
const restored = Array.isArray(snap.messages) ? snap.messages : [];
|
||||
// Keep the freshly built system prompt (workspace may have changed)
|
||||
// and restore the rest of the conversation verbatim.
|
||||
messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...restored.filter((m) => m.role !== 'system'),
|
||||
];
|
||||
const turns = messages.length - 1;
|
||||
console.log(chalk.green(`\n ▶ Recalled: ${snap.description}`));
|
||||
console.log(chalk.dim(` ${turns} message${turns === 1 ? '' : 's'} restored.\n`));
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
try {
|
||||
|
||||
@@ -9,33 +9,22 @@
|
||||
*/
|
||||
|
||||
export const COMMANDS = [
|
||||
{ name: '/help', description: 'Show this list of commands' },
|
||||
{ name: '/paste', description: 'Compose or paste multiline input in $EDITOR' },
|
||||
{ name: '/edit', description: 'Alias for /paste' },
|
||||
{ name: '/suspend', description: 'Save the current conversation under a short description' },
|
||||
{ name: '/recall', description: 'Restore a previously suspended conversation' },
|
||||
{ name: '/undo', description: 'Show recent file changes from the undo log' },
|
||||
{ name: '/quit', description: 'Exit Fido' },
|
||||
{ name: '/exit', description: 'Alias for /quit' },
|
||||
];
|
||||
|
||||
/** Aliases resolve to a canonical command name at dispatch time. */
|
||||
const ALIASES = {
|
||||
'/edit': '/paste',
|
||||
'/exit': '/quit',
|
||||
};
|
||||
|
||||
/** Return the commands whose name starts with `term` (case-insensitive). */
|
||||
export function filterCommands(term) {
|
||||
const t = (term || '').toLowerCase();
|
||||
return COMMANDS.filter((c) => c.name.toLowerCase().startsWith(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a raw input line to a canonical command name (e.g. "/edit" ->
|
||||
* "/paste"), or `null` when the line is not a known directive.
|
||||
*/
|
||||
/** Resolve a raw input line to a command name, or `null` if unknown. */
|
||||
export function findCommand(line) {
|
||||
if (typeof line !== 'string' || !line.startsWith('/')) return null;
|
||||
const name = line.trim().split(/\s+/)[0].toLowerCase();
|
||||
if (ALIASES[name]) return ALIASES[name];
|
||||
return COMMANDS.some((c) => c.name === name) ? name : null;
|
||||
}
|
||||
|
||||
52
src/suspend.js
Normal file
52
src/suspend.js
Normal file
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Suspend snapshots — JSON array, newest-first, persisted on every save.
|
||||
*
|
||||
* `/suspend <description>` captures the current conversation state under a
|
||||
* short description so it can be listed and restored later via `/recall`.
|
||||
* Each entry is:
|
||||
* { description, createdAt, messages }
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ROOT_DIR, SUSPEND_FILE } from './config.js';
|
||||
|
||||
let snapshots = [];
|
||||
|
||||
function suspendPath() {
|
||||
return path.resolve(ROOT_DIR, SUSPEND_FILE);
|
||||
}
|
||||
|
||||
export function loadSuspends() {
|
||||
const p = suspendPath();
|
||||
if (fs.existsSync(p)) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
snapshots = Array.isArray(data) ? data : [];
|
||||
} catch {
|
||||
snapshots = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSuspends() {
|
||||
fs.writeFileSync(suspendPath(), JSON.stringify(snapshots, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
/** Capture `messages` under `description` (newest first) and persist. */
|
||||
export function addSuspend(description, messages) {
|
||||
const entry = {
|
||||
description,
|
||||
createdAt: new Date().toISOString(),
|
||||
messages,
|
||||
};
|
||||
snapshots.unshift(entry);
|
||||
saveSuspends();
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function getSuspends() {
|
||||
return snapshots;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import chalk from 'chalk';
|
||||
import { input, select } from '@inquirer/prompts';
|
||||
import { defineTool } from 'llmorbiter';
|
||||
import { emitToolCall, clip } from './notify.js';
|
||||
|
||||
export const askUserTool = defineTool({
|
||||
name: 'ask_user',
|
||||
@@ -89,3 +90,10 @@ export const askUserTool = defineTool({
|
||||
return { answer };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the question being asked.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
emitToolCall(askUserTool.name, clip(JSON.stringify(args.question ?? '')));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import chalk from 'chalk';
|
||||
import { confirm } from '@inquirer/prompts';
|
||||
import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { ROOT_DIR } from '../config.js';
|
||||
import { emitToolCall, emitToolResult, clip } from './notify.js';
|
||||
|
||||
const DEFAULT_TAIL_LINES = 100; // trailing lines of output returned to the model
|
||||
const DEFAULT_TIMEOUT_MS = 30_000; // 30s hard limit per command
|
||||
@@ -255,3 +256,25 @@ export const bashExecTool = defineTool({
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the command flattened to one line, plus a custom timeout
|
||||
* when one is set. The approval prompt that follows shows the full command.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
const { command, timeout } = args;
|
||||
const oneLine = String(command ?? '').replace(/\s*\n\s*/g, ' ; ');
|
||||
const extras = Number.isInteger(timeout) && timeout !== DEFAULT_TIMEOUT_MS ? ` timeout=${timeout}` : '';
|
||||
emitToolCall(bashExecTool.name, clip(`${oneLine}${extras}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Result notification: the process exit status (or signal / timeout).
|
||||
*/
|
||||
export function notifyResult(result = {}) {
|
||||
const parts = [];
|
||||
if (result.exitCode !== undefined && result.exitCode !== null) parts.push(`exit ${result.exitCode}`);
|
||||
else if (result.signal) parts.push(`signal ${result.signal}`);
|
||||
if (result.timedOut) parts.push('timed out');
|
||||
emitToolResult(bashExecTool.name, parts.join(' ') || 'done');
|
||||
}
|
||||
|
||||
179
src/tools/browse-url.js
Normal file
179
src/tools/browse-url.js
Normal file
@@ -0,0 +1,179 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* browse_url — fetch and extract the contents of one or more web pages via
|
||||
* Tavily, returning the page text (or just the relevant pieces when a `query`
|
||||
* is supplied).
|
||||
*
|
||||
* Design notes:
|
||||
* - Requires a Tavily API key (TAVILY_API_KEY in .env). The client is created
|
||||
* lazily and cached, so a missing key surfaces as a clean ToolError only
|
||||
* when the tool is actually used.
|
||||
* - This tool reaches the network, so results are untrusted third-party text;
|
||||
* the model should treat them accordingly.
|
||||
* - `urls` accepts either a single URL string or an array of them; it is
|
||||
* normalized to an array before being handed to Tavily's extract().
|
||||
* - `query` is an optional LLM prompt used to pull only the relevant pieces
|
||||
* of information out of a page, so the whole page need not be returned.
|
||||
*/
|
||||
|
||||
import { tavily } from '@tavily/core';
|
||||
import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { TAVILY_API_KEY } from '../config.js';
|
||||
import { emitToolCall, emitToolResult, clip, formatBytes, resultSize, plural } from './notify.js';
|
||||
|
||||
const DEFAULT_FORMAT = 'text'; // default output format
|
||||
const FORMATS = ['markdown', 'text'];
|
||||
const EXTRACT_DEPTHS = ['basic', 'advanced'];
|
||||
const DEFAULT_EXTRACT_DEPTH = 'basic';
|
||||
const DEFAULT_CHUNKS_PER_SOURCE = 1;
|
||||
const MAX_URLS = 20; // hard upper bound to keep requests sane
|
||||
|
||||
let client = null;
|
||||
|
||||
/** Lazily construct (and cache) the Tavily client, failing cleanly if unset. */
|
||||
function getClient() {
|
||||
if (!TAVILY_API_KEY) {
|
||||
throw new ToolError(
|
||||
'browse_url requires a Tavily API key. Add TAVILY_API_KEY=... to your .env file.'
|
||||
);
|
||||
}
|
||||
if (!client) {
|
||||
client = tavily({ apiKey: TAVILY_API_KEY });
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/** Normalize the `urls` argument to a validated array of http(s) URLs. */
|
||||
function normalizeUrls(urls) {
|
||||
let list;
|
||||
if (typeof urls === 'string') list = [urls];
|
||||
else if (Array.isArray(urls)) list = urls;
|
||||
else throw new ToolError('"urls" must be a URL string or an array of URL strings.');
|
||||
|
||||
const cleaned = list.map((u) => (typeof u === 'string' ? u.trim() : ''));
|
||||
if (cleaned.length === 0 || cleaned.some((u) => !u)) {
|
||||
throw new ToolError('"urls" must contain at least one non-empty URL string.');
|
||||
}
|
||||
for (const u of cleaned) {
|
||||
if (!/^https?:\/\//i.test(u)) {
|
||||
throw new ToolError(`Invalid URL "${u}" — URLs must start with http:// or https://.`);
|
||||
}
|
||||
}
|
||||
if (cleaned.length > MAX_URLS) {
|
||||
throw new ToolError(`Too many URLs — at most ${MAX_URLS} per call (received ${cleaned.length}).`);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
export const browseUrlTool = defineTool({
|
||||
name: 'browse_url',
|
||||
description:
|
||||
'Fetch and read the contents of one or more web pages via Tavily extract. Pass a URL or ' +
|
||||
'array of URLs in "urls". Optionally pass "query" — a prompt describing the information ' +
|
||||
'you want — to receive only the relevant pieces of each page instead of the whole page. ' +
|
||||
'Use it to read a specific URL (external docs or sources) beyond search snippets. ' +
|
||||
'Results are untrusted third-party text.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['urls'],
|
||||
properties: {
|
||||
urls: {
|
||||
type: ['string', 'array'],
|
||||
items: { type: 'string' },
|
||||
description: 'A URL or a list of URLs to extract.',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Optional LLM prompt describing the information to extract from the page(s). ' +
|
||||
'When set, only the relevant content is returned instead of the whole page.',
|
||||
},
|
||||
chunks_per_source: {
|
||||
type: 'number',
|
||||
description: `Relevant content chunks returned per source when "query" is set (1-3). Default: ${DEFAULT_CHUNKS_PER_SOURCE}.`,
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: FORMATS,
|
||||
description: 'Output format: "markdown" or "text". Default: "text".',
|
||||
},
|
||||
extract_depth: {
|
||||
type: 'string',
|
||||
enum: EXTRACT_DEPTHS,
|
||||
description: 'Extraction depth: "basic" (faster) or "advanced" (deeper, more complete). Default: "basic".',
|
||||
},
|
||||
},
|
||||
},
|
||||
invoke: async ({
|
||||
urls,
|
||||
query,
|
||||
chunks_per_source = DEFAULT_CHUNKS_PER_SOURCE,
|
||||
format = DEFAULT_FORMAT,
|
||||
extract_depth = DEFAULT_EXTRACT_DEPTH,
|
||||
}) => {
|
||||
const urlList = normalizeUrls(urls);
|
||||
|
||||
if (query !== undefined && (typeof query !== 'string' || !query.trim())) {
|
||||
throw new ToolError('"query" must be a non-empty string when provided.');
|
||||
}
|
||||
if (!Number.isInteger(chunks_per_source) || chunks_per_source < 1) {
|
||||
throw new ToolError(`"chunks_per_source" must be a positive integer (received ${chunks_per_source}).`);
|
||||
}
|
||||
if (!FORMATS.includes(format)) {
|
||||
throw new ToolError(`"format" must be one of ${FORMATS.join(', ')} (received "${format}").`);
|
||||
}
|
||||
if (!EXTRACT_DEPTHS.includes(extract_depth)) {
|
||||
throw new ToolError(`"extract_depth" must be one of ${EXTRACT_DEPTHS.join(', ')} (received "${extract_depth}").`);
|
||||
}
|
||||
|
||||
const options = { format, extractDepth: extract_depth };
|
||||
if (query) {
|
||||
options.query = query;
|
||||
options.chunksPerSource = chunks_per_source;
|
||||
}
|
||||
|
||||
const response = await getClient().extract(urlList, options);
|
||||
|
||||
const results = (response.results || []).map((r) => {
|
||||
const item = { url: r.url, content: r.rawContent };
|
||||
if (r.title) item.title = r.title;
|
||||
return item;
|
||||
});
|
||||
const failed = (response.failedResults || []).map((f) => ({
|
||||
url: f.url,
|
||||
error: f.error,
|
||||
}));
|
||||
|
||||
const result = { urls: urlList, results };
|
||||
if (query) result.query = query;
|
||||
if (failed.length) result.failedResults = failed;
|
||||
if (response.responseTime !== undefined) result.responseTime = response.responseTime;
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the URL(s) plus any non-default extract options.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
const { urls, query, format, extract_depth } = args;
|
||||
const list = Array.isArray(urls) ? urls : [urls];
|
||||
const parts = [clip(list.filter(Boolean).join(', '))];
|
||||
if (query) parts.push(`query=${clip(JSON.stringify(query))}`);
|
||||
if (format && format !== DEFAULT_FORMAT) parts.push(`format=${format}`);
|
||||
if (extract_depth && extract_depth !== DEFAULT_EXTRACT_DEPTH) parts.push(`depth=${extract_depth}`);
|
||||
emitToolCall(browseUrlTool.name, clip(parts.join(' ')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Result notification: pages fetched (and any failures) plus the payload size.
|
||||
*/
|
||||
export function notifyResult(result = {}) {
|
||||
const n = Array.isArray(result.results) ? result.results.length : 0;
|
||||
const failed = Array.isArray(result.failedResults) ? result.failedResults.length : 0;
|
||||
let detail = plural(n, 'page');
|
||||
if (failed) detail += `, ${failed} failed`;
|
||||
emitToolResult(browseUrlTool.name, `${detail} ${formatBytes(resultSize(result))}`);
|
||||
}
|
||||
@@ -1,18 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Tool registry — one module per tool, aggregated here in registration order.
|
||||
* Tool registry \u2014 one module per tool, aggregated here in registration order.
|
||||
*
|
||||
* Each tool module also exports a `notifyCall(args)` renderer. Those are
|
||||
* collected into `toolCallNotifications` (tool name -> renderer) so the UI can
|
||||
* print a tailored notification for each individual tool call.
|
||||
*/
|
||||
|
||||
import { listFilesTool } from './list-files.js';
|
||||
import { readFileTool } from './read-file.js';
|
||||
import { ripgrepTool } from './ripgrep.js';
|
||||
import { patchTool } from './patch.js';
|
||||
import { mkdirTool } from './mkdir.js';
|
||||
import { rmdirTool } from './rmdir.js';
|
||||
import { rmTool } from './rm.js';
|
||||
import { bashExecTool } from './bash-exec.js';
|
||||
import { askUserTool } from './ask-user.js';
|
||||
import { listFilesTool, notifyCall as listFilesNotify, notifyResult as listFilesResult } from './list-files.js';
|
||||
import { readFileTool, notifyCall as readFileNotify, notifyResult as readFileResult } from './read-file.js';
|
||||
import { ripgrepTool, notifyCall as ripgrepNotify, notifyResult as ripgrepResult } from './ripgrep.js';
|
||||
import { patchTool, notifyCall as patchNotify } from './patch.js';
|
||||
import { mkdirTool, notifyCall as mkdirNotify } from './mkdir.js';
|
||||
import { rmdirTool, notifyCall as rmdirNotify } from './rmdir.js';
|
||||
import { rmTool, notifyCall as rmNotify } from './rm.js';
|
||||
import { bashExecTool, notifyCall as bashExecNotify, notifyResult as bashExecResult } from './bash-exec.js';
|
||||
import { websearchTool, notifyCall as websearchNotify, notifyResult as websearchResult } from './websearch.js';
|
||||
import { browseUrlTool, notifyCall as browseUrlNotify, notifyResult as browseUrlResult } from './browse-url.js';
|
||||
import { askUserTool, notifyCall as askUserNotify } from './ask-user.js';
|
||||
|
||||
export {
|
||||
listFilesTool,
|
||||
@@ -23,6 +29,8 @@ export {
|
||||
rmdirTool,
|
||||
rmTool,
|
||||
bashExecTool,
|
||||
websearchTool,
|
||||
browseUrlTool,
|
||||
askUserTool,
|
||||
};
|
||||
|
||||
@@ -35,5 +43,45 @@ export const tools = [
|
||||
rmdirTool,
|
||||
rmTool,
|
||||
bashExecTool,
|
||||
websearchTool,
|
||||
browseUrlTool,
|
||||
askUserTool,
|
||||
];
|
||||
|
||||
/**
|
||||
* Per-tool call-notification renderers, keyed by tool name. The UI looks up the
|
||||
* renderer for the tool being invoked so each tool gets an individual
|
||||
* notification; tools without an entry fall back to a generic renderer.
|
||||
*/
|
||||
export const toolCallNotifications = Object.fromEntries(
|
||||
[
|
||||
[listFilesTool, listFilesNotify],
|
||||
[readFileTool, readFileNotify],
|
||||
[ripgrepTool, ripgrepNotify],
|
||||
[patchTool, patchNotify],
|
||||
[mkdirTool, mkdirNotify],
|
||||
[rmdirTool, rmdirNotify],
|
||||
[rmTool, rmNotify],
|
||||
[bashExecTool, bashExecNotify],
|
||||
[websearchTool, websearchNotify],
|
||||
[browseUrlTool, browseUrlNotify],
|
||||
[askUserTool, askUserNotify],
|
||||
].map(([tool, notify]) => [tool.name, notify]),
|
||||
);
|
||||
|
||||
/**
|
||||
* Per-tool result-notification renderers, keyed by tool name. The UI looks up
|
||||
* the renderer for the tool that just finished so each tool gets a tailored
|
||||
* completion notification (payload size, exit code, and so on); tools without
|
||||
* an entry fall back to a generic size renderer.
|
||||
*/
|
||||
export const toolResultNotifications = Object.fromEntries(
|
||||
[
|
||||
[listFilesTool, listFilesResult],
|
||||
[readFileTool, readFileResult],
|
||||
[ripgrepTool, ripgrepResult],
|
||||
[bashExecTool, bashExecResult],
|
||||
[websearchTool, websearchResult],
|
||||
[browseUrlTool, browseUrlResult],
|
||||
].map(([tool, notify]) => [tool.name, notify]),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from 'fs';
|
||||
import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { resolveSafe } from '../sandbox.js';
|
||||
import { listDir } from '../dir-listing.js';
|
||||
import { emitToolCall, emitToolResult, clip, formatBytes, resultSize, plural } from './notify.js';
|
||||
|
||||
export const listFilesTool = defineTool({
|
||||
name: 'list_files',
|
||||
@@ -64,3 +65,27 @@ export const listFilesTool = defineTool({
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the target directory plus any traversal flags in effect.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
const { dirname = '.', recursive = false, includeDotDirs = false } = args;
|
||||
const flags = [];
|
||||
if (recursive) flags.push('recursive');
|
||||
if (includeDotDirs) flags.push('includeDotDirs');
|
||||
const detail = `${JSON.stringify(dirname)}${flags.length ? ' ' + flags.join(' ') : ''}`;
|
||||
emitToolCall(listFilesTool.name, clip(detail));
|
||||
}
|
||||
|
||||
/**
|
||||
* Result notification: how many entries came back and the payload size.
|
||||
*/
|
||||
export function notifyResult(result = {}) {
|
||||
const returned = result.returned ?? (Array.isArray(result.entries) ? result.entries.length : 0);
|
||||
const total = result.total ?? returned;
|
||||
const count = total > returned
|
||||
? `${returned} of ${total} entries`
|
||||
: plural(returned, 'entry', 'entries');
|
||||
emitToolResult(listFilesTool.name, `${count} ${formatBytes(resultSize(result))}`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from 'fs';
|
||||
import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { resolveSafe } from '../sandbox.js';
|
||||
import { pushUndo } from '../undo.js';
|
||||
import { emitToolCall, clip } from './notify.js';
|
||||
|
||||
export const mkdirTool = defineTool({
|
||||
name: 'mkdir',
|
||||
@@ -32,3 +33,10 @@ export const mkdirTool = defineTool({
|
||||
return { message: `Directory created: "${dirname}"`, created: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the directory to create.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
emitToolCall(mkdirTool.name, clip(JSON.stringify(args.dirname ?? '')));
|
||||
}
|
||||
|
||||
89
src/tools/notify.js
Normal file
89
src/tools/notify.js
Normal file
@@ -0,0 +1,89 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Shared helpers for per-tool call notifications.
|
||||
*
|
||||
* Every tool module exports a `notifyCall(args)` renderer that prints a
|
||||
* tailored notification when the model invokes that tool. These helpers keep
|
||||
* the styling consistent across tools and provide the generic fallback used
|
||||
* for any tool that does not supply its own renderer.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
// Longest argument string echoed in a notification before it is elided.
|
||||
const MAX_ARG_LEN = 120;
|
||||
|
||||
/** Truncate `text` to at most `max` characters, appending an ellipsis. */
|
||||
export function clip(text, max = MAX_ARG_LEN) {
|
||||
const str = String(text ?? '');
|
||||
return str.length > max ? str.slice(0, max - 3) + '...' : str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the one-line tool-call notification:
|
||||
* "\u2699 Tool: <name>(<detail>)"
|
||||
*/
|
||||
export function emitToolCall(name, detail = '') {
|
||||
console.log(chalk.cyan(` \u2699 Tool: ${chalk.bold(name)}(${detail})`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic renderer \u2014 serialize the args as JSON and emit the notification.
|
||||
* Used as the fallback for tools that do not supply their own `notifyCall`.
|
||||
*/
|
||||
export function printToolCall(name, args) {
|
||||
emitToolCall(name, clip(JSON.stringify(args)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the one-line tool-completion notification, the counterpart to
|
||||
* `emitToolCall`:
|
||||
* " \u2713 <name> <detail>"
|
||||
* Emitted when a tool finishes (see `onToolResult`) so every call is bracketed
|
||||
* by a "started" and a "finished" line.
|
||||
*/
|
||||
export function emitToolResult(name, detail = '') {
|
||||
const suffix = detail ? ' ' + detail : '';
|
||||
console.log(chalk.green(` \u2713 ${chalk.bold(name)}${suffix}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a byte count for display. The requested unit is kilobytes; small
|
||||
* payloads fall back to bytes (so a tiny result never reads as "0.0 KB") and
|
||||
* large ones roll up to megabytes.
|
||||
*/
|
||||
export function formatBytes(bytes) {
|
||||
const n = Math.max(0, Number(bytes) || 0);
|
||||
if (n < 1024) return `${n} B`;
|
||||
const kb = n / 1024;
|
||||
return kb < 1024 ? `${kb.toFixed(1)} KB` : `${(kb / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Byte length of a tool result as it is handed back to the model (the
|
||||
* JSON-serialized payload). Used for the size shown in result notifications.
|
||||
*/
|
||||
export function resultSize(value) {
|
||||
let str;
|
||||
try {
|
||||
str = typeof value === 'string' ? value : JSON.stringify(value ?? '');
|
||||
} catch {
|
||||
str = String(value ?? '');
|
||||
}
|
||||
return Buffer.byteLength(str, 'utf8');
|
||||
}
|
||||
|
||||
/** Render "1 entry" / "2 entries" (or a custom plural form). */
|
||||
export function plural(n, singular, pluralForm) {
|
||||
return `${n} ${n === 1 ? singular : (pluralForm ?? (singular + 's'))}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic result renderer - emit a completion notification carrying the payload
|
||||
* size. Used as the fallback for tools that do not supply their own
|
||||
* `notifyResult`.
|
||||
*/
|
||||
export function emitGenericToolResult(name, result) {
|
||||
emitToolResult(name, formatBytes(resultSize(result)));
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import path from 'path';
|
||||
import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { resolveSafe } from '../sandbox.js';
|
||||
import { pushUndo } from '../undo.js';
|
||||
import { emitToolCall, clip } from './notify.js';
|
||||
|
||||
// ─── Patch executor ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -222,3 +223,14 @@ Example search/replace:
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: how many operations and which files they touch.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
const ops = Array.isArray(args.operations) ? args.operations : [];
|
||||
const files = [...new Set(ops.map((op) => op?.file).filter(Boolean))];
|
||||
const n = ops.length;
|
||||
const detail = `${n} op${n === 1 ? '' : 's'} \u2192 ${files.join(', ')}`;
|
||||
emitToolCall(patchTool.name, clip(detail));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import fs from 'fs';
|
||||
import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { resolveSafe } from '../sandbox.js';
|
||||
import { emitToolCall, emitToolResult, clip, formatBytes, resultSize, plural } from './notify.js';
|
||||
|
||||
export const readFileTool = defineTool({
|
||||
name: 'read_file',
|
||||
@@ -55,3 +56,25 @@ export const readFileTool = defineTool({
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the file plus any non-default seek/limit windowing.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
const { filename, seek = 0, limit = 200 } = args;
|
||||
const extras = [];
|
||||
if (seek) extras.push(`seek=${seek}`);
|
||||
if (limit !== 200) extras.push(`limit=${limit}`);
|
||||
const detail = `${JSON.stringify(filename)}${extras.length ? ' ' + extras.join(' ') : ''}`;
|
||||
emitToolCall(readFileTool.name, clip(detail));
|
||||
}
|
||||
|
||||
/**
|
||||
* Result notification: lines returned and the payload size.
|
||||
*/
|
||||
export function notifyResult(result = {}) {
|
||||
const returned = result.returnedLines ?? 0;
|
||||
const total = result.totalLines ?? returned;
|
||||
const lines = total > returned ? `${returned} of ${total} lines` : plural(returned, 'line');
|
||||
emitToolResult(readFileTool.name, `${lines} ${formatBytes(resultSize(result))}`);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { resolveSafe } from '../sandbox.js';
|
||||
import { ROOT_DIR } from '../config.js';
|
||||
import { walkFiles } from '../dir-listing.js';
|
||||
import { emitToolCall, emitToolResult, clip, formatBytes, resultSize, plural } from './notify.js';
|
||||
|
||||
// Files larger than this are skipped during a search to keep things responsive.
|
||||
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
||||
@@ -272,3 +273,27 @@ export const ripgrepTool = defineTool({
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the pattern plus scope (path/glob) and match modifiers.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
const { pattern, path: searchPath, glob, ignore_case = false, literal = false } = args;
|
||||
const parts = [JSON.stringify(pattern)];
|
||||
if (searchPath && searchPath !== '.') parts.push(`path=${JSON.stringify(searchPath)}`);
|
||||
if (glob) parts.push(`glob=${JSON.stringify(glob)}`);
|
||||
if (ignore_case) parts.push('ignore_case');
|
||||
if (literal) parts.push('literal');
|
||||
emitToolCall(ripgrepTool.name, clip(parts.join(' ')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Result notification: match count and the payload size.
|
||||
*/
|
||||
export function notifyResult(result = {}) {
|
||||
const returned = result.returned ?? (Array.isArray(result.matches) ? result.matches.length : 0);
|
||||
const count = result.truncated
|
||||
? `${returned} matches (capped)`
|
||||
: plural(returned, 'match', 'matches');
|
||||
emitToolResult(ripgrepTool.name, `${count} ${formatBytes(resultSize(result))}`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from 'fs';
|
||||
import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { resolveSafe } from '../sandbox.js';
|
||||
import { pushUndo } from '../undo.js';
|
||||
import { emitToolCall, clip } from './notify.js';
|
||||
|
||||
export const rmTool = defineTool({
|
||||
name: 'rm',
|
||||
@@ -33,3 +34,10 @@ export const rmTool = defineTool({
|
||||
return { message: `File deleted: "${filename}"`, deleted: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the file to delete.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
emitToolCall(rmTool.name, clip(JSON.stringify(args.filename ?? '')));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from 'fs';
|
||||
import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { resolveSafe } from '../sandbox.js';
|
||||
import { pushUndo } from '../undo.js';
|
||||
import { emitToolCall, clip } from './notify.js';
|
||||
|
||||
export const rmdirTool = defineTool({
|
||||
name: 'rmdir',
|
||||
@@ -32,3 +33,10 @@ export const rmdirTool = defineTool({
|
||||
return { message: `Directory removed: "${dirname}"`, removed: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the directory to remove.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
emitToolCall(rmdirTool.name, clip(JSON.stringify(args.dirname ?? '')));
|
||||
}
|
||||
|
||||
137
src/tools/websearch.js
Normal file
137
src/tools/websearch.js
Normal file
@@ -0,0 +1,137 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* websearch — query the web via Tavily and return a synthesized answer plus
|
||||
* ranked results.
|
||||
*
|
||||
* Design notes:
|
||||
* - Requires a Tavily API key (TAVILY_API_KEY in .env). The client is created
|
||||
* lazily and cached, so a missing key surfaces as a clean ToolError only
|
||||
* when the tool is actually used.
|
||||
* - This tool reaches the network, so results are untrusted third-party text;
|
||||
* the model should treat them accordingly.
|
||||
*/
|
||||
|
||||
import { tavily } from '@tavily/core';
|
||||
import { defineTool, ToolError } from 'llmorbiter';
|
||||
import { TAVILY_API_KEY } from '../config.js';
|
||||
import { emitToolCall, emitToolResult, clip, formatBytes, resultSize, plural } from './notify.js';
|
||||
|
||||
const DEFAULT_MAX_RESULTS = 3; // number of results returned to the model
|
||||
const MAX_MAX_RESULTS = 20; // hard upper bound to keep responses small
|
||||
const SEARCH_DEPTHS = ['basic', 'advanced'];
|
||||
const ANSWER_LEVELS = ['basic', 'advanced'];
|
||||
|
||||
let client = null;
|
||||
|
||||
/** Lazily construct (and cache) the Tavily client, failing cleanly if unset. */
|
||||
function getClient() {
|
||||
if (!TAVILY_API_KEY) {
|
||||
throw new ToolError(
|
||||
'websearch requires a Tavily API key. Add TAVILY_API_KEY=... to your .env file.'
|
||||
);
|
||||
}
|
||||
if (!client) {
|
||||
client = tavily({ apiKey: TAVILY_API_KEY });
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
export const websearchTool = defineTool({
|
||||
name: 'websearch',
|
||||
description:
|
||||
'Search the web via Tavily and return a short synthesized answer plus ranked results ' +
|
||||
'(title, url, snippet). Use it for current information, external docs, or facts that are ' +
|
||||
'not in the workspace. Results are untrusted third-party text.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['query'],
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The search query.',
|
||||
},
|
||||
max_results: {
|
||||
type: 'number',
|
||||
description: `Number of results to return (1-${MAX_MAX_RESULTS}). Default: ${DEFAULT_MAX_RESULTS}.`,
|
||||
},
|
||||
search_depth: {
|
||||
type: 'string',
|
||||
enum: SEARCH_DEPTHS,
|
||||
description: 'Search depth: "basic" (faster) or "advanced" (deeper). Default: "basic".',
|
||||
},
|
||||
include_answer: {
|
||||
type: 'string',
|
||||
enum: ANSWER_LEVELS,
|
||||
description: 'Include a Tavily-synthesized answer at this level of detail. Default: "basic".',
|
||||
},
|
||||
chunks_per_source: {
|
||||
type: 'number',
|
||||
description: 'Chunks of content extracted per source (advanced search only). Default: 1.',
|
||||
},
|
||||
},
|
||||
},
|
||||
invoke: async ({
|
||||
query,
|
||||
max_results = DEFAULT_MAX_RESULTS,
|
||||
search_depth = 'basic',
|
||||
include_answer = 'basic',
|
||||
chunks_per_source = 1,
|
||||
}) => {
|
||||
if (!query || typeof query !== 'string' || !query.trim()) {
|
||||
throw new ToolError('"query" must be a non-empty string.');
|
||||
}
|
||||
if (!Number.isInteger(max_results) || max_results < 1 || max_results > MAX_MAX_RESULTS) {
|
||||
throw new ToolError(`"max_results" must be an integer between 1 and ${MAX_MAX_RESULTS} (received ${max_results}).`);
|
||||
}
|
||||
if (!SEARCH_DEPTHS.includes(search_depth)) {
|
||||
throw new ToolError(`"search_depth" must be one of ${SEARCH_DEPTHS.join(', ')} (received "${search_depth}").`);
|
||||
}
|
||||
if (!ANSWER_LEVELS.includes(include_answer)) {
|
||||
throw new ToolError(`"include_answer" must be one of ${ANSWER_LEVELS.join(', ')} (received "${include_answer}").`);
|
||||
}
|
||||
if (!Number.isInteger(chunks_per_source) || chunks_per_source < 1) {
|
||||
throw new ToolError(`"chunks_per_source" must be a positive integer (received ${chunks_per_source}).`);
|
||||
}
|
||||
|
||||
const response = await getClient().search(query, {
|
||||
includeAnswer: include_answer,
|
||||
searchDepth: search_depth,
|
||||
maxResults: max_results,
|
||||
chunksPerSource: chunks_per_source,
|
||||
});
|
||||
|
||||
const results = (response.results || []).map((r) => ({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
content: r.content,
|
||||
score: r.score,
|
||||
}));
|
||||
|
||||
const result = { query, results };
|
||||
if (response.answer) result.answer = response.answer;
|
||||
if (response.responseTime !== undefined) result.responseTime = response.responseTime;
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Call notification: the query plus any non-default search options.
|
||||
*/
|
||||
export function notifyCall(args = {}) {
|
||||
const { query, search_depth, max_results, include_answer } = args;
|
||||
const parts = [JSON.stringify(query)];
|
||||
if (search_depth && search_depth !== 'basic') parts.push(`depth=${search_depth}`);
|
||||
if (max_results && max_results !== DEFAULT_MAX_RESULTS) parts.push(`max_results=${max_results}`);
|
||||
if (include_answer && include_answer !== 'basic') parts.push(`answer=${include_answer}`);
|
||||
emitToolCall(websearchTool.name, clip(parts.join(' ')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Result notification: number of results and the payload size.
|
||||
*/
|
||||
export function notifyResult(result = {}) {
|
||||
const n = Array.isArray(result.results) ? result.results.length : 0;
|
||||
emitToolResult(websearchTool.name, `${plural(n, 'result')} ${formatBytes(resultSize(result))}`);
|
||||
}
|
||||
Reference in New Issue
Block a user