Genesis
This commit is contained in:
14
.env.example
Normal file
14
.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fido — Coding Assistant config
|
||||
# Copy this to .env and fill in your values
|
||||
|
||||
# Required: Your OpenRouter API key
|
||||
OPENROUTER_API_KEY=sk-or-...
|
||||
|
||||
# Optional: Model to use (default: xiaomi/mimo-v2.6-flash)
|
||||
MODEL=xiaomi/mimo-v2.6-flash
|
||||
|
||||
# Optional: File to persist the undo log (relative to workspace root)
|
||||
UNDO_FILE=.fido-undo.json
|
||||
|
||||
# Optional: Workspace root directory (defaults to cwd when running fido)
|
||||
# ROOT_DIR=.
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
.env
|
||||
558
package-lock.json
generated
Normal file
558
package-lock.json
generated
Normal file
@@ -0,0 +1,558 @@
|
||||
{
|
||||
"name": "fido",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "fido",
|
||||
"version": "1.0.0",
|
||||
"license": "0BSD",
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^7.5.1",
|
||||
"chalk": "^5.4.1",
|
||||
"dotenv": "^18.0.2",
|
||||
"llmorbiter": "file:../llmorbiter"
|
||||
}
|
||||
},
|
||||
"../llmorbiter": {
|
||||
"version": "0.1.0",
|
||||
"license": "0BSD",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/ansi": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
|
||||
"integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/checkbox": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz",
|
||||
"integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/ansi": "^1.0.2",
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/figures": "^1.0.15",
|
||||
"@inquirer/type": "^3.0.10",
|
||||
"yoctocolors-cjs": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/confirm": {
|
||||
"version": "5.1.21",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
|
||||
"integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/type": "^3.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core": {
|
||||
"version": "10.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
|
||||
"integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/ansi": "^1.0.2",
|
||||
"@inquirer/figures": "^1.0.15",
|
||||
"@inquirer/type": "^3.0.10",
|
||||
"cli-width": "^4.1.0",
|
||||
"mute-stream": "^2.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"wrap-ansi": "^6.2.0",
|
||||
"yoctocolors-cjs": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/editor": {
|
||||
"version": "4.2.23",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz",
|
||||
"integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/external-editor": "^1.0.3",
|
||||
"@inquirer/type": "^3.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/expand": {
|
||||
"version": "4.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz",
|
||||
"integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/type": "^3.0.10",
|
||||
"yoctocolors-cjs": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/external-editor": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
|
||||
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chardet": "^2.1.1",
|
||||
"iconv-lite": "^0.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/figures": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
|
||||
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/input": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz",
|
||||
"integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/type": "^3.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/number": {
|
||||
"version": "3.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz",
|
||||
"integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/type": "^3.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/password": {
|
||||
"version": "4.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz",
|
||||
"integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/ansi": "^1.0.2",
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/type": "^3.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/prompts": {
|
||||
"version": "7.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz",
|
||||
"integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^4.3.2",
|
||||
"@inquirer/confirm": "^5.1.21",
|
||||
"@inquirer/editor": "^4.2.23",
|
||||
"@inquirer/expand": "^4.0.23",
|
||||
"@inquirer/input": "^4.3.1",
|
||||
"@inquirer/number": "^3.0.23",
|
||||
"@inquirer/password": "^4.0.23",
|
||||
"@inquirer/rawlist": "^4.1.11",
|
||||
"@inquirer/search": "^3.2.2",
|
||||
"@inquirer/select": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/rawlist": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz",
|
||||
"integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/type": "^3.0.10",
|
||||
"yoctocolors-cjs": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/search": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz",
|
||||
"integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/figures": "^1.0.15",
|
||||
"@inquirer/type": "^3.0.10",
|
||||
"yoctocolors-cjs": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/select": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz",
|
||||
"integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/ansi": "^1.0.2",
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/figures": "^1.0.15",
|
||||
"@inquirer/type": "^3.0.10",
|
||||
"yoctocolors-cjs": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/type": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
|
||||
"integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"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-width": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
|
||||
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "18.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.2.tgz",
|
||||
"integrity": "sha512-wl0U3CxC0iobaryqEKpFJxzyp4AxQFYAKj/WWszXZr87vVHLi2yJv2syax5Vxt+u02xqNgE277rsc3lOKg9sPg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"bin": {
|
||||
"dotenv": "dist/index.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"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/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/llmorbiter": {
|
||||
"resolved": "../llmorbiter",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors-cjs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
|
||||
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "fido",
|
||||
"version": "1.0.0",
|
||||
"description": "\"Flight Dynamics Officer\" - Coding Assistant",
|
||||
"license": "0BSD",
|
||||
"author": "",
|
||||
"type": "module",
|
||||
"main": "src/fido.js",
|
||||
"scripts": {
|
||||
"start": "node src/fido.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^7.5.1",
|
||||
"chalk": "^5.4.1",
|
||||
"dotenv": "^18.0.2",
|
||||
"llmorbiter": "file:../llmorbiter"
|
||||
}
|
||||
}
|
||||
655
src/fido.js
Normal file
655
src/fido.js
Normal file
@@ -0,0 +1,655 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Fido — Minimal Coding Assistant
|
||||
*
|
||||
* A sandboxed, interactive coding assistant using LLMOrbiter.
|
||||
*
|
||||
* Config via .env (place in c:\src\fido\ or the working directory):
|
||||
* OPENROUTER_API_KEY=sk-or-...
|
||||
* MODEL=xiaomi/mimo-v2.6-flash
|
||||
* UNDO_FILE=.fido-undo.json
|
||||
* ROOT_DIR=. (optional, defaults to cwd)
|
||||
*
|
||||
* Usage:
|
||||
* node src/fido.js
|
||||
* node src/fido.js --dir /path/to/project
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createInterface } from 'readline';
|
||||
import dotenv from 'dotenv';
|
||||
import chalk from 'chalk';
|
||||
import { input } from '@inquirer/prompts';
|
||||
|
||||
import { LLMOrbiter, OpenRouterProvider, defineTool, ToolError } from 'llmorbiter';
|
||||
|
||||
// ─── Bootstrap ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Load .env from the fido project root (one level up from src/)
|
||||
dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
|
||||
// Also try cwd-relative .env
|
||||
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||||
|
||||
const API_KEY = process.env.OPENROUTER_API_KEY;
|
||||
const MODEL = process.env.MODEL || 'xiaomi/mimo-v2.6-flash';
|
||||
const UNDO_FILE = process.env.UNDO_FILE || '.fido-undo.json';
|
||||
|
||||
// Root dir: --dir flag wins, then env ROOT_DIR, then cwd
|
||||
const args = process.argv.slice(2);
|
||||
const dirFlagIdx = args.indexOf('--dir');
|
||||
const ROOT_DIR = path.resolve(
|
||||
(dirFlagIdx !== -1 && args[dirFlagIdx + 1]) || process.env.ROOT_DIR || process.cwd()
|
||||
);
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error(chalk.red.bold('\n✖ Missing OPENROUTER_API_KEY — add it to .env\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ─── Sandbox ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve a user-supplied path within the sandbox root.
|
||||
* Throws ToolError if the resolved path escapes the root.
|
||||
*/
|
||||
function resolveSafe(userPath) {
|
||||
if (!userPath || typeof userPath !== 'string') {
|
||||
throw new ToolError('Path must be a non-empty string.');
|
||||
}
|
||||
// Strip leading slashes/backslashes so the user can't anchor to filesystem root
|
||||
const stripped = userPath.replace(/^[/\\]+/, '');
|
||||
const resolved = path.resolve(ROOT_DIR, stripped);
|
||||
// Must stay inside root
|
||||
const rootNorm = ROOT_DIR.endsWith(path.sep) ? ROOT_DIR : ROOT_DIR + path.sep;
|
||||
if (resolved !== ROOT_DIR && !resolved.startsWith(rootNorm)) {
|
||||
throw new ToolError(
|
||||
`Access denied: "${userPath}" resolves outside the workspace root "${ROOT_DIR}". ` +
|
||||
`You may only access files within the workspace.`
|
||||
);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// ─── Directory listing helper ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Recursively list files under a directory, relative to ROOT_DIR.
|
||||
* Returns { files: string[], total: number, truncated: number }
|
||||
*/
|
||||
function listFilesRecursive(dir, seek = 0, limit = 50) {
|
||||
const allFiles = [];
|
||||
const walk = (current) => {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const e of entries) {
|
||||
const full = path.join(current, e.name);
|
||||
const rel = path.relative(ROOT_DIR, full).replace(/\\/g, '/');
|
||||
if (e.isDirectory()) {
|
||||
allFiles.push(rel + '/');
|
||||
walk(full);
|
||||
} else {
|
||||
allFiles.push(rel);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(dir);
|
||||
const total = allFiles.length;
|
||||
const sliced = allFiles.slice(seek, seek + limit);
|
||||
const truncated = total - (seek + sliced.length);
|
||||
return { files: sliced, total, truncated: Math.max(0, truncated) };
|
||||
}
|
||||
|
||||
// ─── System prompt builder ─────────────────────────────────────────────────────
|
||||
|
||||
function buildSystemPrompt() {
|
||||
const { files, total, truncated } = listFilesRecursive(ROOT_DIR, 0, 50);
|
||||
let dirSection = files.map(f => ` ${f}`).join('\n');
|
||||
if (truncated > 0) {
|
||||
dirSection += `\n ... (${truncated} more files not shown — use list_files to explore)`;
|
||||
}
|
||||
|
||||
return `You are Fido, a minimal but capable coding assistant.
|
||||
You can read files, list directories, and apply code patches within the workspace.
|
||||
|
||||
## Workspace
|
||||
Root directory: ${ROOT_DIR}
|
||||
Total files: ${total}
|
||||
|
||||
## Current directory listing (first 50 of ${total}):
|
||||
${dirSection}
|
||||
|
||||
## Rules
|
||||
- You may ONLY access files within the workspace root. Never use ".." or absolute paths.
|
||||
- When editing files, prefer targeted SEARCH/REPLACE patches over full rewrites.
|
||||
- After applying a patch, briefly confirm what you did.
|
||||
- Keep responses concise and focused.
|
||||
- Use the tools; do not emit raw file contents unless the user asks.
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Undo log ─────────────────────────────────────────────────────────────────
|
||||
|
||||
let undoLog = [];
|
||||
|
||||
function loadUndoLog() {
|
||||
const undoPath = path.resolve(ROOT_DIR, UNDO_FILE);
|
||||
if (fs.existsSync(undoPath)) {
|
||||
try {
|
||||
undoLog = JSON.parse(fs.readFileSync(undoPath, 'utf8'));
|
||||
} catch {
|
||||
undoLog = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveUndoLog() {
|
||||
const undoPath = path.resolve(ROOT_DIR, UNDO_FILE);
|
||||
fs.writeFileSync(undoPath, JSON.stringify(undoLog, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
function pushUndo(entry) {
|
||||
undoLog.unshift(entry); // newest first
|
||||
saveUndoLog();
|
||||
}
|
||||
|
||||
// ─── Patch parser ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse a patch string into an array of operations.
|
||||
*
|
||||
* Supported operations:
|
||||
* <<<<< CREATE path
|
||||
* ...content...
|
||||
* >>>>>
|
||||
*
|
||||
* <<<<< CREATE_DIR path/
|
||||
* >>>>>
|
||||
*
|
||||
* <<<<< SEARCH path
|
||||
* <<<<<<< SEARCH
|
||||
* old
|
||||
* =======
|
||||
* new
|
||||
* >>>>>>> REPLACE
|
||||
* >>>>>
|
||||
*
|
||||
* <<<<< DELETE path
|
||||
* >>>>>
|
||||
*
|
||||
* <<<<< DELETE_DIR path/
|
||||
* >>>>>
|
||||
*/
|
||||
function parsePatch(patchStr) {
|
||||
const ops = [];
|
||||
// Split on operation boundaries: lines starting with "<<<<<"
|
||||
const opPattern = /^<<<<< (\w+) (.+)$/m;
|
||||
// We'll scan line by line
|
||||
const lines = patchStr.split('\n');
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i].trimEnd();
|
||||
const m = line.match(/^<<<<< (\w+) (.+)$/);
|
||||
if (!m) { i++; continue; }
|
||||
|
||||
const opType = m[1].toUpperCase();
|
||||
const opPath = m[2].trim();
|
||||
i++;
|
||||
|
||||
// Collect body until ">>>>>" (5 right-angle brackets on its own line)
|
||||
const bodyLines = [];
|
||||
while (i < lines.length && lines[i].trimEnd() !== '>>>>>') {
|
||||
bodyLines.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
i++; // skip the ">>>>>"
|
||||
|
||||
const body = bodyLines.join('\n');
|
||||
|
||||
if (opType === 'CREATE') {
|
||||
ops.push({ type: 'CREATE', path: opPath, content: body });
|
||||
} else if (opType === 'CREATE_DIR') {
|
||||
ops.push({ type: 'CREATE_DIR', path: opPath });
|
||||
} else if (opType === 'DELETE') {
|
||||
ops.push({ type: 'DELETE', path: opPath });
|
||||
} else if (opType === 'DELETE_DIR') {
|
||||
ops.push({ type: 'DELETE_DIR', path: opPath });
|
||||
} else if (opType === 'SEARCH') {
|
||||
// Parse inner <<<<<<< SEARCH / ======= / >>>>>>> REPLACE block
|
||||
const searchMatch = body.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
|
||||
if (!searchMatch) {
|
||||
ops.push({ type: 'SEARCH', path: opPath, error: 'Malformed SEARCH block — expected <<<<<<< SEARCH / ======= / >>>>>>> REPLACE markers' });
|
||||
} else {
|
||||
ops.push({ type: 'SEARCH', path: opPath, search: searchMatch[1], replace: searchMatch[2] });
|
||||
}
|
||||
} else {
|
||||
ops.push({ type: 'UNKNOWN', raw: line });
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
// ─── Patch executor ────────────────────────────────────────────────────────────
|
||||
|
||||
function applyPatch(patchStr) {
|
||||
const ops = parsePatch(patchStr);
|
||||
if (ops.length === 0) {
|
||||
return { success: false, error: 'No valid operations found in patch string. Check the patch format.' };
|
||||
}
|
||||
|
||||
// Validate all paths first (fast-fail before any writes)
|
||||
for (const op of ops) {
|
||||
if (op.error) {
|
||||
return { success: false, error: op.error, failedOp: op };
|
||||
}
|
||||
if (op.type === 'UNKNOWN') {
|
||||
return { success: false, error: `Unrecognised operation: "${op.raw}"`, failedOp: op };
|
||||
}
|
||||
try {
|
||||
resolveSafe(op.path);
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message, failedOp: op };
|
||||
}
|
||||
}
|
||||
|
||||
const applied = [];
|
||||
for (const op of ops) {
|
||||
const absPath = resolveSafe(op.path);
|
||||
try {
|
||||
if (op.type === 'CREATE') {
|
||||
// Build undo: delete the file (or restore if it existed)
|
||||
let undoEntry;
|
||||
if (fs.existsSync(absPath)) {
|
||||
const existing = fs.readFileSync(absPath, 'utf8');
|
||||
undoEntry = { op: 'RESTORE', path: op.path, content: existing };
|
||||
} else {
|
||||
undoEntry = { op: 'DELETE', path: op.path };
|
||||
}
|
||||
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
||||
fs.writeFileSync(absPath, op.content, 'utf8');
|
||||
pushUndo(undoEntry);
|
||||
applied.push(`CREATE ${op.path}`);
|
||||
|
||||
} else if (op.type === 'CREATE_DIR') {
|
||||
if (!fs.existsSync(absPath)) {
|
||||
fs.mkdirSync(absPath, { recursive: true });
|
||||
pushUndo({ op: 'DELETE_DIR', path: op.path });
|
||||
}
|
||||
applied.push(`CREATE_DIR ${op.path}`);
|
||||
|
||||
} else if (op.type === 'DELETE') {
|
||||
if (!fs.existsSync(absPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `DELETE failed: file not found: "${op.path}"`,
|
||||
failedOp: op,
|
||||
appliedSoFar: applied,
|
||||
};
|
||||
}
|
||||
const content = fs.readFileSync(absPath, 'utf8');
|
||||
fs.unlinkSync(absPath);
|
||||
pushUndo({ op: 'CREATE', path: op.path, content });
|
||||
applied.push(`DELETE ${op.path}`);
|
||||
|
||||
} else if (op.type === 'DELETE_DIR') {
|
||||
if (!fs.existsSync(absPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `DELETE_DIR failed: directory not found: "${op.path}"`,
|
||||
failedOp: op,
|
||||
appliedSoFar: applied,
|
||||
};
|
||||
}
|
||||
fs.rmSync(absPath, { recursive: true, force: true });
|
||||
pushUndo({ op: 'DELETE_DIR_UNDO_NOTE', path: op.path, note: 'Directory was deleted; contents cannot be automatically restored.' });
|
||||
applied.push(`DELETE_DIR ${op.path}`);
|
||||
|
||||
} else if (op.type === 'SEARCH') {
|
||||
if (!fs.existsSync(absPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SEARCH/REPLACE failed: file not found: "${op.path}"`,
|
||||
failedOp: op,
|
||||
appliedSoFar: applied,
|
||||
};
|
||||
}
|
||||
const original = fs.readFileSync(absPath, 'utf8');
|
||||
if (!original.includes(op.search)) {
|
||||
// Provide context to help the LLM fix the search string
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
`SEARCH/REPLACE failed in "${op.path}": search text not found.\n` +
|
||||
`Search string was:\n${op.search}\n\n` +
|
||||
`Tip: use read_file to check the exact current content of "${op.path}" before retrying.`,
|
||||
failedOp: op,
|
||||
appliedSoFar: applied,
|
||||
};
|
||||
}
|
||||
const updated = original.replace(op.search, op.replace);
|
||||
fs.writeFileSync(absPath, updated, 'utf8');
|
||||
pushUndo({ op: 'RESTORE', path: op.path, content: original });
|
||||
applied.push(`SEARCH/REPLACE in ${op.path}`);
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Operation "${op.type} ${op.path}" failed: ${err.message}`,
|
||||
failedOp: op,
|
||||
appliedSoFar: applied,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, applied, undoLogEntries: applied.length };
|
||||
}
|
||||
|
||||
// ─── Tool definitions ──────────────────────────────────────────────────────────
|
||||
|
||||
const listFilesTool = defineTool({
|
||||
name: 'list_files',
|
||||
description:
|
||||
'List files and directories within the workspace. Use seek/limit for pagination. ' +
|
||||
'Paths are relative to the workspace root. Returns truncation info when results are cut.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['dirname'],
|
||||
properties: {
|
||||
dirname: {
|
||||
type: 'string',
|
||||
description: 'Directory path relative to workspace root. Use "." for root.',
|
||||
},
|
||||
seek: {
|
||||
type: 'number',
|
||||
description: 'Number of entries to skip (for pagination). Default: 0.',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of entries to return. Default: 50.',
|
||||
},
|
||||
},
|
||||
},
|
||||
invoke: async ({ dirname, seek = 0, limit = 50 }) => {
|
||||
const absDir = resolveSafe(dirname);
|
||||
if (!fs.existsSync(absDir)) {
|
||||
throw new ToolError(`Directory not found: "${dirname}"`);
|
||||
}
|
||||
if (!fs.statSync(absDir).isDirectory()) {
|
||||
throw new ToolError(`"${dirname}" is not a directory.`);
|
||||
}
|
||||
const { files, total, truncated } = listFilesRecursive(absDir, seek, limit);
|
||||
const result = { dirname, files, total, returned: files.length, seek };
|
||||
if (truncated > 0) {
|
||||
result.truncated = truncated;
|
||||
result.truncationNote = `${truncated} entries not shown. Use seek=${seek + files.length} to continue.`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
const readFileTool = defineTool({
|
||||
name: 'read_file',
|
||||
description:
|
||||
'Read a file from the workspace. Use seek/limit to read in chunks. ' +
|
||||
'Returns truncation info when the file is larger than the limit.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['filename'],
|
||||
properties: {
|
||||
filename: {
|
||||
type: 'string',
|
||||
description: 'File path relative to workspace root.',
|
||||
},
|
||||
seek: {
|
||||
type: 'number',
|
||||
description: 'Line number to start reading from (0-indexed). Default: 0.',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of lines to return. Default: 200.',
|
||||
},
|
||||
},
|
||||
},
|
||||
invoke: async ({ filename, seek = 0, limit = 200 }) => {
|
||||
const absPath = resolveSafe(filename);
|
||||
if (!fs.existsSync(absPath)) {
|
||||
throw new ToolError(`File not found: "${filename}"`);
|
||||
}
|
||||
if (!fs.statSync(absPath).isFile()) {
|
||||
throw new ToolError(`"${filename}" is not a file.`);
|
||||
}
|
||||
const allLines = fs.readFileSync(absPath, 'utf8').split('\n');
|
||||
const total = allLines.length;
|
||||
const slice = allLines.slice(seek, seek + limit);
|
||||
const truncated = total - (seek + slice.length);
|
||||
|
||||
const result = {
|
||||
filename,
|
||||
totalLines: total,
|
||||
seek,
|
||||
returnedLines: slice.length,
|
||||
content: slice.join('\n'),
|
||||
};
|
||||
if (truncated > 0) {
|
||||
result.truncated = truncated;
|
||||
result.truncationNote = `${truncated} lines not shown. Use seek=${seek + slice.length} to continue.`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
const patchTool = defineTool({
|
||||
name: 'patch',
|
||||
description: `Apply file operations to the workspace. Supports CREATE, CREATE_DIR, SEARCH/REPLACE, DELETE, DELETE_DIR.
|
||||
|
||||
Patch format (one or more operations):
|
||||
|
||||
<<<<< CREATE path/to/new_file.py
|
||||
# New file contents here
|
||||
def hello():
|
||||
return "world"
|
||||
>>>>>
|
||||
|
||||
<<<<< CREATE_DIR path/to/new_folder/
|
||||
>>>>>
|
||||
|
||||
<<<<< SEARCH path/to/existing_file.py
|
||||
<<<<<<< SEARCH
|
||||
old_code_line_1()
|
||||
=======
|
||||
new_code_line_1()
|
||||
>>>>>>> REPLACE
|
||||
>>>>>
|
||||
|
||||
<<<<< DELETE path/to/old_file.txt
|
||||
>>>>>
|
||||
|
||||
<<<<< DELETE_DIR path/to/old_directory/
|
||||
>>>>>
|
||||
|
||||
Rules:
|
||||
- Paths must be relative to workspace root. No ".." or absolute paths.
|
||||
- SEARCH text must match the file exactly (including whitespace/indentation).
|
||||
- Multiple operations in one patch are applied in order; if one fails the rest are skipped.
|
||||
- On failure, a structured error is returned explaining what went wrong so you can fix and retry.`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['patch'],
|
||||
properties: {
|
||||
patch: {
|
||||
type: 'string',
|
||||
description: 'The patch string containing one or more operations.',
|
||||
},
|
||||
},
|
||||
},
|
||||
invoke: async ({ patch }) => {
|
||||
const result = applyPatch(patch);
|
||||
if (!result.success) {
|
||||
// Return as a ToolError so LLMOrbiter feeds it back to the LLM
|
||||
throw new ToolError(
|
||||
`Patch failed: ${result.error}` +
|
||||
(result.appliedSoFar?.length ? `\nSuccessfully applied before failure: ${result.appliedSoFar.join(', ')}` : '')
|
||||
);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Console UI ────────────────────────────────────────────────────────────────
|
||||
|
||||
function printBanner() {
|
||||
console.log('');
|
||||
console.log(chalk.cyan.bold('╔══════════════════════════════════════════════╗'));
|
||||
console.log(chalk.cyan.bold('║') + chalk.white.bold(' 🐕 FIDO — Coding Assistant ') + chalk.cyan.bold('║'));
|
||||
console.log(chalk.cyan.bold('╚══════════════════════════════════════════════╝'));
|
||||
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('');
|
||||
}
|
||||
|
||||
function printDivider() {
|
||||
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
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) {
|
||||
const str = typeof result === 'string' ? result : JSON.stringify(result);
|
||||
const display = str.length > 200 ? str.slice(0, 197) + '...' : str;
|
||||
console.log(chalk.dim(` ↳ ${display}`));
|
||||
}
|
||||
|
||||
function printToolError(name, error) {
|
||||
console.log(chalk.red(` ✖ Tool error [${name}]: ${error}`));
|
||||
}
|
||||
|
||||
function printUsage(u) {
|
||||
const parts = [`in=${u.input}`, `out=${u.output}`];
|
||||
if (u.cached) parts.push(`cached=${u.cached}`);
|
||||
if (u.cost) parts.push(`cost=$${u.cost.toFixed(5)}`);
|
||||
console.log(chalk.dim(` 📊 ${parts.join(' ')}`));
|
||||
}
|
||||
|
||||
// ─── Main loop ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
loadUndoLog();
|
||||
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),
|
||||
onToolError: (e) => printToolError(e.name, e.error),
|
||||
onUsage: (u) => printUsage(u),
|
||||
});
|
||||
|
||||
// Conversation state: persisted across turns
|
||||
const systemPrompt = buildSystemPrompt();
|
||||
let messages = [{ role: 'system', content: systemPrompt }];
|
||||
|
||||
console.log(chalk.dim(' Type your request below. Press Ctrl+C to exit.\n'));
|
||||
|
||||
while (true) {
|
||||
let userInput;
|
||||
try {
|
||||
userInput = await input({
|
||||
message: chalk.bold.cyan('You'),
|
||||
prefix: chalk.cyan('›'),
|
||||
theme: {
|
||||
prefix: chalk.cyan('›'),
|
||||
style: {
|
||||
answer: chalk.white,
|
||||
message: chalk.bold.cyan,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Ctrl+C / EOF
|
||||
console.log(chalk.dim('\n Goodbye! 🐕\n'));
|
||||
break;
|
||||
}
|
||||
|
||||
if (!userInput.trim()) continue;
|
||||
|
||||
// Handle special commands
|
||||
if (userInput.trim() === '/quit' || userInput.trim() === '/exit') {
|
||||
console.log(chalk.dim('\n Goodbye! 🐕\n'));
|
||||
break;
|
||||
}
|
||||
if (userInput.trim() === '/undo') {
|
||||
if (undoLog.length === 0) {
|
||||
console.log(chalk.yellow('\n Nothing to undo.\n'));
|
||||
} else {
|
||||
console.log(chalk.yellow('\n Undo log (newest first):'));
|
||||
for (const e of undoLog.slice(0, 10)) {
|
||||
console.log(chalk.dim(` ${e.op} ${e.path}`));
|
||||
}
|
||||
if (undoLog.length > 10) {
|
||||
console.log(chalk.dim(` ... and ${undoLog.length - 10} more in ${UNDO_FILE}`));
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
try {
|
||||
const result = await orbiter.run({
|
||||
prompt: userInput,
|
||||
messages,
|
||||
model: MODEL,
|
||||
tools: [listFilesTool, readFileTool, patchTool],
|
||||
maxIterations: 20,
|
||||
});
|
||||
|
||||
// Update conversation history for next turn
|
||||
messages = result.messages;
|
||||
|
||||
printAssistant(result.content);
|
||||
} catch (err) {
|
||||
console.log(chalk.red(`\n ✖ Error: ${err.message}\n`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(chalk.red.bold('\nFATAL: ' + err.message));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user