feat: listen mode (#997)
Co-authored-by: cpacker <packercharles@gmail.com>
This commit is contained in:
8
bun.lock
8
bun.lock
@@ -10,12 +10,14 @@
|
|||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
|
"ws": "^8.19.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.7",
|
"@types/bun": "^1.3.7",
|
||||||
"@types/diff": "^8.0.0",
|
"@types/diff": "^8.0.0",
|
||||||
"@types/picomatch": "^4.0.2",
|
"@types/picomatch": "^4.0.2",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.9",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"ink": "^5.0.0",
|
"ink": "^5.0.0",
|
||||||
@@ -103,6 +105,8 @@
|
|||||||
|
|
||||||
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
|
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
|
||||||
|
|
||||||
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"@vscode/ripgrep": ["@vscode/ripgrep@1.17.0", "", { "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", "yauzl": "^2.9.2" } }, "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g=="],
|
"@vscode/ripgrep": ["@vscode/ripgrep@1.17.0", "", { "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", "yauzl": "^2.9.2" } }, "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g=="],
|
||||||
|
|
||||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
@@ -291,7 +295,7 @@
|
|||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||||
|
|
||||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||||
|
|
||||||
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||||
|
|
||||||
@@ -305,6 +309,8 @@
|
|||||||
|
|
||||||
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||||
|
|
||||||
|
"ink/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
"ink-text-input/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
|
"ink-text-input/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
|
||||||
|
|
||||||
"listr2/cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
|
"listr2/cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
|
||||||
|
|||||||
88
package-lock.json
generated
88
package-lock.json
generated
@@ -10,20 +10,22 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@letta-ai/letta-client": "^1.7.6",
|
"@letta-ai/letta-client": "^1.7.8",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5",
|
||||||
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"letta": "letta.js"
|
"letta": "letta.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "*",
|
"@types/bun": "^1.3.7",
|
||||||
"@types/diff": "^8.0.0",
|
"@types/diff": "^8.0.0",
|
||||||
"@types/picomatch": "^4.0.2",
|
"@types/picomatch": "^4.0.2",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.9",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"ink": "^5.0.0",
|
"ink": "^5.0.0",
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz",
|
||||||
"integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==",
|
"integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^6.2.1",
|
"ansi-styles": "^6.2.1",
|
||||||
@@ -549,19 +552,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@letta-ai/letta-client": {
|
"node_modules/@letta-ai/letta-client": {
|
||||||
"version": "1.7.6",
|
"version": "1.7.8",
|
||||||
"resolved": "https://registry.npmjs.org/@letta-ai/letta-client/-/letta-client-1.7.6.tgz",
|
"resolved": "https://registry.npmjs.org/@letta-ai/letta-client/-/letta-client-1.7.8.tgz",
|
||||||
"integrity": "sha512-C/f03uE3TJdgfHk/8rRBxzWvY0YHCYAlrePHcTd0CRHMo++0TA1OTcgiCF+EFVDVYGzfPSeMpqgAZTNvD9r9GQ==",
|
"integrity": "sha512-l7BGsAZOI8b+H1RO2IMzMv3P0VXj6Of2wXLCg5LZejOk0dHLLBi2me4WspLg+6zWPGIRJwMRsxmfp4kV9Zzh6g==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@types/bun": {
|
"node_modules/@types/bun": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.9.tgz",
|
||||||
"integrity": "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA==",
|
"integrity": "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bun-types": "1.3.6"
|
"bun-types": "1.3.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/diff": {
|
"node_modules/@types/diff": {
|
||||||
@@ -576,10 +579,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.0.10",
|
"version": "25.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
|
||||||
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
|
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
@@ -596,12 +598,22 @@
|
|||||||
"version": "19.2.9",
|
"version": "19.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
||||||
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
},
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@vscode/ripgrep": {
|
"node_modules/@vscode/ripgrep": {
|
||||||
"version": "1.17.0",
|
"version": "1.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.17.0.tgz",
|
||||||
@@ -644,6 +656,7 @@
|
|||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -656,6 +669,7 @@
|
|||||||
"version": "6.2.3",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -668,6 +682,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz",
|
||||||
"integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==",
|
"integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
@@ -700,9 +715,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bun-types": {
|
"node_modules/bun-types": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.9.tgz",
|
||||||
"integrity": "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ==",
|
"integrity": "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -728,6 +743,7 @@
|
|||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
@@ -740,6 +756,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
|
||||||
"integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
|
"integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -752,6 +769,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
|
||||||
"integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
|
"integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"restore-cursor": "^4.0.0"
|
"restore-cursor": "^4.0.0"
|
||||||
@@ -780,6 +798,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
|
||||||
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
|
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"slice-ansi": "^5.0.0",
|
"slice-ansi": "^5.0.0",
|
||||||
@@ -796,6 +815,7 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
|
||||||
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
|
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^6.0.0",
|
"ansi-styles": "^6.0.0",
|
||||||
@@ -812,6 +832,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
|
||||||
"integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
|
"integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"convert-to-spaces": "^2.0.1"
|
"convert-to-spaces": "^2.0.1"
|
||||||
@@ -841,6 +862,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
|
||||||
"integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
|
"integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
@@ -850,7 +872,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@@ -934,6 +956,7 @@
|
|||||||
"version": "10.6.0",
|
"version": "10.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/environment": {
|
"node_modules/environment": {
|
||||||
@@ -952,6 +975,7 @@
|
|||||||
"version": "1.44.0",
|
"version": "1.44.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
|
||||||
"integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==",
|
"integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"docs",
|
"docs",
|
||||||
@@ -962,6 +986,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
|
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -1001,6 +1026,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
||||||
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -1072,6 +1098,7 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
|
||||||
"integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
|
"integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -1084,6 +1111,7 @@
|
|||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz",
|
||||||
"integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==",
|
"integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.1.3",
|
"@alcalzone/ansi-tokenize": "^0.1.3",
|
||||||
@@ -1213,6 +1241,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
|
||||||
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
|
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -1225,6 +1254,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz",
|
||||||
"integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==",
|
"integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"is-in-ci": "cli.js"
|
"is-in-ci": "cli.js"
|
||||||
@@ -1283,6 +1313,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lint-staged": {
|
"node_modules/lint-staged": {
|
||||||
@@ -1448,6 +1479,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
@@ -1483,6 +1515,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -1549,6 +1582,7 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||||
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mimic-fn": "^2.1.0"
|
"mimic-fn": "^2.1.0"
|
||||||
@@ -1582,6 +1616,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz",
|
||||||
"integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==",
|
"integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
@@ -1647,6 +1682,7 @@
|
|||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
@@ -1659,6 +1695,7 @@
|
|||||||
"version": "0.29.2",
|
"version": "0.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz",
|
||||||
"integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==",
|
"integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
@@ -1675,6 +1712,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
|
||||||
"integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
|
"integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"onetime": "^5.1.0",
|
"onetime": "^5.1.0",
|
||||||
@@ -1710,6 +1748,7 @@
|
|||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
@@ -1775,12 +1814,14 @@
|
|||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/slice-ansi": {
|
"node_modules/slice-ansi": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||||
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
|
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^6.2.1",
|
"ansi-styles": "^6.2.1",
|
||||||
@@ -1797,6 +1838,7 @@
|
|||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||||
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-east-asian-width": "^1.3.1"
|
"get-east-asian-width": "^1.3.1"
|
||||||
@@ -1812,6 +1854,7 @@
|
|||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||||
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
|
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"escape-string-regexp": "^2.0.0"
|
"escape-string-regexp": "^2.0.0"
|
||||||
@@ -1834,6 +1877,7 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^10.3.0",
|
"emoji-regex": "^10.3.0",
|
||||||
@@ -1851,6 +1895,7 @@
|
|||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^6.0.1"
|
"ansi-regex": "^6.0.1"
|
||||||
@@ -1930,6 +1975,7 @@
|
|||||||
"version": "4.41.0",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||||
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||||
|
"dev": true,
|
||||||
"license": "(MIT OR CC0-1.0)",
|
"license": "(MIT OR CC0-1.0)",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
@@ -1956,13 +2002,13 @@
|
|||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/widest-line": {
|
"node_modules/widest-line": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
|
||||||
"integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
|
"integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"string-width": "^7.0.0"
|
"string-width": "^7.0.0"
|
||||||
@@ -1978,6 +2024,7 @@
|
|||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||||
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^6.2.1",
|
"ansi-styles": "^6.2.1",
|
||||||
@@ -2058,6 +2105,7 @@
|
|||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
|
||||||
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
|
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5",
|
||||||
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@vscode/ripgrep": "^1.17.0"
|
"@vscode/ripgrep": "^1.17.0"
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"@types/diff": "^8.0.0",
|
"@types/diff": "^8.0.0",
|
||||||
"@types/picomatch": "^4.0.2",
|
"@types/picomatch": "^4.0.2",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.9",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"ink": "^5.0.0",
|
"ink": "^5.0.0",
|
||||||
|
|||||||
@@ -6223,6 +6223,54 @@ export default function App({
|
|||||||
return { submitted: true };
|
return { submitted: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special handling for /listen command - start listener mode
|
||||||
|
if (trimmed === "/listen" || trimmed.startsWith("/listen ")) {
|
||||||
|
// Tokenize with quote support: --name "my laptop"
|
||||||
|
const parts = Array.from(
|
||||||
|
trimmed.matchAll(
|
||||||
|
/"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|(\S+)/g,
|
||||||
|
),
|
||||||
|
(match) => match[1] ?? match[2] ?? match[3],
|
||||||
|
);
|
||||||
|
|
||||||
|
let name: string | undefined;
|
||||||
|
let listenAgentId: string | undefined;
|
||||||
|
|
||||||
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
const nextPart = parts[i + 1];
|
||||||
|
if (part === "--name" && nextPart) {
|
||||||
|
name = nextPart;
|
||||||
|
i++;
|
||||||
|
} else if (part === "--agent" && nextPart) {
|
||||||
|
listenAgentId = nextPart;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to current agent if not specified
|
||||||
|
const targetAgentId = listenAgentId || agentId;
|
||||||
|
|
||||||
|
const cmd = commandRunner.start(msg, "Starting listener...");
|
||||||
|
const { handleListen, setActiveCommandId: setActiveListenCommandId } =
|
||||||
|
await import("./commands/listen");
|
||||||
|
setActiveListenCommandId(cmd.id);
|
||||||
|
try {
|
||||||
|
await handleListen(
|
||||||
|
{
|
||||||
|
buffersRef,
|
||||||
|
refreshDerived,
|
||||||
|
setCommandRunning,
|
||||||
|
},
|
||||||
|
msg,
|
||||||
|
{ name, agentId: targetAgentId },
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setActiveListenCommandId(null);
|
||||||
|
}
|
||||||
|
return { submitted: true };
|
||||||
|
}
|
||||||
|
|
||||||
// Special handling for /help command - opens help dialog
|
// Special handling for /help command - opens help dialog
|
||||||
if (trimmed === "/help") {
|
if (trimmed === "/help") {
|
||||||
startOverlayCommand(
|
startOverlayCommand(
|
||||||
|
|||||||
335
src/cli/commands/listen.ts
Normal file
335
src/cli/commands/listen.ts
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
/**
|
||||||
|
* Listen mode - Register letta-code as a listener to receive messages from Letta Cloud
|
||||||
|
* Usage: letta listen --name "george"
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { hostname } from "node:os";
|
||||||
|
import { getServerUrl } from "../../agent/client";
|
||||||
|
import { settingsManager } from "../../settings-manager";
|
||||||
|
import { getErrorMessage } from "../../utils/error";
|
||||||
|
import type { Buffers, Line } from "../helpers/accumulator";
|
||||||
|
|
||||||
|
// tiny helper for unique ids
|
||||||
|
function uid(prefix: string) {
|
||||||
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper type for command result
|
||||||
|
type CommandLine = Extract<Line, { kind: "command" }>;
|
||||||
|
|
||||||
|
let activeCommandId: string | null = null;
|
||||||
|
|
||||||
|
export function setActiveCommandId(id: string | null): void {
|
||||||
|
activeCommandId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context passed to listen handler
|
||||||
|
export interface ListenCommandContext {
|
||||||
|
buffersRef: { current: Buffers };
|
||||||
|
refreshDerived: () => void;
|
||||||
|
setCommandRunning: (running: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to add a command result to buffers
|
||||||
|
function addCommandResult(
|
||||||
|
buffersRef: { current: Buffers },
|
||||||
|
refreshDerived: () => void,
|
||||||
|
input: string,
|
||||||
|
output: string,
|
||||||
|
success: boolean,
|
||||||
|
phase: "running" | "finished" = "finished",
|
||||||
|
): string {
|
||||||
|
const cmdId = activeCommandId ?? uid("cmd");
|
||||||
|
const existing = buffersRef.current.byId.get(cmdId);
|
||||||
|
const nextInput =
|
||||||
|
existing && existing.kind === "command" ? existing.input : input;
|
||||||
|
const line: CommandLine = {
|
||||||
|
kind: "command",
|
||||||
|
id: cmdId,
|
||||||
|
input: nextInput,
|
||||||
|
output,
|
||||||
|
phase,
|
||||||
|
...(phase === "finished" && { success }),
|
||||||
|
};
|
||||||
|
buffersRef.current.byId.set(cmdId, line);
|
||||||
|
if (!buffersRef.current.order.includes(cmdId)) {
|
||||||
|
buffersRef.current.order.push(cmdId);
|
||||||
|
}
|
||||||
|
refreshDerived();
|
||||||
|
return cmdId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to update an existing command result
|
||||||
|
function updateCommandResult(
|
||||||
|
buffersRef: { current: Buffers },
|
||||||
|
refreshDerived: () => void,
|
||||||
|
cmdId: string,
|
||||||
|
input: string,
|
||||||
|
output: string,
|
||||||
|
success: boolean,
|
||||||
|
phase: "running" | "finished" = "finished",
|
||||||
|
): void {
|
||||||
|
const existing = buffersRef.current.byId.get(cmdId);
|
||||||
|
const nextInput =
|
||||||
|
existing && existing.kind === "command" ? existing.input : input;
|
||||||
|
const line: CommandLine = {
|
||||||
|
kind: "command",
|
||||||
|
id: cmdId,
|
||||||
|
input: nextInput,
|
||||||
|
output,
|
||||||
|
phase,
|
||||||
|
...(phase === "finished" && { success }),
|
||||||
|
};
|
||||||
|
buffersRef.current.byId.set(cmdId, line);
|
||||||
|
refreshDerived();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListenOptions {
|
||||||
|
name?: string;
|
||||||
|
agentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle /listen command
|
||||||
|
* Usage: /listen --name "george" [--agent agent-xyz]
|
||||||
|
*/
|
||||||
|
export async function handleListen(
|
||||||
|
ctx: ListenCommandContext,
|
||||||
|
msg: string,
|
||||||
|
opts: ListenOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
|
// Show usage if needed
|
||||||
|
if (msg.includes("--help") || msg.includes("-h")) {
|
||||||
|
addCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
msg,
|
||||||
|
"Usage: /listen --name <connection-name> [--agent <agent-id>]\n\n" +
|
||||||
|
"Register this letta-code instance to receive messages from Letta Cloud.\n\n" +
|
||||||
|
"Options:\n" +
|
||||||
|
" --name <name> Friendly name for this connection (required)\n" +
|
||||||
|
" --agent <id> Bind connection to specific agent (defaults to current agent)\n\n" +
|
||||||
|
"Examples:\n" +
|
||||||
|
' /listen --name "george" # Uses current agent\n' +
|
||||||
|
' /listen --name "laptop-work" --agent agent-abc123\n\n' +
|
||||||
|
"Once connected, this instance will listen for incoming messages from cloud agents.\n" +
|
||||||
|
"Messages will be executed locally using your letta-code environment.",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
const connectionName = opts.name;
|
||||||
|
const agentId = opts.agentId;
|
||||||
|
|
||||||
|
if (!connectionName) {
|
||||||
|
addCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
msg,
|
||||||
|
"Error: --name is required\n\n" +
|
||||||
|
'Usage: /listen --name "george"\n\n' +
|
||||||
|
"Provide a friendly name to identify this connection (e.g., your name, device name).",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
addCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
msg,
|
||||||
|
"Error: No agent specified\n\n" +
|
||||||
|
"This connection needs a default agent to execute messages.\n" +
|
||||||
|
"If you're seeing this, it means no agent is active in this conversation.\n\n" +
|
||||||
|
"Please start a conversation with an agent first, or specify one explicitly:\n" +
|
||||||
|
' /listen --name "george" --agent agent-abc123',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listen flow
|
||||||
|
ctx.setCommandRunning(true);
|
||||||
|
|
||||||
|
const cmdId = addCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
msg,
|
||||||
|
"Connecting to Letta Cloud...",
|
||||||
|
true,
|
||||||
|
"running",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get device ID (stable across sessions)
|
||||||
|
const deviceId = settingsManager.getOrCreateDeviceId();
|
||||||
|
const deviceName = hostname();
|
||||||
|
|
||||||
|
updateCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
cmdId,
|
||||||
|
msg,
|
||||||
|
`Registering listener "${connectionName}"...\n` +
|
||||||
|
`Device: ${deviceName} (${deviceId.slice(0, 8)}...)`,
|
||||||
|
true,
|
||||||
|
"running",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register with cloud to get connectionId
|
||||||
|
const serverUrl = getServerUrl();
|
||||||
|
const settings = await settingsManager.getSettingsWithSecureTokens();
|
||||||
|
const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error("Missing LETTA_API_KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call register endpoint
|
||||||
|
const registerUrl = `${serverUrl}/v1/listeners/register`;
|
||||||
|
const registerResponse = await fetch(registerUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"X-Letta-Source": "letta-code",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
deviceId,
|
||||||
|
connectionName,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!registerResponse.ok) {
|
||||||
|
const error = (await registerResponse.json()) as { message?: string };
|
||||||
|
throw new Error(error.message || "Registration failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { connectionId, wsUrl } = (await registerResponse.json()) as {
|
||||||
|
connectionId: string;
|
||||||
|
wsUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build agent info message
|
||||||
|
const adeUrl = `https://app.letta.com/agents/${agentId}`;
|
||||||
|
const agentInfo = `Agent: ${agentId}\n→ ${adeUrl}\n\n`;
|
||||||
|
|
||||||
|
updateCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
cmdId,
|
||||||
|
msg,
|
||||||
|
`✓ Registered successfully!\n\n` +
|
||||||
|
`Connection ID: ${connectionId}\n` +
|
||||||
|
`Name: "${connectionName}"\n` +
|
||||||
|
agentInfo +
|
||||||
|
`WebSocket: ${wsUrl}\n\n` +
|
||||||
|
`Starting WebSocket connection...`,
|
||||||
|
true,
|
||||||
|
"running",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Import and start WebSocket client
|
||||||
|
const { startListenerClient } = await import(
|
||||||
|
"../../websocket/listen-client"
|
||||||
|
);
|
||||||
|
|
||||||
|
await startListenerClient({
|
||||||
|
connectionId,
|
||||||
|
wsUrl,
|
||||||
|
deviceId,
|
||||||
|
connectionName,
|
||||||
|
agentId,
|
||||||
|
onStatusChange: (status, connId) => {
|
||||||
|
const adeUrl = `https://app.letta.com/agents/${agentId}?deviceId=${connId}`;
|
||||||
|
const statusText =
|
||||||
|
status === "receiving"
|
||||||
|
? "Receiving message"
|
||||||
|
: status === "processing"
|
||||||
|
? "Processing message"
|
||||||
|
: "Awaiting instructions";
|
||||||
|
|
||||||
|
updateCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
cmdId,
|
||||||
|
msg,
|
||||||
|
`Connected to Letta Cloud\n` +
|
||||||
|
`${statusText}\n\n` +
|
||||||
|
`View in ADE → ${adeUrl}`,
|
||||||
|
true,
|
||||||
|
"finished",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onRetrying: (attempt, _maxAttempts, nextRetryIn) => {
|
||||||
|
const adeUrl = `https://app.letta.com/agents/${agentId}?deviceId=${connectionId}`;
|
||||||
|
updateCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
cmdId,
|
||||||
|
msg,
|
||||||
|
`Reconnecting to Letta Cloud...\n` +
|
||||||
|
`Attempt ${attempt}, retrying in ${Math.round(nextRetryIn / 1000)}s\n\n` +
|
||||||
|
`View in ADE → ${adeUrl}`,
|
||||||
|
true,
|
||||||
|
"running",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onConnected: () => {
|
||||||
|
const adeUrl = `https://app.letta.com/agents/${agentId}?deviceId=${connectionId}`;
|
||||||
|
|
||||||
|
updateCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
cmdId,
|
||||||
|
msg,
|
||||||
|
`Connected to Letta Cloud\n` +
|
||||||
|
`Awaiting instructions\n\n` +
|
||||||
|
`View in ADE → ${adeUrl}`,
|
||||||
|
true,
|
||||||
|
"finished",
|
||||||
|
);
|
||||||
|
ctx.setCommandRunning(false);
|
||||||
|
},
|
||||||
|
onDisconnected: () => {
|
||||||
|
updateCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
cmdId,
|
||||||
|
msg,
|
||||||
|
`✗ Listener disconnected\n\n` + `Connection to Letta Cloud was lost.`,
|
||||||
|
false,
|
||||||
|
"finished",
|
||||||
|
);
|
||||||
|
ctx.setCommandRunning(false);
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
updateCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
cmdId,
|
||||||
|
msg,
|
||||||
|
`✗ Listener error: ${getErrorMessage(error)}`,
|
||||||
|
false,
|
||||||
|
"finished",
|
||||||
|
);
|
||||||
|
ctx.setCommandRunning(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
updateCommandResult(
|
||||||
|
ctx.buffersRef,
|
||||||
|
ctx.refreshDerived,
|
||||||
|
cmdId,
|
||||||
|
msg,
|
||||||
|
`✗ Failed to start listener: ${getErrorMessage(error)}`,
|
||||||
|
false,
|
||||||
|
"finished",
|
||||||
|
);
|
||||||
|
ctx.setCommandRunning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/cli/components/ListenerStatusUI.tsx
Normal file
77
src/cli/components/ListenerStatusUI.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { Box, Text } from "ink";
|
||||||
|
import Spinner from "ink-spinner";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface ListenerStatusUIProps {
|
||||||
|
agentId: string;
|
||||||
|
connectionId: string;
|
||||||
|
onReady: (callbacks: {
|
||||||
|
updateStatus: (status: "idle" | "receiving" | "processing") => void;
|
||||||
|
updateRetryStatus: (attempt: number, nextRetryIn: number) => void;
|
||||||
|
clearRetryStatus: () => void;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListenerStatusUI(props: ListenerStatusUIProps) {
|
||||||
|
const { agentId, connectionId, onReady } = props;
|
||||||
|
const [status, setStatus] = useState<"idle" | "receiving" | "processing">(
|
||||||
|
"idle",
|
||||||
|
);
|
||||||
|
const [retryInfo, setRetryInfo] = useState<{
|
||||||
|
attempt: number;
|
||||||
|
nextRetryIn: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onReady({
|
||||||
|
updateStatus: setStatus,
|
||||||
|
updateRetryStatus: (attempt, nextRetryIn) => {
|
||||||
|
setRetryInfo({ attempt, nextRetryIn });
|
||||||
|
},
|
||||||
|
clearRetryStatus: () => {
|
||||||
|
setRetryInfo(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [onReady]);
|
||||||
|
|
||||||
|
const adeUrl = `https://app.letta.com/agents/${agentId}?deviceId=${connectionId}`;
|
||||||
|
|
||||||
|
const statusText = retryInfo
|
||||||
|
? `Reconnecting (attempt ${retryInfo.attempt}, retry in ${Math.round(retryInfo.nextRetryIn / 1000)}s)`
|
||||||
|
: status === "receiving"
|
||||||
|
? "Receiving message"
|
||||||
|
: status === "processing"
|
||||||
|
? "Processing message"
|
||||||
|
: "Awaiting instructions";
|
||||||
|
|
||||||
|
const showSpinner = status !== "idle" || retryInfo !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
||||||
|
<Box marginBottom={1}>
|
||||||
|
<Text bold color="green">
|
||||||
|
Connected to Letta Cloud
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginBottom={1}>
|
||||||
|
{showSpinner && (
|
||||||
|
<Text>
|
||||||
|
<Text color={retryInfo ? "yellow" : "cyan"}>
|
||||||
|
<Spinner type="dots" />
|
||||||
|
</Text>{" "}
|
||||||
|
<Text color={retryInfo ? "yellow" : undefined}>{statusText}</Text>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!showSpinner && <Text dimColor>{statusText}</Text>}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text dimColor>View in ADE → </Text>
|
||||||
|
<Text color="blue" underline>
|
||||||
|
{adeUrl}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
187
src/cli/subcommands/listen.tsx
Normal file
187
src/cli/subcommands/listen.tsx
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* CLI subcommand: letta listen --name "george"
|
||||||
|
* Register letta-code as a listener to receive messages from Letta Cloud
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parseArgs } from "node:util";
|
||||||
|
import { render } from "ink";
|
||||||
|
import { getServerUrl } from "../../agent/client";
|
||||||
|
import { settingsManager } from "../../settings-manager";
|
||||||
|
import { ListenerStatusUI } from "../components/ListenerStatusUI";
|
||||||
|
|
||||||
|
export async function runListenSubcommand(argv: string[]): Promise<number> {
|
||||||
|
// Parse arguments
|
||||||
|
const { values } = parseArgs({
|
||||||
|
args: argv,
|
||||||
|
options: {
|
||||||
|
name: { type: "string" },
|
||||||
|
agent: { type: "string" },
|
||||||
|
help: { type: "boolean", short: "h" },
|
||||||
|
},
|
||||||
|
allowPositionals: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show help
|
||||||
|
if (values.help) {
|
||||||
|
console.log(
|
||||||
|
"Usage: letta listen --name <connection-name> [--agent <agent-id>]\n",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"Register this letta-code instance to receive messages from Letta Cloud.\n",
|
||||||
|
);
|
||||||
|
console.log("Options:");
|
||||||
|
console.log(
|
||||||
|
" --name <name> Friendly name for this connection (required)",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" --agent <id> Bind connection to specific agent (required for CLI usage)",
|
||||||
|
);
|
||||||
|
console.log(" -h, --help Show this help message\n");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(' letta listen --name "george" --agent agent-abc123');
|
||||||
|
console.log(' letta listen --name "laptop-work" --agent agent-xyz789\n');
|
||||||
|
console.log(
|
||||||
|
"Once connected, this instance will listen for incoming messages from cloud agents.",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"Messages will be executed locally using your letta-code environment.",
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionName = values.name;
|
||||||
|
const agentId = values.agent;
|
||||||
|
|
||||||
|
if (!connectionName) {
|
||||||
|
console.error("Error: --name is required\n");
|
||||||
|
console.error('Usage: letta listen --name "george" --agent agent-abc123\n');
|
||||||
|
console.error(
|
||||||
|
"Provide a friendly name to identify this connection (e.g., your name, device name).",
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
console.error("Error: --agent is required\n");
|
||||||
|
console.error('Usage: letta listen --name "george" --agent agent-abc123\n');
|
||||||
|
console.error(
|
||||||
|
"A listener connection needs a default agent to execute messages.",
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Specify which agent should receive messages from this connection.",
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get device ID
|
||||||
|
const deviceId = settingsManager.getOrCreateDeviceId();
|
||||||
|
|
||||||
|
// Get API key (include secure token storage fallback)
|
||||||
|
const settings = await settingsManager.getSettingsWithSecureTokens();
|
||||||
|
const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error("Error: LETTA_API_KEY not found");
|
||||||
|
console.error("Set your API key with: export LETTA_API_KEY=<your-key>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register with cloud
|
||||||
|
const serverUrl = getServerUrl();
|
||||||
|
const registerUrl = `${serverUrl}/v1/listeners/register`;
|
||||||
|
|
||||||
|
const registerResponse = await fetch(registerUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"X-Letta-Source": "letta-code",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
deviceId,
|
||||||
|
connectionName,
|
||||||
|
agentId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!registerResponse.ok) {
|
||||||
|
const error = (await registerResponse.json()) as { message?: string };
|
||||||
|
console.error(`Registration failed: ${error.message || "Unknown error"}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { connectionId, wsUrl } = (await registerResponse.json()) as {
|
||||||
|
connectionId: string;
|
||||||
|
wsUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear screen and render Ink UI
|
||||||
|
console.clear();
|
||||||
|
|
||||||
|
let updateStatusCallback:
|
||||||
|
| ((status: "idle" | "receiving" | "processing") => void)
|
||||||
|
| null = null;
|
||||||
|
let updateRetryStatusCallback:
|
||||||
|
| ((attempt: number, nextRetryIn: number) => void)
|
||||||
|
| null = null;
|
||||||
|
let clearRetryStatusCallback: (() => void) | null = null;
|
||||||
|
|
||||||
|
const { unmount } = render(
|
||||||
|
<ListenerStatusUI
|
||||||
|
agentId={agentId}
|
||||||
|
connectionId={connectionId}
|
||||||
|
onReady={(callbacks) => {
|
||||||
|
updateStatusCallback = callbacks.updateStatus;
|
||||||
|
updateRetryStatusCallback = callbacks.updateRetryStatus;
|
||||||
|
clearRetryStatusCallback = callbacks.clearRetryStatus;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Import and start WebSocket client
|
||||||
|
const { startListenerClient } = await import(
|
||||||
|
"../../websocket/listen-client"
|
||||||
|
);
|
||||||
|
|
||||||
|
await startListenerClient({
|
||||||
|
connectionId,
|
||||||
|
wsUrl,
|
||||||
|
deviceId,
|
||||||
|
connectionName,
|
||||||
|
agentId,
|
||||||
|
onStatusChange: (status) => {
|
||||||
|
clearRetryStatusCallback?.();
|
||||||
|
updateStatusCallback?.(status);
|
||||||
|
},
|
||||||
|
onConnected: () => {
|
||||||
|
clearRetryStatusCallback?.();
|
||||||
|
updateStatusCallback?.("idle");
|
||||||
|
},
|
||||||
|
onRetrying: (attempt, _maxAttempts, nextRetryIn) => {
|
||||||
|
updateRetryStatusCallback?.(attempt, nextRetryIn);
|
||||||
|
},
|
||||||
|
onDisconnected: () => {
|
||||||
|
unmount();
|
||||||
|
console.log("\n✗ Listener disconnected");
|
||||||
|
console.log("Connection to Letta Cloud was lost.\n");
|
||||||
|
process.exit(1);
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
unmount();
|
||||||
|
console.error(`\n✗ Listener error: ${error.message}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep process alive
|
||||||
|
return new Promise<number>(() => {
|
||||||
|
// Never resolves - runs until Ctrl+C
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to start listener: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { runAgentsSubcommand } from "./agents";
|
import { runAgentsSubcommand } from "./agents";
|
||||||
import { runBlocksSubcommand } from "./blocks";
|
import { runBlocksSubcommand } from "./blocks";
|
||||||
|
import { runListenSubcommand } from "./listen.tsx";
|
||||||
import { runMemfsSubcommand } from "./memfs";
|
import { runMemfsSubcommand } from "./memfs";
|
||||||
import { runMessagesSubcommand } from "./messages";
|
import { runMessagesSubcommand } from "./messages";
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|||||||
return runMessagesSubcommand(rest);
|
return runMessagesSubcommand(rest);
|
||||||
case "blocks":
|
case "blocks":
|
||||||
return runBlocksSubcommand(rest);
|
return runBlocksSubcommand(rest);
|
||||||
|
case "listen":
|
||||||
|
return runListenSubcommand(rest);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
552
src/websocket/listen-client.ts
Normal file
552
src/websocket/listen-client.ts
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket client for listen mode
|
||||||
|
* Connects to Letta Cloud and receives messages to execute locally
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Stream } from "@letta-ai/letta-client/core/streaming";
|
||||||
|
import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents";
|
||||||
|
import type {
|
||||||
|
ApprovalCreate,
|
||||||
|
LettaStreamingResponse,
|
||||||
|
ToolReturn,
|
||||||
|
} from "@letta-ai/letta-client/resources/agents/messages";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
import {
|
||||||
|
type ApprovalDecision,
|
||||||
|
type ApprovalResult,
|
||||||
|
executeApprovalBatch,
|
||||||
|
} from "../agent/approval-execution";
|
||||||
|
import { getResumeData } from "../agent/check-approval";
|
||||||
|
import { getClient } from "../agent/client";
|
||||||
|
import { sendMessageStream } from "../agent/message";
|
||||||
|
import { createBuffers } from "../cli/helpers/accumulator";
|
||||||
|
import { drainStreamWithResume } from "../cli/helpers/stream";
|
||||||
|
import { settingsManager } from "../settings-manager";
|
||||||
|
import { loadTools } from "../tools/manager";
|
||||||
|
|
||||||
|
interface StartListenerOptions {
|
||||||
|
connectionId: string;
|
||||||
|
wsUrl: string;
|
||||||
|
deviceId: string;
|
||||||
|
connectionName: string;
|
||||||
|
agentId?: string;
|
||||||
|
onConnected: () => void;
|
||||||
|
onDisconnected: () => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
onStatusChange?: (
|
||||||
|
status: "idle" | "receiving" | "processing",
|
||||||
|
connectionId: string,
|
||||||
|
) => void;
|
||||||
|
onRetrying?: (
|
||||||
|
attempt: number,
|
||||||
|
maxAttempts: number,
|
||||||
|
nextRetryIn: number,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PingMessage {
|
||||||
|
type: "ping";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PongMessage {
|
||||||
|
type: "pong";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IncomingMessage {
|
||||||
|
type: "message";
|
||||||
|
agentId?: string;
|
||||||
|
conversationId?: string;
|
||||||
|
messages: Array<MessageCreate | ApprovalCreate>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResultMessage {
|
||||||
|
type: "result";
|
||||||
|
success: boolean;
|
||||||
|
stopReason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunStartedMessage {
|
||||||
|
type: "run_started";
|
||||||
|
runId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerMessage = PongMessage | IncomingMessage;
|
||||||
|
type ClientMessage = PingMessage | ResultMessage | RunStartedMessage;
|
||||||
|
|
||||||
|
type ListenerRuntime = {
|
||||||
|
socket: WebSocket | null;
|
||||||
|
heartbeatInterval: NodeJS.Timeout | null;
|
||||||
|
reconnectTimeout: NodeJS.Timeout | null;
|
||||||
|
intentionallyClosed: boolean;
|
||||||
|
hasSuccessfulConnection: boolean;
|
||||||
|
messageQueue: Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApprovalSlot =
|
||||||
|
| { type: "result"; value: ApprovalResult }
|
||||||
|
| { type: "decision" };
|
||||||
|
|
||||||
|
// Listen mode supports one active connection per process.
|
||||||
|
let activeRuntime: ListenerRuntime | null = null;
|
||||||
|
|
||||||
|
const MAX_RETRY_DURATION_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
const INITIAL_RETRY_DELAY_MS = 1000; // 1 second
|
||||||
|
const MAX_RETRY_DELAY_MS = 30000; // 30 seconds
|
||||||
|
|
||||||
|
function createRuntime(): ListenerRuntime {
|
||||||
|
return {
|
||||||
|
socket: null,
|
||||||
|
heartbeatInterval: null,
|
||||||
|
reconnectTimeout: null,
|
||||||
|
intentionallyClosed: false,
|
||||||
|
hasSuccessfulConnection: false,
|
||||||
|
messageQueue: Promise.resolve(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearRuntimeTimers(runtime: ListenerRuntime): void {
|
||||||
|
if (runtime.reconnectTimeout) {
|
||||||
|
clearTimeout(runtime.reconnectTimeout);
|
||||||
|
runtime.reconnectTimeout = null;
|
||||||
|
}
|
||||||
|
if (runtime.heartbeatInterval) {
|
||||||
|
clearInterval(runtime.heartbeatInterval);
|
||||||
|
runtime.heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRuntime(
|
||||||
|
runtime: ListenerRuntime,
|
||||||
|
suppressCallbacks: boolean,
|
||||||
|
): void {
|
||||||
|
runtime.intentionallyClosed = true;
|
||||||
|
clearRuntimeTimers(runtime);
|
||||||
|
|
||||||
|
if (!runtime.socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const socket = runtime.socket;
|
||||||
|
runtime.socket = null;
|
||||||
|
|
||||||
|
// Stale runtimes being replaced should not emit callbacks/retries.
|
||||||
|
if (suppressCallbacks) {
|
||||||
|
socket.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
socket.readyState === WebSocket.OPEN ||
|
||||||
|
socket.readyState === WebSocket.CONNECTING
|
||||||
|
) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseServerMessage(data: WebSocket.RawData): ServerMessage | null {
|
||||||
|
try {
|
||||||
|
const raw = typeof data === "string" ? data : data.toString();
|
||||||
|
const parsed = JSON.parse(raw) as { type?: string };
|
||||||
|
if (parsed.type === "pong" || parsed.type === "message") {
|
||||||
|
return parsed as ServerMessage;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendClientMessage(socket: WebSocket, payload: ClientMessage): void {
|
||||||
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
|
socket.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApprovalExecutionPlan(
|
||||||
|
approvalMessage: ApprovalCreate,
|
||||||
|
pendingApprovals: Array<{
|
||||||
|
toolCallId: string;
|
||||||
|
toolName: string;
|
||||||
|
toolArgs: string;
|
||||||
|
}>,
|
||||||
|
): {
|
||||||
|
slots: ApprovalSlot[];
|
||||||
|
decisions: ApprovalDecision[];
|
||||||
|
} {
|
||||||
|
const pendingByToolCallId = new Map(
|
||||||
|
pendingApprovals.map((approval) => [approval.toolCallId, approval]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const slots: ApprovalSlot[] = [];
|
||||||
|
const decisions: ApprovalDecision[] = [];
|
||||||
|
|
||||||
|
for (const approval of approvalMessage.approvals ?? []) {
|
||||||
|
if (approval.type === "tool") {
|
||||||
|
slots.push({ type: "result", value: approval as ToolReturn });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approval.type !== "approval") {
|
||||||
|
slots.push({
|
||||||
|
type: "result",
|
||||||
|
value: {
|
||||||
|
type: "tool",
|
||||||
|
tool_call_id: "unknown",
|
||||||
|
tool_return: "Error: Unsupported approval payload",
|
||||||
|
status: "error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = pendingByToolCallId.get(approval.tool_call_id);
|
||||||
|
|
||||||
|
if (approval.approve) {
|
||||||
|
if (!pending) {
|
||||||
|
slots.push({
|
||||||
|
type: "result",
|
||||||
|
value: {
|
||||||
|
type: "tool",
|
||||||
|
tool_call_id: approval.tool_call_id,
|
||||||
|
tool_return: "Error: Pending approval not found",
|
||||||
|
status: "error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
decisions.push({
|
||||||
|
type: "approve",
|
||||||
|
approval: {
|
||||||
|
toolCallId: pending.toolCallId,
|
||||||
|
toolName: pending.toolName,
|
||||||
|
toolArgs: pending.toolArgs || "{}",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
slots.push({ type: "decision" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
decisions.push({
|
||||||
|
type: "deny",
|
||||||
|
approval: {
|
||||||
|
toolCallId: approval.tool_call_id,
|
||||||
|
toolName: pending?.toolName ?? "",
|
||||||
|
toolArgs: pending?.toolArgs ?? "{}",
|
||||||
|
},
|
||||||
|
reason:
|
||||||
|
typeof approval.reason === "string" && approval.reason.length > 0
|
||||||
|
? approval.reason
|
||||||
|
: "Tool execution denied",
|
||||||
|
});
|
||||||
|
slots.push({ type: "decision" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { slots, decisions };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the listener WebSocket client with automatic retry.
|
||||||
|
*/
|
||||||
|
export async function startListenerClient(
|
||||||
|
opts: StartListenerOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
// Replace any existing runtime without stale callback leakage.
|
||||||
|
if (activeRuntime) {
|
||||||
|
stopRuntime(activeRuntime, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtime = createRuntime();
|
||||||
|
activeRuntime = runtime;
|
||||||
|
|
||||||
|
await connectWithRetry(runtime, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to WebSocket with exponential backoff retry.
|
||||||
|
*/
|
||||||
|
async function connectWithRetry(
|
||||||
|
runtime: ListenerRuntime,
|
||||||
|
opts: StartListenerOptions,
|
||||||
|
attempt: number = 0,
|
||||||
|
startTime: number = Date.now(),
|
||||||
|
): Promise<void> {
|
||||||
|
if (runtime !== activeRuntime || runtime.intentionallyClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (attempt > 0) {
|
||||||
|
if (elapsedTime >= MAX_RETRY_DURATION_MS) {
|
||||||
|
opts.onError(new Error("Failed to connect after 5 minutes of retrying"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = Math.min(
|
||||||
|
INITIAL_RETRY_DELAY_MS * 2 ** (attempt - 1),
|
||||||
|
MAX_RETRY_DELAY_MS,
|
||||||
|
);
|
||||||
|
const maxAttempts = Math.ceil(
|
||||||
|
Math.log2(MAX_RETRY_DURATION_MS / INITIAL_RETRY_DELAY_MS),
|
||||||
|
);
|
||||||
|
|
||||||
|
opts.onRetrying?.(attempt, maxAttempts, delay);
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
runtime.reconnectTimeout = setTimeout(resolve, delay);
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.reconnectTimeout = null;
|
||||||
|
if (runtime !== activeRuntime || runtime.intentionallyClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearRuntimeTimers(runtime);
|
||||||
|
|
||||||
|
if (attempt === 0) {
|
||||||
|
await loadTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await settingsManager.getSettingsWithSecureTokens();
|
||||||
|
const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error("Missing LETTA_API_KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(opts.wsUrl);
|
||||||
|
url.searchParams.set("deviceId", opts.deviceId);
|
||||||
|
url.searchParams.set("connectionName", opts.connectionName);
|
||||||
|
if (opts.agentId) {
|
||||||
|
url.searchParams.set("agentId", opts.agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const socket = new WebSocket(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.socket = socket;
|
||||||
|
|
||||||
|
socket.on("open", () => {
|
||||||
|
if (runtime !== activeRuntime || runtime.intentionallyClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.hasSuccessfulConnection = true;
|
||||||
|
opts.onConnected();
|
||||||
|
|
||||||
|
runtime.heartbeatInterval = setInterval(() => {
|
||||||
|
sendClientMessage(socket, { type: "ping" });
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("message", (data: WebSocket.RawData) => {
|
||||||
|
const parsed = parseServerMessage(data);
|
||||||
|
if (!parsed || parsed.type !== "message") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.messageQueue = runtime.messageQueue
|
||||||
|
.then(async () => {
|
||||||
|
if (runtime !== activeRuntime || runtime.intentionallyClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.onStatusChange?.("receiving", opts.connectionId);
|
||||||
|
await handleIncomingMessage(
|
||||||
|
parsed,
|
||||||
|
socket,
|
||||||
|
opts.onStatusChange,
|
||||||
|
opts.connectionId,
|
||||||
|
);
|
||||||
|
opts.onStatusChange?.("idle", opts.connectionId);
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error("[Listen] Error handling queued message:", error);
|
||||||
|
}
|
||||||
|
opts.onStatusChange?.("idle", opts.connectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("close", (code: number, reason: Buffer) => {
|
||||||
|
if (runtime !== activeRuntime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.log(
|
||||||
|
`[Listen] WebSocket disconnected (code: ${code}, reason: ${reason.toString()})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearRuntimeTimers(runtime);
|
||||||
|
runtime.socket = null;
|
||||||
|
|
||||||
|
if (runtime.intentionallyClosed) {
|
||||||
|
opts.onDisconnected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we had connected before, restart backoff from zero for this outage window.
|
||||||
|
const nextAttempt = runtime.hasSuccessfulConnection ? 0 : attempt + 1;
|
||||||
|
const nextStartTime = runtime.hasSuccessfulConnection
|
||||||
|
? Date.now()
|
||||||
|
: startTime;
|
||||||
|
runtime.hasSuccessfulConnection = false;
|
||||||
|
|
||||||
|
connectWithRetry(runtime, opts, nextAttempt, nextStartTime).catch(
|
||||||
|
(error) => {
|
||||||
|
opts.onError(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", (error: Error) => {
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error("[Listen] WebSocket error:", error);
|
||||||
|
}
|
||||||
|
// Error triggers close(), which handles retry logic.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming message from the cloud.
|
||||||
|
*/
|
||||||
|
async function handleIncomingMessage(
|
||||||
|
msg: IncomingMessage,
|
||||||
|
socket: WebSocket,
|
||||||
|
onStatusChange?: (
|
||||||
|
status: "idle" | "receiving" | "processing",
|
||||||
|
connectionId: string,
|
||||||
|
) => void,
|
||||||
|
connectionId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const agentId = msg.agentId;
|
||||||
|
const requestedConversationId = msg.conversationId;
|
||||||
|
const conversationId = requestedConversationId ?? "default";
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionId) {
|
||||||
|
onStatusChange?.("processing", connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let messagesToSend: Array<MessageCreate | ApprovalCreate> = msg.messages;
|
||||||
|
|
||||||
|
const firstMessage = msg.messages[0];
|
||||||
|
const isApprovalMessage =
|
||||||
|
firstMessage &&
|
||||||
|
"type" in firstMessage &&
|
||||||
|
firstMessage.type === "approval" &&
|
||||||
|
"approvals" in firstMessage;
|
||||||
|
|
||||||
|
if (isApprovalMessage) {
|
||||||
|
const approvalMessage = firstMessage as ApprovalCreate;
|
||||||
|
const client = await getClient();
|
||||||
|
const agent = await client.agents.retrieve(agentId);
|
||||||
|
const resumeData = await getResumeData(
|
||||||
|
client,
|
||||||
|
agent,
|
||||||
|
requestedConversationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { slots, decisions } = buildApprovalExecutionPlan(
|
||||||
|
approvalMessage,
|
||||||
|
resumeData.pendingApprovals,
|
||||||
|
);
|
||||||
|
const decisionResults =
|
||||||
|
decisions.length > 0 ? await executeApprovalBatch(decisions) : [];
|
||||||
|
|
||||||
|
const rebuiltApprovals: ApprovalResult[] = [];
|
||||||
|
let decisionResultIndex = 0;
|
||||||
|
|
||||||
|
for (const slot of slots) {
|
||||||
|
if (slot.type === "result") {
|
||||||
|
rebuiltApprovals.push(slot.value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = decisionResults[decisionResultIndex];
|
||||||
|
if (next) {
|
||||||
|
rebuiltApprovals.push(next);
|
||||||
|
decisionResultIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuiltApprovals.push({
|
||||||
|
type: "tool",
|
||||||
|
tool_call_id: "unknown",
|
||||||
|
tool_return: "Error: Missing approval execution result",
|
||||||
|
status: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesToSend = [
|
||||||
|
{
|
||||||
|
type: "approval",
|
||||||
|
approvals: rebuiltApprovals,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = await sendMessageStream(conversationId, messagesToSend, {
|
||||||
|
agentId,
|
||||||
|
streamTokens: true,
|
||||||
|
background: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let runIdSent = false;
|
||||||
|
|
||||||
|
const buffers = createBuffers(agentId);
|
||||||
|
const result = await drainStreamWithResume(
|
||||||
|
stream as Stream<LettaStreamingResponse>,
|
||||||
|
buffers,
|
||||||
|
() => {},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
({ chunk }) => {
|
||||||
|
const maybeRunId = (chunk as { run_id?: unknown }).run_id;
|
||||||
|
if (!runIdSent && typeof maybeRunId === "string") {
|
||||||
|
runIdSent = true;
|
||||||
|
sendClientMessage(socket, {
|
||||||
|
type: "run_started",
|
||||||
|
runId: maybeRunId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
sendClientMessage(socket, {
|
||||||
|
type: "result",
|
||||||
|
success: result.stopReason === "end_turn",
|
||||||
|
stopReason: result.stopReason,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
sendClientMessage(socket, {
|
||||||
|
type: "result",
|
||||||
|
success: false,
|
||||||
|
stopReason: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the active listener connection.
|
||||||
|
*/
|
||||||
|
export function stopListenerClient(): void {
|
||||||
|
if (!activeRuntime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtime = activeRuntime;
|
||||||
|
activeRuntime = null;
|
||||||
|
stopRuntime(runtime, true);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user