mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
new gateway
This commit is contained in:
parent
01958298e0
commit
9a0b848789
34 changed files with 1632 additions and 290 deletions
634
package-lock.json
generated
634
package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "co-mono",
|
"name": "co-mono",
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "co-mono",
|
"name": "co-mono",
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
@ -958,6 +958,30 @@
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@e9n/pi-channels": {
|
"node_modules/@e9n/pi-channels": {
|
||||||
"resolved": "packages/pi-channels",
|
"resolved": "packages/pi-channels",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -3538,6 +3562,13 @@
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/cli": {
|
"node_modules/@tailwindcss/cli": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.1.tgz",
|
||||||
|
|
@ -3843,6 +3874,34 @@
|
||||||
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
|
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tsconfig/node10": {
|
||||||
|
"version": "1.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
|
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node12": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node14": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node16": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
|
@ -4214,6 +4273,32 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/acorn": {
|
||||||
|
"version": "8.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"acorn": "bin/acorn"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
|
||||||
|
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
|
@ -4295,6 +4380,22 @@
|
||||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sprintf-js": "~1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
|
@ -4817,6 +4918,13 @@
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/croner": {
|
"node_modules/croner": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/croner/-/croner-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/croner/-/croner-9.1.0.tgz",
|
||||||
|
|
@ -5254,6 +5362,18 @@
|
||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/extend-shallow": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-extendable": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/extract-zip": {
|
"node_modules/extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
|
|
@ -5701,6 +5821,21 @@
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/gray-matter": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-yaml": "^3.13.1",
|
||||||
|
"kind-of": "^6.0.2",
|
||||||
|
"section-matter": "^1.0.0",
|
||||||
|
"strip-bom-string": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
|
@ -5907,6 +6042,15 @@
|
||||||
"integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==",
|
"integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-extendable": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
|
@ -5990,6 +6134,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/js-yaml": {
|
||||||
|
"version": "3.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||||
|
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^1.0.7",
|
||||||
|
"esprima": "^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-bigint": {
|
"node_modules/json-bigint": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||||
|
|
@ -6115,6 +6272,15 @@
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kind-of": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/koffi": {
|
"node_modules/koffi": {
|
||||||
"version": "2.15.1",
|
"version": "2.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.1.tgz",
|
||||||
|
|
@ -6458,6 +6624,13 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "15.0.12",
|
"version": "15.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||||
|
|
@ -6726,6 +6899,17 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/obug": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/sxzz",
|
||||||
|
"https://opencollective.com/debug"
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ollama": {
|
"node_modules/ollama": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz",
|
||||||
|
|
@ -6976,6 +7160,14 @@
|
||||||
"resolved": "packages/coding-agent/examples/extensions/with-deps",
|
"resolved": "packages/coding-agent/examples/extensions/with-deps",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/pi-memory-md": {
|
||||||
|
"resolved": "packages/pi-memory-md",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/pi-teams": {
|
||||||
|
"resolved": "packages/pi-teams",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/pi-web-ui-example": {
|
"node_modules/pi-web-ui-example": {
|
||||||
"resolved": "packages/web-ui/example",
|
"resolved": "packages/web-ui/example",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -7399,6 +7591,19 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/section-matter": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"extend-shallow": "^2.0.1",
|
||||||
|
"kind-of": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
|
@ -7606,6 +7811,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sprintf-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/stackback": {
|
"node_modules/stackback": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
|
@ -7679,6 +7890,15 @@
|
||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-bom-string": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-eof": {
|
"node_modules/strip-eof": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||||
|
|
@ -8004,6 +8224,60 @@
|
||||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-node": {
|
||||||
|
"version": "10.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.1",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-node": "dist/bin.js",
|
||||||
|
"ts-node-cwd": "dist/bin-cwd.js",
|
||||||
|
"ts-node-esm": "dist/bin-esm.js",
|
||||||
|
"ts-node-script": "dist/bin-script.js",
|
||||||
|
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||||
|
"ts-script": "dist/bin-script-deprecated.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": ">=1.2.50",
|
||||||
|
"@swc/wasm": ">=1.2.50",
|
||||||
|
"@types/node": "*",
|
||||||
|
"typescript": ">=2.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/wasm": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-node/node_modules/diff": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
|
@ -8099,6 +8373,26 @@
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/esm/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
|
|
@ -8494,6 +8788,16 @@
|
||||||
"fd-slicer": "~1.1.0"
|
"fd-slicer": "~1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yoctocolors": {
|
"node_modules/yoctocolors": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
||||||
|
|
@ -8764,6 +9068,66 @@
|
||||||
"@sinclair/typebox": "*"
|
"@sinclair/typebox": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/pi-memory-md": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"gray-matter": "^4.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mariozechner/pi-coding-agent": "latest",
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-memory-md/node_modules/@mariozechner/pi-coding-agent": {
|
||||||
|
"version": "0.56.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.56.2.tgz",
|
||||||
|
"integrity": "sha512-svK9zg5f+I4yko57MzdfBQBqZpFT1Hr8nZ3o7nYMTuIFcf2vABylA8lNI57Avjg38js1PToc6jXXFa/3JWqELg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@mariozechner/jiti": "^2.6.2",
|
||||||
|
"@mariozechner/pi-agent-core": "^0.56.2",
|
||||||
|
"@mariozechner/pi-ai": "^0.56.2",
|
||||||
|
"@mariozechner/pi-tui": "^0.56.2",
|
||||||
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||||
|
"chalk": "^5.5.0",
|
||||||
|
"cli-highlight": "^2.1.11",
|
||||||
|
"diff": "^8.0.2",
|
||||||
|
"extract-zip": "^2.0.1",
|
||||||
|
"file-type": "^21.1.1",
|
||||||
|
"glob": "^13.0.1",
|
||||||
|
"hosted-git-info": "^9.0.2",
|
||||||
|
"ignore": "^7.0.5",
|
||||||
|
"marked": "^15.0.12",
|
||||||
|
"minimatch": "^10.2.3",
|
||||||
|
"proper-lockfile": "^4.1.2",
|
||||||
|
"strip-ansi": "^7.1.0",
|
||||||
|
"undici": "^7.19.1",
|
||||||
|
"yaml": "^2.8.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pi": "dist/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.6.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@mariozechner/clipboard": "^0.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-memory-md/node_modules/@types/node": {
|
||||||
|
"version": "20.19.37",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
||||||
|
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/pi-runtime-daemon": {
|
"packages/pi-runtime-daemon": {
|
||||||
"name": "@local/pi-runtime-daemon",
|
"name": "@local/pi-runtime-daemon",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
|
@ -8775,6 +9139,272 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/pi-teams": {
|
||||||
|
"version": "0.8.6",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": "^11.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.3.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@mariozechner/pi-coding-agent": "*",
|
||||||
|
"@sinclair/typebox": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/@types/node": {
|
||||||
|
"version": "25.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz",
|
||||||
|
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/@vitest/expect": {
|
||||||
|
"version": "4.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
||||||
|
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/spy": "4.0.18",
|
||||||
|
"@vitest/utils": "4.0.18",
|
||||||
|
"chai": "^6.2.1",
|
||||||
|
"tinyrainbow": "^3.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/@vitest/mocker": {
|
||||||
|
"version": "4.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
|
||||||
|
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "4.0.18",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.21"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0-0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "4.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
||||||
|
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^3.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/@vitest/runner": {
|
||||||
|
"version": "4.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
|
||||||
|
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "4.0.18",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/@vitest/snapshot": {
|
||||||
|
"version": "4.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
||||||
|
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.0.18",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/@vitest/spy": {
|
||||||
|
"version": "4.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
|
||||||
|
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/@vitest/utils": {
|
||||||
|
"version": "4.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
|
||||||
|
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.0.18",
|
||||||
|
"tinyrainbow": "^3.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/chai": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/picomatch": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/tinyexec": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/tinyrainbow": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/undici-types": {
|
||||||
|
"version": "7.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
|
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"packages/pi-teams/node_modules/vitest": {
|
||||||
|
"version": "4.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
||||||
|
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "4.0.18",
|
||||||
|
"@vitest/mocker": "4.0.18",
|
||||||
|
"@vitest/pretty-format": "4.0.18",
|
||||||
|
"@vitest/runner": "4.0.18",
|
||||||
|
"@vitest/snapshot": "4.0.18",
|
||||||
|
"@vitest/spy": "4.0.18",
|
||||||
|
"@vitest/utils": "4.0.18",
|
||||||
|
"es-module-lexer": "^1.7.0",
|
||||||
|
"expect-type": "^1.2.2",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"std-env": "^3.10.0",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^1.0.2",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"tinyrainbow": "^3.0.3",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
|
"@vitest/browser-playwright": "4.0.18",
|
||||||
|
"@vitest/browser-preview": "4.0.18",
|
||||||
|
"@vitest/browser-webdriverio": "4.0.18",
|
||||||
|
"@vitest/ui": "4.0.18",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-playwright": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-preview": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-webdriverio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/pods": {
|
"packages/pods": {
|
||||||
"name": "@mariozechner/pi",
|
"name": "@mariozechner/pi",
|
||||||
"version": "0.56.2",
|
"version": "0.56.2",
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,8 @@ ${chalk.bold("Commands:")}
|
||||||
${APP_NAME} remove <source> [-l] Remove extension source from settings
|
${APP_NAME} remove <source> [-l] Remove extension source from settings
|
||||||
${APP_NAME} update [source] Update installed extensions (skips pinned sources)
|
${APP_NAME} update [source] Update installed extensions (skips pinned sources)
|
||||||
${APP_NAME} list List installed extensions from settings
|
${APP_NAME} list List installed extensions from settings
|
||||||
${APP_NAME} daemon Run in long-lived daemon mode (extensions stay active)
|
${APP_NAME} gateway Run the always-on gateway process
|
||||||
|
${APP_NAME} daemon Alias for gateway
|
||||||
${APP_NAME} config Open TUI to enable/disable package resources
|
${APP_NAME} config Open TUI to enable/disable package resources
|
||||||
${APP_NAME} <command> --help Show help for install/remove/update/list
|
${APP_NAME} <command> --help Show help for install/remove/update/list
|
||||||
|
|
||||||
|
|
|
||||||
652
packages/coding-agent/src/core/gateway-runtime.ts
Normal file
652
packages/coding-agent/src/core/gateway-runtime.ts
Normal file
|
|
@ -0,0 +1,652 @@
|
||||||
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { URL } from "node:url";
|
||||||
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
|
import type { AgentSession, AgentSessionEvent } from "./agent-session.js";
|
||||||
|
import { SessionManager } from "./session-manager.js";
|
||||||
|
|
||||||
|
export interface GatewayConfig {
|
||||||
|
bind: string;
|
||||||
|
port: number;
|
||||||
|
bearerToken?: string;
|
||||||
|
session: {
|
||||||
|
idleMinutes: number;
|
||||||
|
maxQueuePerSession: number;
|
||||||
|
};
|
||||||
|
webhook: {
|
||||||
|
enabled: boolean;
|
||||||
|
basePath: string;
|
||||||
|
secret?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GatewaySessionFactory = (sessionKey: string) => Promise<AgentSession>;
|
||||||
|
|
||||||
|
export interface GatewayMessageRequest {
|
||||||
|
sessionKey: string;
|
||||||
|
text: string;
|
||||||
|
source?: "interactive" | "rpc" | "extension";
|
||||||
|
images?: ImageContent[];
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GatewayMessageResult {
|
||||||
|
ok: boolean;
|
||||||
|
response: string;
|
||||||
|
error?: string;
|
||||||
|
sessionKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GatewaySessionSnapshot {
|
||||||
|
sessionKey: string;
|
||||||
|
sessionId: string;
|
||||||
|
messageCount: number;
|
||||||
|
queueDepth: number;
|
||||||
|
processing: boolean;
|
||||||
|
lastActiveAt: number;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GatewayRuntimeOptions {
|
||||||
|
config: GatewayConfig;
|
||||||
|
primarySessionKey: string;
|
||||||
|
primarySession: AgentSession;
|
||||||
|
createSession: GatewaySessionFactory;
|
||||||
|
log?: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GatewayQueuedMessage {
|
||||||
|
request: GatewayMessageRequest;
|
||||||
|
resolve: (result: GatewayMessageResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GatewayEvent =
|
||||||
|
| { type: "hello"; sessionKey: string; snapshot: GatewaySessionSnapshot }
|
||||||
|
| { type: "session_state"; sessionKey: string; snapshot: GatewaySessionSnapshot }
|
||||||
|
| { type: "turn_start"; sessionKey: string }
|
||||||
|
| { type: "turn_end"; sessionKey: string }
|
||||||
|
| { type: "message_start"; sessionKey: string; role?: string }
|
||||||
|
| { type: "token"; sessionKey: string; delta: string; contentIndex: number }
|
||||||
|
| { type: "thinking"; sessionKey: string; delta: string; contentIndex: number }
|
||||||
|
| { type: "tool_start"; sessionKey: string; toolCallId: string; toolName: string; args: unknown }
|
||||||
|
| { type: "tool_update"; sessionKey: string; toolCallId: string; toolName: string; partialResult: unknown }
|
||||||
|
| {
|
||||||
|
type: "tool_complete";
|
||||||
|
sessionKey: string;
|
||||||
|
toolCallId: string;
|
||||||
|
toolName: string;
|
||||||
|
result: unknown;
|
||||||
|
isError: boolean;
|
||||||
|
}
|
||||||
|
| { type: "message_complete"; sessionKey: string; text: string }
|
||||||
|
| { type: "error"; sessionKey: string; error: string }
|
||||||
|
| { type: "aborted"; sessionKey: string };
|
||||||
|
|
||||||
|
interface ManagedGatewaySession {
|
||||||
|
sessionKey: string;
|
||||||
|
session: AgentSession;
|
||||||
|
queue: GatewayQueuedMessage[];
|
||||||
|
processing: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
lastActiveAt: number;
|
||||||
|
listeners: Set<(event: GatewayEvent) => void>;
|
||||||
|
unsubscribe: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeGatewayRuntime: GatewayRuntime | null = null;
|
||||||
|
|
||||||
|
export function setActiveGatewayRuntime(runtime: GatewayRuntime | null): void {
|
||||||
|
activeGatewayRuntime = runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveGatewayRuntime(): GatewayRuntime | null {
|
||||||
|
return activeGatewayRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GatewayRuntime {
|
||||||
|
private readonly config: GatewayConfig;
|
||||||
|
private readonly primarySessionKey: string;
|
||||||
|
private readonly primarySession: AgentSession;
|
||||||
|
private readonly createSession: GatewaySessionFactory;
|
||||||
|
private readonly log: (message: string) => void;
|
||||||
|
private readonly sessions = new Map<string, ManagedGatewaySession>();
|
||||||
|
private readonly sessionDirRoot: string;
|
||||||
|
private server: Server | null = null;
|
||||||
|
private idleSweepTimer: NodeJS.Timeout | null = null;
|
||||||
|
private ready = false;
|
||||||
|
|
||||||
|
constructor(options: GatewayRuntimeOptions) {
|
||||||
|
this.config = options.config;
|
||||||
|
this.primarySessionKey = options.primarySessionKey;
|
||||||
|
this.primarySession = options.primarySession;
|
||||||
|
this.createSession = options.createSession;
|
||||||
|
this.log = options.log ?? (() => {});
|
||||||
|
this.sessionDirRoot = join(options.primarySession.sessionManager.getSessionDir(), "..", "gateway-sessions");
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
if (this.server) return;
|
||||||
|
|
||||||
|
await this.ensureSession(this.primarySessionKey, this.primarySession);
|
||||||
|
this.server = createServer((request, response) => {
|
||||||
|
void this.handleHttpRequest(request, response).catch((error) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
this.writeJson(response, 500, { error: message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
this.server?.once("error", reject);
|
||||||
|
this.server?.listen(this.config.port, this.config.bind, () => {
|
||||||
|
this.server?.off("error", reject);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.idleSweepTimer = setInterval(() => {
|
||||||
|
void this.evictIdleSessions();
|
||||||
|
}, 60_000);
|
||||||
|
this.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
this.ready = false;
|
||||||
|
if (this.idleSweepTimer) {
|
||||||
|
clearInterval(this.idleSweepTimer);
|
||||||
|
this.idleSweepTimer = null;
|
||||||
|
}
|
||||||
|
if (this.server) {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
this.server?.close((error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.server = null;
|
||||||
|
}
|
||||||
|
for (const [sessionKey, managedSession] of this.sessions) {
|
||||||
|
managedSession.unsubscribe();
|
||||||
|
if (sessionKey !== this.primarySessionKey) {
|
||||||
|
managedSession.session.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sessions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
isReady(): boolean {
|
||||||
|
return this.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddress(): { bind: string; port: number } {
|
||||||
|
return { bind: this.config.bind, port: this.config.port };
|
||||||
|
}
|
||||||
|
|
||||||
|
async enqueueMessage(request: GatewayMessageRequest): Promise<GatewayMessageResult> {
|
||||||
|
const managedSession = await this.ensureSession(request.sessionKey);
|
||||||
|
if (managedSession.queue.length >= this.config.session.maxQueuePerSession) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: "",
|
||||||
|
error: `Queue full (${this.config.session.maxQueuePerSession} pending).`,
|
||||||
|
sessionKey: request.sessionKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<GatewayMessageResult>((resolve) => {
|
||||||
|
managedSession.queue.push({ request, resolve });
|
||||||
|
this.emitState(managedSession);
|
||||||
|
void this.processNext(managedSession);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addSubscriber(sessionKey: string, listener: (event: GatewayEvent) => void): Promise<() => void> {
|
||||||
|
const managedSession = await this.ensureSession(sessionKey);
|
||||||
|
managedSession.listeners.add(listener);
|
||||||
|
listener({ type: "hello", sessionKey, snapshot: this.createSnapshot(managedSession) });
|
||||||
|
return () => {
|
||||||
|
managedSession.listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
abortSession(sessionKey: string): boolean {
|
||||||
|
const managedSession = this.sessions.get(sessionKey);
|
||||||
|
if (!managedSession?.processing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void managedSession.session.abort().catch((error) => {
|
||||||
|
this.emit(managedSession, {
|
||||||
|
type: "error",
|
||||||
|
sessionKey,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearQueue(sessionKey: string): void {
|
||||||
|
const managedSession = this.sessions.get(sessionKey);
|
||||||
|
if (!managedSession) return;
|
||||||
|
managedSession.queue.length = 0;
|
||||||
|
this.emitState(managedSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetSession(sessionKey: string): Promise<void> {
|
||||||
|
const managedSession = this.sessions.get(sessionKey);
|
||||||
|
if (!managedSession) return;
|
||||||
|
|
||||||
|
if (sessionKey === this.primarySessionKey) {
|
||||||
|
await managedSession.session.newSession();
|
||||||
|
managedSession.queue.length = 0;
|
||||||
|
managedSession.processing = false;
|
||||||
|
managedSession.lastActiveAt = Date.now();
|
||||||
|
this.emitState(managedSession);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (managedSession.processing) {
|
||||||
|
await managedSession.session.abort();
|
||||||
|
}
|
||||||
|
managedSession.unsubscribe();
|
||||||
|
managedSession.session.dispose();
|
||||||
|
this.sessions.delete(sessionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
listSessions(): GatewaySessionSnapshot[] {
|
||||||
|
return Array.from(this.sessions.values()).map((session) => this.createSnapshot(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSession(sessionKey: string): GatewaySessionSnapshot | undefined {
|
||||||
|
const session = this.sessions.get(sessionKey);
|
||||||
|
return session ? this.createSnapshot(session) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureSession(sessionKey: string, existingSession?: AgentSession): Promise<ManagedGatewaySession> {
|
||||||
|
const found = this.sessions.get(sessionKey);
|
||||||
|
if (found) {
|
||||||
|
found.lastActiveAt = Date.now();
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = existingSession ?? (await this.createSession(sessionKey));
|
||||||
|
const managedSession: ManagedGatewaySession = {
|
||||||
|
sessionKey,
|
||||||
|
session,
|
||||||
|
queue: [],
|
||||||
|
processing: false,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
lastActiveAt: Date.now(),
|
||||||
|
listeners: new Set(),
|
||||||
|
unsubscribe: () => {},
|
||||||
|
};
|
||||||
|
managedSession.unsubscribe = session.subscribe((event) => {
|
||||||
|
this.handleSessionEvent(managedSession, event);
|
||||||
|
});
|
||||||
|
this.sessions.set(sessionKey, managedSession);
|
||||||
|
this.emitState(managedSession);
|
||||||
|
return managedSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processNext(managedSession: ManagedGatewaySession): Promise<void> {
|
||||||
|
if (managedSession.processing || managedSession.queue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queued = managedSession.queue.shift();
|
||||||
|
if (!queued) return;
|
||||||
|
|
||||||
|
managedSession.processing = true;
|
||||||
|
managedSession.lastActiveAt = Date.now();
|
||||||
|
this.emitState(managedSession);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await managedSession.session.prompt(queued.request.text, {
|
||||||
|
images: queued.request.images,
|
||||||
|
source: queued.request.source ?? "extension",
|
||||||
|
});
|
||||||
|
const response = getLastAssistantText(managedSession.session);
|
||||||
|
queued.resolve({
|
||||||
|
ok: true,
|
||||||
|
response,
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
if (message.includes("aborted")) {
|
||||||
|
this.emit(managedSession, { type: "aborted", sessionKey: managedSession.sessionKey });
|
||||||
|
} else {
|
||||||
|
this.emit(managedSession, { type: "error", sessionKey: managedSession.sessionKey, error: message });
|
||||||
|
}
|
||||||
|
queued.resolve({
|
||||||
|
ok: false,
|
||||||
|
response: "",
|
||||||
|
error: message,
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
managedSession.processing = false;
|
||||||
|
managedSession.lastActiveAt = Date.now();
|
||||||
|
this.emitState(managedSession);
|
||||||
|
if (managedSession.queue.length > 0) {
|
||||||
|
void this.processNext(managedSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSessionEvent(managedSession: ManagedGatewaySession, event: AgentSessionEvent): void {
|
||||||
|
switch (event.type) {
|
||||||
|
case "turn_start":
|
||||||
|
this.emit(managedSession, { type: "turn_start", sessionKey: managedSession.sessionKey });
|
||||||
|
return;
|
||||||
|
case "turn_end":
|
||||||
|
this.emit(managedSession, { type: "turn_end", sessionKey: managedSession.sessionKey });
|
||||||
|
return;
|
||||||
|
case "message_start":
|
||||||
|
this.emit(managedSession, {
|
||||||
|
type: "message_start",
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
role: event.message.role,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
case "message_update":
|
||||||
|
switch (event.assistantMessageEvent.type) {
|
||||||
|
case "text_delta":
|
||||||
|
this.emit(managedSession, {
|
||||||
|
type: "token",
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
delta: event.assistantMessageEvent.delta,
|
||||||
|
contentIndex: event.assistantMessageEvent.contentIndex,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
case "thinking_delta":
|
||||||
|
this.emit(managedSession, {
|
||||||
|
type: "thinking",
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
delta: event.assistantMessageEvent.delta,
|
||||||
|
contentIndex: event.assistantMessageEvent.contentIndex,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case "message_end":
|
||||||
|
if (event.message.role === "assistant") {
|
||||||
|
this.emit(managedSession, {
|
||||||
|
type: "message_complete",
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
text: extractMessageText(event.message),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case "tool_execution_start":
|
||||||
|
this.emit(managedSession, {
|
||||||
|
type: "tool_start",
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
toolCallId: event.toolCallId,
|
||||||
|
toolName: event.toolName,
|
||||||
|
args: event.args,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
case "tool_execution_update":
|
||||||
|
this.emit(managedSession, {
|
||||||
|
type: "tool_update",
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
toolCallId: event.toolCallId,
|
||||||
|
toolName: event.toolName,
|
||||||
|
partialResult: event.partialResult,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
case "tool_execution_end":
|
||||||
|
this.emit(managedSession, {
|
||||||
|
type: "tool_complete",
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
toolCallId: event.toolCallId,
|
||||||
|
toolName: event.toolName,
|
||||||
|
result: event.result,
|
||||||
|
isError: event.isError,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit(managedSession: ManagedGatewaySession, event: GatewayEvent): void {
|
||||||
|
for (const listener of managedSession.listeners) {
|
||||||
|
listener(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitState(managedSession: ManagedGatewaySession): void {
|
||||||
|
this.emit(managedSession, {
|
||||||
|
type: "session_state",
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
snapshot: this.createSnapshot(managedSession),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSnapshot(managedSession: ManagedGatewaySession): GatewaySessionSnapshot {
|
||||||
|
return {
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
sessionId: managedSession.session.sessionId,
|
||||||
|
messageCount: managedSession.session.messages.length,
|
||||||
|
queueDepth: managedSession.queue.length,
|
||||||
|
processing: managedSession.processing,
|
||||||
|
lastActiveAt: managedSession.lastActiveAt,
|
||||||
|
createdAt: managedSession.createdAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async evictIdleSessions(): Promise<void> {
|
||||||
|
const cutoff = Date.now() - this.config.session.idleMinutes * 60_000;
|
||||||
|
for (const [sessionKey, managedSession] of this.sessions) {
|
||||||
|
if (sessionKey === this.primarySessionKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (managedSession.processing || managedSession.queue.length > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (managedSession.lastActiveAt > cutoff) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (managedSession.listeners.size > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
managedSession.unsubscribe();
|
||||||
|
managedSession.session.dispose();
|
||||||
|
this.sessions.delete(sessionKey);
|
||||||
|
this.log(`evicted idle session ${sessionKey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleHttpRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
||||||
|
const method = request.method ?? "GET";
|
||||||
|
const url = new URL(
|
||||||
|
request.url ?? "/",
|
||||||
|
`http://${request.headers.host ?? `${this.config.bind}:${this.config.port}`}`,
|
||||||
|
);
|
||||||
|
const path = url.pathname;
|
||||||
|
|
||||||
|
if (method === "GET" && path === "/health") {
|
||||||
|
this.writeJson(response, 200, { ok: true, ready: this.ready });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "GET" && path === "/ready") {
|
||||||
|
this.requireAuth(request, response);
|
||||||
|
if (response.writableEnded) return;
|
||||||
|
this.writeJson(response, 200, { ok: true, ready: this.ready, sessions: this.sessions.size });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.webhook.enabled && method === "POST" && path.startsWith(this.config.webhook.basePath)) {
|
||||||
|
await this.handleWebhookRequest(path, request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requireAuth(request, response);
|
||||||
|
if (response.writableEnded) return;
|
||||||
|
|
||||||
|
if (method === "GET" && path === "/sessions") {
|
||||||
|
this.writeJson(response, 200, { sessions: this.listSessions() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionMatch = path.match(/^\/sessions\/([^/]+)(?:\/(events|messages|abort|reset))?$/);
|
||||||
|
if (!sessionMatch) {
|
||||||
|
this.writeJson(response, 404, { error: "Not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionKey = decodeURIComponent(sessionMatch[1]);
|
||||||
|
const action = sessionMatch[2];
|
||||||
|
|
||||||
|
if (!action && method === "GET") {
|
||||||
|
const session = await this.ensureSession(sessionKey);
|
||||||
|
this.writeJson(response, 200, { session: this.createSnapshot(session) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "events" && method === "GET") {
|
||||||
|
await this.handleSse(sessionKey, request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "messages" && method === "POST") {
|
||||||
|
const body = await this.readJsonBody(request);
|
||||||
|
const text = typeof body.text === "string" ? body.text : "";
|
||||||
|
if (!text.trim()) {
|
||||||
|
this.writeJson(response, 400, { error: "Missing text" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await this.enqueueMessage({
|
||||||
|
sessionKey,
|
||||||
|
text,
|
||||||
|
source: "extension",
|
||||||
|
});
|
||||||
|
this.writeJson(response, result.ok ? 200 : 500, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "abort" && method === "POST") {
|
||||||
|
this.writeJson(response, 200, { ok: this.abortSession(sessionKey) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "reset" && method === "POST") {
|
||||||
|
await this.resetSession(sessionKey);
|
||||||
|
this.writeJson(response, 200, { ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writeJson(response, 405, { error: "Method not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleWebhookRequest(path: string, request: IncomingMessage, response: ServerResponse): Promise<void> {
|
||||||
|
const route = path.slice(this.config.webhook.basePath.length).replace(/^\/+/, "") || "default";
|
||||||
|
if (this.config.webhook.secret) {
|
||||||
|
const presentedSecret = request.headers["x-pi-webhook-secret"];
|
||||||
|
if (presentedSecret !== this.config.webhook.secret) {
|
||||||
|
this.writeJson(response, 401, { error: "Invalid webhook secret" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await this.readJsonBody(request);
|
||||||
|
const text = typeof body.text === "string" ? body.text : "";
|
||||||
|
if (!text.trim()) {
|
||||||
|
this.writeJson(response, 400, { error: "Missing text" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversationId =
|
||||||
|
typeof body.sessionKey === "string"
|
||||||
|
? body.sessionKey
|
||||||
|
: `webhook:${route}:${typeof body.sender === "string" ? body.sender : "default"}`;
|
||||||
|
const result = await this.enqueueMessage({
|
||||||
|
sessionKey: conversationId,
|
||||||
|
text,
|
||||||
|
source: "extension",
|
||||||
|
metadata: typeof body.metadata === "object" && body.metadata ? (body.metadata as Record<string, unknown>) : {},
|
||||||
|
});
|
||||||
|
this.writeJson(response, result.ok ? 200 : 500, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSse(sessionKey: string, request: IncomingMessage, response: ServerResponse): Promise<void> {
|
||||||
|
response.writeHead(200, {
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache, no-transform",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
});
|
||||||
|
response.write("\n");
|
||||||
|
|
||||||
|
const unsubscribe = await this.addSubscriber(sessionKey, (event) => {
|
||||||
|
response.write(`data: ${JSON.stringify(event)}\n\n`);
|
||||||
|
});
|
||||||
|
request.on("close", () => {
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private requireAuth(request: IncomingMessage, response: ServerResponse): void {
|
||||||
|
if (!this.config.bearerToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const header = request.headers.authorization;
|
||||||
|
if (header === `Bearer ${this.config.bearerToken}`) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.writeJson(response, 401, { error: "Unauthorized" });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readJsonBody(request: IncomingMessage): Promise<Record<string, unknown>> {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
for await (const chunk of request) {
|
||||||
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
||||||
|
}
|
||||||
|
if (chunks.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const body = Buffer.concat(chunks).toString("utf8");
|
||||||
|
return JSON.parse(body) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeJson(response: ServerResponse, statusCode: number, payload: unknown): void {
|
||||||
|
response.statusCode = statusCode;
|
||||||
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
||||||
|
response.end(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
getGatewaySessionDir(sessionKey: string): string {
|
||||||
|
return join(this.sessionDirRoot, sanitizeSessionKey(sessionKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMessageText(message: { content: unknown }): string {
|
||||||
|
if (!Array.isArray(message.content)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return message.content
|
||||||
|
.filter((part): part is { type: "text"; text: string } => {
|
||||||
|
return typeof part === "object" && part !== null && "type" in part && "text" in part && part.type === "text";
|
||||||
|
})
|
||||||
|
.map((part) => part.text)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastAssistantText(session: AgentSession): string {
|
||||||
|
for (let index = session.messages.length - 1; index >= 0; index--) {
|
||||||
|
const message = session.messages[index];
|
||||||
|
if (message.role === "assistant") {
|
||||||
|
return extractMessageText(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeSessionKey(sessionKey: string): string {
|
||||||
|
return sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGatewaySessionManager(cwd: string, sessionKey: string, sessionDirRoot: string): SessionManager {
|
||||||
|
return SessionManager.create(cwd, join(sessionDirRoot, sanitizeSessionKey(sessionKey)));
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,26 @@ export interface MarkdownSettings {
|
||||||
codeBlockIndent?: string; // default: " "
|
codeBlockIndent?: string; // default: " "
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GatewaySessionSettings {
|
||||||
|
idleMinutes?: number;
|
||||||
|
maxQueuePerSession?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GatewayWebhookSettings {
|
||||||
|
enabled?: boolean;
|
||||||
|
basePath?: string;
|
||||||
|
secret?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GatewaySettings {
|
||||||
|
enabled?: boolean;
|
||||||
|
bind?: string;
|
||||||
|
port?: number;
|
||||||
|
bearerToken?: string;
|
||||||
|
session?: GatewaySessionSettings;
|
||||||
|
webhook?: GatewayWebhookSettings;
|
||||||
|
}
|
||||||
|
|
||||||
export type TransportSetting = Transport;
|
export type TransportSetting = Transport;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -93,6 +113,7 @@ export interface Settings {
|
||||||
autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)
|
autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)
|
||||||
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
|
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
|
||||||
markdown?: MarkdownSettings;
|
markdown?: MarkdownSettings;
|
||||||
|
gateway?: GatewaySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
||||||
|
|
@ -912,4 +933,8 @@ export class SettingsManager {
|
||||||
getCodeBlockIndent(): string {
|
getCodeBlockIndent(): string {
|
||||||
return this.settings.markdown?.codeBlockIndent ?? " ";
|
return this.settings.markdown?.codeBlockIndent ?? " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGatewaySettings(): GatewaySettings {
|
||||||
|
return structuredClone(this.settings.gateway ?? {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,19 @@ export {
|
||||||
} from "./core/extensions/index.js";
|
} from "./core/extensions/index.js";
|
||||||
// Footer data provider (git branch + extension statuses - data not otherwise available to extensions)
|
// Footer data provider (git branch + extension statuses - data not otherwise available to extensions)
|
||||||
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js";
|
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js";
|
||||||
|
export {
|
||||||
|
createGatewaySessionManager,
|
||||||
|
type GatewayConfig,
|
||||||
|
type GatewayMessageRequest,
|
||||||
|
type GatewayMessageResult,
|
||||||
|
GatewayRuntime,
|
||||||
|
type GatewayRuntimeOptions,
|
||||||
|
type GatewaySessionFactory,
|
||||||
|
type GatewaySessionSnapshot,
|
||||||
|
getActiveGatewayRuntime,
|
||||||
|
sanitizeSessionKey,
|
||||||
|
setActiveGatewayRuntime,
|
||||||
|
} from "./core/gateway-runtime.js";
|
||||||
export { convertToLlm } from "./core/messages.js";
|
export { convertToLlm } from "./core/messages.js";
|
||||||
export { ModelRegistry } from "./core/model-registry.js";
|
export { ModelRegistry } from "./core/model-registry.js";
|
||||||
export type {
|
export type {
|
||||||
|
|
@ -198,6 +211,7 @@ export {
|
||||||
} from "./core/session-manager.js";
|
} from "./core/session-manager.js";
|
||||||
export {
|
export {
|
||||||
type CompactionSettings,
|
type CompactionSettings,
|
||||||
|
type GatewaySettings,
|
||||||
type ImageSettings,
|
type ImageSettings,
|
||||||
type PackageSource,
|
type PackageSource,
|
||||||
type RetrySettings,
|
type RetrySettings,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
* createAgentSession() options. The SDK does the heavy lifting.
|
* createAgentSession() options. The SDK does the heavy lifting.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { join } from "node:path";
|
||||||
import { type ImageContent, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
import { type ImageContent, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { createInterface } from "readline";
|
import { createInterface } from "readline";
|
||||||
|
|
@ -17,6 +18,7 @@ import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
|
||||||
import { AuthStorage } from "./core/auth-storage.js";
|
import { AuthStorage } from "./core/auth-storage.js";
|
||||||
import { exportFromFile } from "./core/export-html/index.js";
|
import { exportFromFile } from "./core/export-html/index.js";
|
||||||
import type { LoadExtensionsResult } from "./core/extensions/index.js";
|
import type { LoadExtensionsResult } from "./core/extensions/index.js";
|
||||||
|
import { createGatewaySessionManager } from "./core/gateway-runtime.js";
|
||||||
import { KeybindingsManager } from "./core/keybindings.js";
|
import { KeybindingsManager } from "./core/keybindings.js";
|
||||||
import { ModelRegistry } from "./core/model-registry.js";
|
import { ModelRegistry } from "./core/model-registry.js";
|
||||||
import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.js";
|
import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.js";
|
||||||
|
|
@ -81,9 +83,10 @@ interface PackageCommandOptions {
|
||||||
|
|
||||||
function printDaemonHelp(): void {
|
function printDaemonHelp(): void {
|
||||||
console.log(`${chalk.bold("Usage:")}
|
console.log(`${chalk.bold("Usage:")}
|
||||||
|
${APP_NAME} gateway [options] [messages...]
|
||||||
${APP_NAME} daemon [options] [messages...]
|
${APP_NAME} daemon [options] [messages...]
|
||||||
|
|
||||||
Run pi as a long-lived daemon (non-interactive) with extensions enabled.
|
Run pi as a long-lived gateway (non-interactive) with extensions enabled.
|
||||||
Messages passed as positional args are sent once at startup.
|
Messages passed as positional args are sent once at startup.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
@ -553,9 +556,9 @@ async function handleConfigCommand(args: string[]): Promise<boolean> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function main(args: string[]) {
|
export async function main(args: string[]) {
|
||||||
const isDaemonCommand = args[0] === "daemon";
|
const isGatewayCommand = args[0] === "daemon" || args[0] === "gateway";
|
||||||
const parsedArgs = isDaemonCommand ? args.slice(1) : args;
|
const parsedArgs = isGatewayCommand ? args.slice(1) : args;
|
||||||
const offlineMode = args.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE);
|
const offlineMode = parsedArgs.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE);
|
||||||
if (offlineMode) {
|
if (offlineMode) {
|
||||||
process.env.PI_OFFLINE = "1";
|
process.env.PI_OFFLINE = "1";
|
||||||
process.env.PI_SKIP_VERSION_CHECK = "1";
|
process.env.PI_SKIP_VERSION_CHECK = "1";
|
||||||
|
|
@ -634,7 +637,7 @@ export async function main(args: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.help) {
|
if (parsed.help) {
|
||||||
if (isDaemonCommand) {
|
if (isGatewayCommand) {
|
||||||
printDaemonHelp();
|
printDaemonHelp();
|
||||||
} else {
|
} else {
|
||||||
printHelp();
|
printHelp();
|
||||||
|
|
@ -648,13 +651,13 @@ export async function main(args: string[]) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDaemonCommand && parsed.mode === "rpc") {
|
if (isGatewayCommand && parsed.mode === "rpc") {
|
||||||
console.error(chalk.red("Cannot use --mode rpc with the daemon command."));
|
console.error(chalk.red("Cannot use --mode rpc with the gateway command."));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read piped stdin content (if any) - skip for daemon and RPC modes
|
// Read piped stdin content (if any) - skip for daemon and RPC modes
|
||||||
if (!isDaemonCommand && parsed.mode !== "rpc") {
|
if (!isGatewayCommand && parsed.mode !== "rpc") {
|
||||||
const stdinContent = await readPipedStdin();
|
const stdinContent = await readPipedStdin();
|
||||||
if (stdinContent !== undefined) {
|
if (stdinContent !== undefined) {
|
||||||
// Force print mode since interactive mode requires a TTY for keyboard input
|
// Force print mode since interactive mode requires a TTY for keyboard input
|
||||||
|
|
@ -684,7 +687,7 @@ export async function main(args: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
|
const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
|
||||||
const isInteractive = !isDaemonCommand && !parsed.print && parsed.mode === undefined;
|
const isInteractive = !isGatewayCommand && !parsed.print && parsed.mode === undefined;
|
||||||
const mode = parsed.mode || "text";
|
const mode = parsed.mode || "text";
|
||||||
initTheme(settingsManager.getTheme(), isInteractive);
|
initTheme(settingsManager.getTheme(), isInteractive);
|
||||||
|
|
||||||
|
|
@ -789,11 +792,44 @@ export async function main(args: string[]) {
|
||||||
verbose: parsed.verbose,
|
verbose: parsed.verbose,
|
||||||
});
|
});
|
||||||
await mode.run();
|
await mode.run();
|
||||||
} else if (isDaemonCommand) {
|
} else if (isGatewayCommand) {
|
||||||
|
const gatewayLoaderOptions = {
|
||||||
|
additionalExtensionPaths: firstPass.extensions,
|
||||||
|
additionalSkillPaths: firstPass.skills,
|
||||||
|
additionalPromptTemplatePaths: firstPass.promptTemplates,
|
||||||
|
additionalThemePaths: firstPass.themes,
|
||||||
|
noExtensions: firstPass.noExtensions,
|
||||||
|
noSkills: firstPass.noSkills,
|
||||||
|
noPromptTemplates: firstPass.noPromptTemplates,
|
||||||
|
noThemes: firstPass.noThemes,
|
||||||
|
systemPrompt: firstPass.systemPrompt,
|
||||||
|
appendSystemPrompt: firstPass.appendSystemPrompt,
|
||||||
|
};
|
||||||
|
const gatewaySessionRoot = join(agentDir, "gateway-sessions");
|
||||||
const daemonOptions: DaemonModeOptions = {
|
const daemonOptions: DaemonModeOptions = {
|
||||||
initialMessage,
|
initialMessage,
|
||||||
initialImages,
|
initialImages,
|
||||||
messages: parsed.messages,
|
messages: parsed.messages,
|
||||||
|
gateway: settingsManager.getGatewaySettings(),
|
||||||
|
createSession: async (sessionKey) => {
|
||||||
|
const gatewayResourceLoader = new DefaultResourceLoader({
|
||||||
|
cwd,
|
||||||
|
agentDir,
|
||||||
|
settingsManager,
|
||||||
|
...gatewayLoaderOptions,
|
||||||
|
});
|
||||||
|
await gatewayResourceLoader.reload();
|
||||||
|
const gatewaySessionOptions: CreateAgentSessionOptions = {
|
||||||
|
...sessionOptions,
|
||||||
|
authStorage,
|
||||||
|
modelRegistry,
|
||||||
|
settingsManager,
|
||||||
|
resourceLoader: gatewayResourceLoader,
|
||||||
|
sessionManager: createGatewaySessionManager(cwd, sessionKey, gatewaySessionRoot),
|
||||||
|
};
|
||||||
|
const { session: gatewaySession } = await createAgentSession(gatewaySessionOptions);
|
||||||
|
return gatewaySession;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
await runDaemonMode(session, daemonOptions);
|
await runDaemonMode(session, daemonOptions);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
import type { ImageContent } from "@mariozechner/pi-ai";
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
import type { AgentSession } from "../core/agent-session.js";
|
import type { AgentSession } from "../core/agent-session.js";
|
||||||
|
import { GatewayRuntime, type GatewaySessionFactory, setActiveGatewayRuntime } from "../core/gateway-runtime.js";
|
||||||
|
import type { GatewaySettings } from "../core/settings-manager.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for daemon mode.
|
* Options for daemon mode.
|
||||||
|
|
@ -19,6 +21,10 @@ export interface DaemonModeOptions {
|
||||||
initialImages?: ImageContent[];
|
initialImages?: ImageContent[];
|
||||||
/** Additional startup messages (sent after initialMessage, one by one). */
|
/** Additional startup messages (sent after initialMessage, one by one). */
|
||||||
messages?: string[];
|
messages?: string[];
|
||||||
|
/** Factory for creating additional gateway-owned sessions. */
|
||||||
|
createSession: GatewaySessionFactory;
|
||||||
|
/** Gateway config from settings/env. */
|
||||||
|
gateway: GatewaySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCommandContextActions(session: AgentSession) {
|
function createCommandContextActions(session: AgentSession) {
|
||||||
|
|
@ -71,12 +77,39 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
|
||||||
const ready = new Promise<void>((resolve) => {
|
const ready = new Promise<void>((resolve) => {
|
||||||
resolveReady = resolve;
|
resolveReady = resolve;
|
||||||
});
|
});
|
||||||
|
const gatewayBind = process.env.PI_GATEWAY_BIND ?? options.gateway.bind ?? "127.0.0.1";
|
||||||
|
const gatewayPort = Number.parseInt(process.env.PI_GATEWAY_PORT ?? "", 10) || options.gateway.port || 8787;
|
||||||
|
const gatewayToken = process.env.PI_GATEWAY_TOKEN ?? options.gateway.bearerToken;
|
||||||
|
const gateway = new GatewayRuntime({
|
||||||
|
config: {
|
||||||
|
bind: gatewayBind,
|
||||||
|
port: gatewayPort,
|
||||||
|
bearerToken: gatewayToken,
|
||||||
|
session: {
|
||||||
|
idleMinutes: options.gateway.session?.idleMinutes ?? 60,
|
||||||
|
maxQueuePerSession: options.gateway.session?.maxQueuePerSession ?? 8,
|
||||||
|
},
|
||||||
|
webhook: {
|
||||||
|
enabled: options.gateway.webhook?.enabled ?? true,
|
||||||
|
basePath: options.gateway.webhook?.basePath ?? "/webhooks",
|
||||||
|
secret: process.env.PI_GATEWAY_WEBHOOK_SECRET ?? options.gateway.webhook?.secret,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
primarySessionKey: "web:main",
|
||||||
|
primarySession: session,
|
||||||
|
createSession: options.createSession,
|
||||||
|
log: (message) => {
|
||||||
|
console.error(`[pi-gateway] ${message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const shutdown = async (reason: "signal" | "extension"): Promise<void> => {
|
const shutdown = async (reason: "signal" | "extension"): Promise<void> => {
|
||||||
if (isShuttingDown) return;
|
if (isShuttingDown) return;
|
||||||
isShuttingDown = true;
|
isShuttingDown = true;
|
||||||
|
|
||||||
console.error(`[co-mono-daemon] shutdown requested: ${reason}`);
|
console.error(`[pi-gateway] shutdown requested: ${reason}`);
|
||||||
|
setActiveGatewayRuntime(null);
|
||||||
|
await gateway.stop();
|
||||||
|
|
||||||
const runner = session.extensionRunner;
|
const runner = session.extensionRunner;
|
||||||
if (runner?.hasHandlers("session_shutdown")) {
|
if (runner?.hasHandlers("session_shutdown")) {
|
||||||
|
|
@ -90,7 +123,7 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
|
||||||
const handleShutdownSignal = (signal: NodeJS.Signals) => {
|
const handleShutdownSignal = (signal: NodeJS.Signals) => {
|
||||||
void shutdown("signal").catch((error) => {
|
void shutdown("signal").catch((error) => {
|
||||||
console.error(
|
console.error(
|
||||||
`[co-mono-daemon] shutdown failed for ${signal}: ${error instanceof Error ? error.message : String(error)}`,
|
`[pi-gateway] shutdown failed for ${signal}: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
@ -102,7 +135,7 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
|
||||||
process.once("SIGHUP", () => handleShutdownSignal("SIGHUP"));
|
process.once("SIGHUP", () => handleShutdownSignal("SIGHUP"));
|
||||||
|
|
||||||
process.on("unhandledRejection", (error) => {
|
process.on("unhandledRejection", (error) => {
|
||||||
console.error(`[co-mono-daemon] unhandled rejection: ${error instanceof Error ? error.message : String(error)}`);
|
console.error(`[pi-gateway] unhandled rejection: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
await session.bindExtensions({
|
await session.bindExtensions({
|
||||||
|
|
@ -110,7 +143,7 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
|
||||||
shutdownHandler: () => {
|
shutdownHandler: () => {
|
||||||
void shutdown("extension").catch((error) => {
|
void shutdown("extension").catch((error) => {
|
||||||
console.error(
|
console.error(
|
||||||
`[co-mono-daemon] extension shutdown failed: ${error instanceof Error ? error.message : String(error)}`,
|
`[pi-gateway] extension shutdown failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
@ -135,7 +168,11 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
|
||||||
await session.prompt(message);
|
await session.prompt(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`[co-mono-daemon] startup complete (session=${session.sessionId ?? "unknown"})`);
|
await gateway.start();
|
||||||
|
setActiveGatewayRuntime(gateway);
|
||||||
|
console.error(
|
||||||
|
`[pi-gateway] startup complete (session=${session.sessionId ?? "unknown"}, bind=${gatewayBind}, port=${gatewayPort})`,
|
||||||
|
);
|
||||||
|
|
||||||
// Keep process alive forever.
|
// Keep process alive forever.
|
||||||
const keepAlive = setInterval(() => {
|
const keepAlive = setInterval(() => {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "@e9n/pi-channels",
|
"name": "@e9n/pi-channels",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Two-way channel extension for pi — route messages between agents and Telegram, webhooks, and custom adapters",
|
"description": "Two-way channel extension for pi — route messages between agents and Telegram, webhooks, and custom adapters",
|
||||||
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"pi-package"
|
"pi-package"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@
|
||||||
|
|
||||||
import { SocketModeClient } from "@slack/socket-mode";
|
import { SocketModeClient } from "@slack/socket-mode";
|
||||||
import { WebClient } from "@slack/web-api";
|
import { WebClient } from "@slack/web-api";
|
||||||
import { getChannelSetting } from "../config.ts";
|
import { getChannelSetting } from "../config.js";
|
||||||
import type { AdapterConfig, ChannelAdapter, ChannelMessage, OnIncomingMessage } from "../types.ts";
|
import type { AdapterConfig, ChannelAdapter, ChannelMessage, OnIncomingMessage } from "../types.js";
|
||||||
|
|
||||||
const MAX_LENGTH = 3000; // Slack block text limit; actual API limit is 4000 but leave margin
|
const MAX_LENGTH = 3000; // Slack block text limit; actual API limit is 4000 but leave margin
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@ export function createSlackAdapter(config: AdapterConfig, cwd?: string, log?: Sl
|
||||||
return {
|
return {
|
||||||
direction: "bidirectional" as const,
|
direction: "bidirectional" as const,
|
||||||
|
|
||||||
async sendTyping(recipient: string): Promise<void> {
|
async sendTyping(_recipient: string): Promise<void> {
|
||||||
// Slack doesn't have a direct "typing" API for bots in channels.
|
// Slack doesn't have a direct "typing" API for bots in channels.
|
||||||
// We can use a reaction or simply no-op. For DMs, there's no API either.
|
// We can use a reaction or simply no-op. For DMs, there's no API either.
|
||||||
// Best we can do is nothing — Slack bots don't show typing indicators.
|
// Best we can do is nothing — Slack bots don't show typing indicators.
|
||||||
|
|
@ -309,7 +309,7 @@ export function createSlackAdapter(config: AdapterConfig, cwd?: string, log?: Sl
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Interactive payloads (future: button clicks, modals) ──
|
// ── Interactive payloads (future: button clicks, modals) ──
|
||||||
socketClient.on("interactive", async ({ body, ack }: { body: any; ack: () => Promise<void> }) => {
|
socketClient.on("interactive", async ({ body: _body, ack }: { body: any; ack: () => Promise<void> }) => {
|
||||||
try {
|
try {
|
||||||
await ack();
|
await ack();
|
||||||
// TODO: handle interactive payloads (block actions, modals)
|
// TODO: handle interactive payloads (block actions, modals)
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ import type {
|
||||||
IncomingMessage,
|
IncomingMessage,
|
||||||
OnIncomingMessage,
|
OnIncomingMessage,
|
||||||
TranscriptionConfig,
|
TranscriptionConfig,
|
||||||
} from "../types.ts";
|
} from "../types.js";
|
||||||
import { createTranscriptionProvider, type TranscriptionProvider } from "./transcription.ts";
|
import { createTranscriptionProvider, type TranscriptionProvider } from "./transcription.js";
|
||||||
|
|
||||||
const MAX_LENGTH = 4096;
|
const MAX_LENGTH = 4096;
|
||||||
const MAX_FILE_SIZE = 1_048_576; // 1MB
|
const MAX_FILE_SIZE = 1_048_576; // 1MB
|
||||||
|
|
@ -388,7 +388,6 @@ export function createTelegramAdapter(config: AdapterConfig): ChannelAdapter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = path.extname(filename || "").toLowerCase();
|
|
||||||
const attachment: IncomingAttachment = {
|
const attachment: IncomingAttachment = {
|
||||||
type: "image",
|
type: "image",
|
||||||
path: downloaded.localPath,
|
path: downloaded.localPath,
|
||||||
|
|
@ -472,7 +471,7 @@ export function createTelegramAdapter(config: AdapterConfig): ChannelAdapter {
|
||||||
return {
|
return {
|
||||||
adapter: "telegram",
|
adapter: "telegram",
|
||||||
sender: chatId,
|
sender: chatId,
|
||||||
text: `🎵 ${filename || "audio"} (transcription failed${result.error ? ": " + result.error : ""})`,
|
text: `🎵 ${filename || "audio"} (transcription failed${result.error ? `: ${result.error}` : ""})`,
|
||||||
metadata: { ...metadata, hasAudio: true },
|
metadata: { ...metadata, hasAudio: true },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -535,7 +534,7 @@ export function createTelegramAdapter(config: AdapterConfig): ChannelAdapter {
|
||||||
return {
|
return {
|
||||||
adapter: "telegram",
|
adapter: "telegram",
|
||||||
sender: chatId,
|
sender: chatId,
|
||||||
text: `🎤 (voice message — transcription failed${result.error ? ": " + result.error : ""})`,
|
text: `🎤 (voice message — transcription failed${result.error ? `: ${result.error}` : ""})`,
|
||||||
metadata: { ...metadata, hasVoice: true, voiceDuration: voice.duration },
|
metadata: { ...metadata, hasVoice: true, voiceDuration: voice.duration },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -588,7 +587,7 @@ export function createTelegramAdapter(config: AdapterConfig): ChannelAdapter {
|
||||||
return {
|
return {
|
||||||
adapter: "telegram",
|
adapter: "telegram",
|
||||||
sender: chatId,
|
sender: chatId,
|
||||||
text: `🎵 ${audioName} (transcription failed${result.error ? ": " + result.error : ""})`,
|
text: `🎵 ${audioName} (transcription failed${result.error ? `: ${result.error}` : ""})`,
|
||||||
metadata: { ...metadata, hasAudio: true, audioTitle: audio.title, audioDuration: audio.duration },
|
metadata: { ...metadata, hasAudio: true, audioTitle: audio.title, audioDuration: audio.duration },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import type { TranscriptionConfig } from "../types.ts";
|
import type { TranscriptionConfig } from "../types.js";
|
||||||
|
|
||||||
// ── Public interface ────────────────────────────────────────────
|
// ── Public interface ────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AdapterConfig, ChannelAdapter, ChannelMessage } from "../types.ts";
|
import type { AdapterConfig, ChannelAdapter, ChannelMessage } from "../types.js";
|
||||||
|
|
||||||
export function createWebhookAdapter(config: AdapterConfig): ChannelAdapter {
|
export function createWebhookAdapter(config: AdapterConfig): ChannelAdapter {
|
||||||
const method = (config.method as string) ?? "POST";
|
const method = (config.method as string) ?? "POST";
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,18 @@
|
||||||
* pi-channels — Chat bridge.
|
* pi-channels — Chat bridge.
|
||||||
*
|
*
|
||||||
* Listens for incoming messages (channel:receive), serializes per sender,
|
* Listens for incoming messages (channel:receive), serializes per sender,
|
||||||
* runs prompts via isolated subprocesses, and sends responses back via
|
* routes prompts into the live pi gateway runtime, and sends responses
|
||||||
* the same adapter. Each sender gets their own FIFO queue. Multiple
|
* back via the same adapter. Each sender gets their own FIFO queue.
|
||||||
* senders run concurrently up to maxConcurrent.
|
* Multiple senders run concurrently up to maxConcurrent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { EventBus } from "@mariozechner/pi-coding-agent";
|
import { readFileSync } from "node:fs";
|
||||||
import type { ChannelRegistry } from "../registry.ts";
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
import type { BridgeConfig, IncomingAttachment, IncomingMessage, QueuedPrompt, SenderSession } from "../types.ts";
|
import { type EventBus, getActiveGatewayRuntime } from "@mariozechner/pi-coding-agent";
|
||||||
import { type CommandContext, handleCommand, isCommand } from "./commands.ts";
|
import type { ChannelRegistry } from "../registry.js";
|
||||||
import { RpcSessionManager } from "./rpc-runner.ts";
|
import type { BridgeConfig, IncomingMessage, QueuedPrompt, SenderSession } from "../types.js";
|
||||||
import { runPrompt } from "./runner.ts";
|
import { type CommandContext, handleCommand, isCommand } from "./commands.js";
|
||||||
import { startTyping } from "./typing.ts";
|
import { startTyping } from "./typing.js";
|
||||||
|
|
||||||
const BRIDGE_DEFAULTS: Required<BridgeConfig> = {
|
const BRIDGE_DEFAULTS: Required<BridgeConfig> = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
@ -38,24 +38,21 @@ function nextId(): string {
|
||||||
|
|
||||||
export class ChatBridge {
|
export class ChatBridge {
|
||||||
private config: Required<BridgeConfig>;
|
private config: Required<BridgeConfig>;
|
||||||
private cwd: string;
|
|
||||||
private registry: ChannelRegistry;
|
private registry: ChannelRegistry;
|
||||||
private events: EventBus;
|
private events: EventBus;
|
||||||
private log: LogFn;
|
private log: LogFn;
|
||||||
private sessions = new Map<string, SenderSession>();
|
private sessions = new Map<string, SenderSession>();
|
||||||
private activeCount = 0;
|
private activeCount = 0;
|
||||||
private running = false;
|
private running = false;
|
||||||
private rpcManager: RpcSessionManager | null = null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
bridgeConfig: BridgeConfig | undefined,
|
bridgeConfig: BridgeConfig | undefined,
|
||||||
cwd: string,
|
_cwd: string,
|
||||||
registry: ChannelRegistry,
|
registry: ChannelRegistry,
|
||||||
events: EventBus,
|
events: EventBus,
|
||||||
log: LogFn = () => {},
|
log: LogFn = () => {},
|
||||||
) {
|
) {
|
||||||
this.config = { ...BRIDGE_DEFAULTS, ...bridgeConfig };
|
this.config = { ...BRIDGE_DEFAULTS, ...bridgeConfig };
|
||||||
this.cwd = cwd;
|
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
this.events = events;
|
this.events = events;
|
||||||
this.log = log;
|
this.log = log;
|
||||||
|
|
@ -65,18 +62,11 @@ export class ChatBridge {
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
if (this.running) return;
|
if (this.running) return;
|
||||||
|
if (!getActiveGatewayRuntime()) {
|
||||||
|
this.log("bridge-unavailable", { reason: "no active pi gateway runtime" }, "WARN");
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.running = true;
|
this.running = true;
|
||||||
|
|
||||||
// Always create the RPC manager — it's used on-demand for persistent senders
|
|
||||||
this.rpcManager = new RpcSessionManager(
|
|
||||||
{
|
|
||||||
cwd: this.cwd,
|
|
||||||
model: this.config.model,
|
|
||||||
timeoutMs: this.config.timeoutMs,
|
|
||||||
extensions: this.config.extensions,
|
|
||||||
},
|
|
||||||
this.config.idleTimeoutMinutes * 60_000,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
|
@ -86,8 +76,6 @@ export class ChatBridge {
|
||||||
}
|
}
|
||||||
this.sessions.clear();
|
this.sessions.clear();
|
||||||
this.activeCount = 0;
|
this.activeCount = 0;
|
||||||
this.rpcManager?.killAll();
|
|
||||||
this.rpcManager = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive(): boolean {
|
isActive(): boolean {
|
||||||
|
|
@ -180,38 +168,32 @@ export class ChatBridge {
|
||||||
// Typing indicator
|
// Typing indicator
|
||||||
const adapter = this.registry.getAdapter(prompt.adapter);
|
const adapter = this.registry.getAdapter(prompt.adapter);
|
||||||
const typing = this.config.typingIndicators ? startTyping(adapter, prompt.sender) : { stop() {} };
|
const typing = this.config.typingIndicators ? startTyping(adapter, prompt.sender) : { stop() {} };
|
||||||
|
const gateway = getActiveGatewayRuntime();
|
||||||
const ac = new AbortController();
|
if (!gateway) {
|
||||||
session.abortController = ac;
|
typing.stop();
|
||||||
|
session.processing = false;
|
||||||
const usePersistent = this.shouldUsePersistent(senderKey);
|
this.activeCount--;
|
||||||
|
this.sendReply(prompt.adapter, prompt.sender, "❌ pi gateway is not running.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.events.emit("bridge:start", {
|
this.events.emit("bridge:start", {
|
||||||
id: prompt.id,
|
id: prompt.id,
|
||||||
adapter: prompt.adapter,
|
adapter: prompt.adapter,
|
||||||
sender: prompt.sender,
|
sender: prompt.sender,
|
||||||
text: prompt.text.slice(0, 100),
|
text: prompt.text.slice(0, 100),
|
||||||
persistent: usePersistent,
|
persistent: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result;
|
session.abortController = new AbortController();
|
||||||
|
const result = await gateway.enqueueMessage({
|
||||||
if (usePersistent && this.rpcManager) {
|
sessionKey: senderKey,
|
||||||
// Persistent mode: use RPC session
|
text: buildPromptText(prompt),
|
||||||
result = await this.runWithRpc(senderKey, prompt, ac.signal);
|
images: collectImageAttachments(prompt.attachments),
|
||||||
} else {
|
source: "extension",
|
||||||
// Stateless mode: spawn subprocess
|
metadata: prompt.metadata,
|
||||||
result = await runPrompt({
|
});
|
||||||
prompt: prompt.text,
|
|
||||||
cwd: this.cwd,
|
|
||||||
timeoutMs: this.config.timeoutMs,
|
|
||||||
model: this.config.model,
|
|
||||||
signal: ac.signal,
|
|
||||||
attachments: prompt.attachments,
|
|
||||||
extensions: this.config.extensions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
typing.stop();
|
typing.stop();
|
||||||
|
|
||||||
|
|
@ -229,8 +211,7 @@ export class ChatBridge {
|
||||||
adapter: prompt.adapter,
|
adapter: prompt.adapter,
|
||||||
sender: prompt.sender,
|
sender: prompt.sender,
|
||||||
ok: result.ok,
|
ok: result.ok,
|
||||||
durationMs: result.durationMs,
|
persistent: true,
|
||||||
persistent: usePersistent,
|
|
||||||
});
|
});
|
||||||
this.log(
|
this.log(
|
||||||
"bridge-complete",
|
"bridge-complete",
|
||||||
|
|
@ -238,15 +219,15 @@ export class ChatBridge {
|
||||||
id: prompt.id,
|
id: prompt.id,
|
||||||
adapter: prompt.adapter,
|
adapter: prompt.adapter,
|
||||||
ok: result.ok,
|
ok: result.ok,
|
||||||
durationMs: result.durationMs,
|
persistent: true,
|
||||||
persistent: usePersistent,
|
|
||||||
},
|
},
|
||||||
result.ok ? "INFO" : "WARN",
|
result.ok ? "INFO" : "WARN",
|
||||||
);
|
);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
typing.stop();
|
typing.stop();
|
||||||
this.log("bridge-error", { adapter: prompt.adapter, sender: prompt.sender, error: err.message }, "ERROR");
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
this.sendReply(prompt.adapter, prompt.sender, `❌ Unexpected error: ${err.message}`);
|
this.log("bridge-error", { adapter: prompt.adapter, sender: prompt.sender, error: message }, "ERROR");
|
||||||
|
this.sendReply(prompt.adapter, prompt.sender, `❌ Unexpected error: ${message}`);
|
||||||
} finally {
|
} finally {
|
||||||
session.abortController = null;
|
session.abortController = null;
|
||||||
session.processing = false;
|
session.processing = false;
|
||||||
|
|
@ -257,29 +238,6 @@ export class ChatBridge {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run a prompt via persistent RPC session. */
|
|
||||||
private async runWithRpc(
|
|
||||||
senderKey: string,
|
|
||||||
prompt: QueuedPrompt,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<import("../types.ts").RunResult> {
|
|
||||||
try {
|
|
||||||
const rpcSession = await this.rpcManager!.getSession(senderKey);
|
|
||||||
return await rpcSession.runPrompt(prompt.text, {
|
|
||||||
signal,
|
|
||||||
attachments: prompt.attachments,
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
response: "",
|
|
||||||
error: err.message,
|
|
||||||
durationMs: 0,
|
|
||||||
exitCode: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** After a slot frees up, check other senders waiting for concurrency. */
|
/** After a slot frees up, check other senders waiting for concurrency. */
|
||||||
private drainWaiting(): void {
|
private drainWaiting(): void {
|
||||||
if (this.activeCount >= this.config.maxConcurrent) return;
|
if (this.activeCount >= this.config.maxConcurrent) return;
|
||||||
|
|
@ -327,37 +285,17 @@ export class ChatBridge {
|
||||||
return this.sessions;
|
return this.sessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Session mode resolution ───────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a sender should use persistent (RPC) or stateless mode.
|
|
||||||
* Checks sessionRules first (first match wins), falls back to sessionMode default.
|
|
||||||
*/
|
|
||||||
private shouldUsePersistent(senderKey: string): boolean {
|
|
||||||
for (const rule of this.config.sessionRules) {
|
|
||||||
if (globMatch(rule.match, senderKey)) {
|
|
||||||
return rule.mode === "persistent";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.config.sessionMode === "persistent";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Command context ───────────────────────────────────────
|
// ── Command context ───────────────────────────────────────
|
||||||
|
|
||||||
private commandContext(): CommandContext {
|
private commandContext(): CommandContext {
|
||||||
|
const gateway = getActiveGatewayRuntime();
|
||||||
return {
|
return {
|
||||||
isPersistent: (sender: string) => {
|
isPersistent: () => true,
|
||||||
// Find the sender key to check mode
|
|
||||||
for (const [key, session] of this.sessions) {
|
|
||||||
if (session.sender === sender) return this.shouldUsePersistent(key);
|
|
||||||
}
|
|
||||||
return this.config.sessionMode === "persistent";
|
|
||||||
},
|
|
||||||
abortCurrent: (sender: string): boolean => {
|
abortCurrent: (sender: string): boolean => {
|
||||||
for (const session of this.sessions.values()) {
|
if (!gateway) return false;
|
||||||
|
for (const [key, session] of this.sessions) {
|
||||||
if (session.sender === sender && session.abortController) {
|
if (session.sender === sender && session.abortController) {
|
||||||
session.abortController.abort();
|
return gateway.abortSession(key);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -368,13 +306,11 @@ export class ChatBridge {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetSession: (sender: string): void => {
|
resetSession: (sender: string): void => {
|
||||||
|
if (!gateway) return;
|
||||||
for (const [key, session] of this.sessions) {
|
for (const [key, session] of this.sessions) {
|
||||||
if (session.sender === sender) {
|
if (session.sender === sender) {
|
||||||
this.sessions.delete(key);
|
this.sessions.delete(key);
|
||||||
// Also reset persistent RPC session
|
void gateway.resetSession(key);
|
||||||
if (this.rpcManager) {
|
|
||||||
this.rpcManager.resetSession(key).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -388,21 +324,6 @@ export class ChatBridge {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple glob matcher supporting `*` (any chars) and `?` (single char).
|
|
||||||
* Used for sessionRules pattern matching against "adapter:senderId" keys.
|
|
||||||
*/
|
|
||||||
function globMatch(pattern: string, text: string): boolean {
|
|
||||||
// Escape regex special chars except * and ?
|
|
||||||
const re = pattern
|
|
||||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
||||||
.replace(/\*/g, ".*")
|
|
||||||
.replace(/\?/g, ".");
|
|
||||||
return new RegExp(`^${re}$`).test(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_ERROR_LENGTH = 200;
|
const MAX_ERROR_LENGTH = 200;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -428,5 +349,36 @@ function sanitizeError(error: string | undefined): string {
|
||||||
|
|
||||||
const msg = meaningful?.trim() || "Something went wrong. Please try again.";
|
const msg = meaningful?.trim() || "Something went wrong. Please try again.";
|
||||||
|
|
||||||
return msg.length > MAX_ERROR_LENGTH ? msg.slice(0, MAX_ERROR_LENGTH) + "…" : msg;
|
return msg.length > MAX_ERROR_LENGTH ? `${msg.slice(0, MAX_ERROR_LENGTH)}…` : msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectImageAttachments(attachments: QueuedPrompt["attachments"]): ImageContent[] | undefined {
|
||||||
|
if (!attachments || attachments.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const images = attachments
|
||||||
|
.filter((attachment) => attachment.type === "image")
|
||||||
|
.map((attachment) => ({
|
||||||
|
type: "image" as const,
|
||||||
|
data: readFileSync(attachment.path).toString("base64"),
|
||||||
|
mimeType: attachment.mimeType || "image/jpeg",
|
||||||
|
}));
|
||||||
|
return images.length > 0 ? images : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPromptText(prompt: QueuedPrompt): string {
|
||||||
|
if (!prompt.attachments || prompt.attachments.length === 0) {
|
||||||
|
return prompt.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachmentNotes = prompt.attachments
|
||||||
|
.filter((attachment) => attachment.type !== "image")
|
||||||
|
.map((attachment) => {
|
||||||
|
const label = attachment.filename ?? attachment.path;
|
||||||
|
return `Attachment (${attachment.type}): ${label}`;
|
||||||
|
});
|
||||||
|
if (attachmentNotes.length === 0) {
|
||||||
|
return prompt.text;
|
||||||
|
}
|
||||||
|
return `${prompt.text}\n\n${attachmentNotes.join("\n")}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* Built-in: /start, /help, /abort, /status, /new
|
* Built-in: /start, /help, /abort, /status, /new
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SenderSession } from "../types.ts";
|
import type { SenderSession } from "../types.js";
|
||||||
|
|
||||||
export interface BotCommand {
|
export interface BotCommand {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import { type ChildProcess, spawn } from "node:child_process";
|
import { type ChildProcess, spawn } from "node:child_process";
|
||||||
import * as readline from "node:readline";
|
import * as readline from "node:readline";
|
||||||
import type { IncomingAttachment, RunResult } from "../types.ts";
|
import type { IncomingAttachment, RunResult } from "../types.js";
|
||||||
|
|
||||||
export interface RpcRunnerOptions {
|
export interface RpcRunnerOptions {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
|
@ -118,95 +118,97 @@ export class RpcSession {
|
||||||
onStreaming?: (text: string) => void;
|
onStreaming?: (text: string) => void;
|
||||||
},
|
},
|
||||||
): Promise<RunResult> {
|
): Promise<RunResult> {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Ensure subprocess is running
|
void (async () => {
|
||||||
if (!this.ready) {
|
// Ensure subprocess is running
|
||||||
const ok = await this.start();
|
if (!this.ready) {
|
||||||
if (!ok) {
|
const ok = await this.start();
|
||||||
resolve({
|
if (!ok) {
|
||||||
ok: false,
|
resolve({
|
||||||
response: "",
|
ok: false,
|
||||||
error: "Failed to start RPC session",
|
response: "",
|
||||||
durationMs: 0,
|
error: "Failed to start RPC session",
|
||||||
exitCode: 1,
|
durationMs: 0,
|
||||||
});
|
exitCode: 1,
|
||||||
return;
|
});
|
||||||
}
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
this._onStreaming = options?.onStreaming ?? null;
|
|
||||||
|
|
||||||
// Timeout
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
if (this.pending) {
|
|
||||||
const p = this.pending;
|
|
||||||
this.pending = null;
|
|
||||||
const text = p.textChunks.join("");
|
|
||||||
p.resolve({
|
|
||||||
ok: false,
|
|
||||||
response: text || "(timed out)",
|
|
||||||
error: "Timeout",
|
|
||||||
durationMs: Date.now() - p.startTime,
|
|
||||||
exitCode: 124,
|
|
||||||
});
|
|
||||||
// Kill and restart on next message
|
|
||||||
this.cleanup();
|
|
||||||
}
|
|
||||||
}, this.options.timeoutMs);
|
|
||||||
|
|
||||||
this.pending = { resolve, startTime, timer, textChunks: [] };
|
|
||||||
|
|
||||||
// Abort handler
|
|
||||||
const onAbort = () => {
|
|
||||||
this.sendCommand({ type: "abort" });
|
|
||||||
};
|
|
||||||
if (options?.signal) {
|
|
||||||
if (options.signal.aborted) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
this.pending = null;
|
|
||||||
this.sendCommand({ type: "abort" });
|
|
||||||
resolve({
|
|
||||||
ok: false,
|
|
||||||
response: "(aborted)",
|
|
||||||
error: "Aborted by user",
|
|
||||||
durationMs: Date.now() - startTime,
|
|
||||||
exitCode: 130,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
||||||
this.pending.abortHandler = () => options.signal?.removeEventListener("abort", onAbort);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build prompt command
|
|
||||||
const cmd: Record<string, unknown> = {
|
|
||||||
type: "prompt",
|
|
||||||
message: prompt,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attach images as base64
|
|
||||||
if (options?.attachments?.length) {
|
|
||||||
const images: Array<Record<string, string>> = [];
|
|
||||||
for (const att of options.attachments) {
|
|
||||||
if (att.type === "image") {
|
|
||||||
try {
|
|
||||||
const fs = await import("node:fs");
|
|
||||||
const data = fs.readFileSync(att.path).toString("base64");
|
|
||||||
images.push({
|
|
||||||
type: "image",
|
|
||||||
data,
|
|
||||||
mimeType: att.mimeType || "image/jpeg",
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Skip unreadable attachments
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (images.length > 0) cmd.images = images;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendCommand(cmd);
|
const startTime = Date.now();
|
||||||
|
this._onStreaming = options?.onStreaming ?? null;
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (this.pending) {
|
||||||
|
const p = this.pending;
|
||||||
|
this.pending = null;
|
||||||
|
const text = p.textChunks.join("");
|
||||||
|
p.resolve({
|
||||||
|
ok: false,
|
||||||
|
response: text || "(timed out)",
|
||||||
|
error: "Timeout",
|
||||||
|
durationMs: Date.now() - p.startTime,
|
||||||
|
exitCode: 124,
|
||||||
|
});
|
||||||
|
// Kill and restart on next message
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
}, this.options.timeoutMs);
|
||||||
|
|
||||||
|
this.pending = { resolve, startTime, timer, textChunks: [] };
|
||||||
|
|
||||||
|
// Abort handler
|
||||||
|
const onAbort = () => {
|
||||||
|
this.sendCommand({ type: "abort" });
|
||||||
|
};
|
||||||
|
if (options?.signal) {
|
||||||
|
if (options.signal.aborted) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
this.pending = null;
|
||||||
|
this.sendCommand({ type: "abort" });
|
||||||
|
resolve({
|
||||||
|
ok: false,
|
||||||
|
response: "(aborted)",
|
||||||
|
error: "Aborted by user",
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
exitCode: 130,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
this.pending.abortHandler = () => options.signal?.removeEventListener("abort", onAbort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build prompt command
|
||||||
|
const cmd: Record<string, unknown> = {
|
||||||
|
type: "prompt",
|
||||||
|
message: prompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach images as base64
|
||||||
|
if (options?.attachments?.length) {
|
||||||
|
const images: Array<Record<string, string>> = [];
|
||||||
|
for (const att of options.attachments) {
|
||||||
|
if (att.type === "image") {
|
||||||
|
try {
|
||||||
|
const fs = await import("node:fs");
|
||||||
|
const data = fs.readFileSync(att.path).toString("base64");
|
||||||
|
images.push({
|
||||||
|
type: "image",
|
||||||
|
data,
|
||||||
|
mimeType: att.mimeType || "image/jpeg",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable attachments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (images.length > 0) cmd.images = images;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendCommand(cmd);
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,7 +255,7 @@ export class RpcSession {
|
||||||
|
|
||||||
private sendCommand(cmd: Record<string, unknown>): void {
|
private sendCommand(cmd: Record<string, unknown>): void {
|
||||||
if (!this.child?.stdin?.writable) return;
|
if (!this.child?.stdin?.writable) return;
|
||||||
this.child.stdin.write(JSON.stringify(cmd) + "\n");
|
this.child.stdin.write(`${JSON.stringify(cmd)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleLine(line: string): void {
|
private handleLine(line: string): void {
|
||||||
|
|
@ -358,7 +360,7 @@ export class RpcSessionManager {
|
||||||
/** Get or create a session for a sender. */
|
/** Get or create a session for a sender. */
|
||||||
async getSession(senderKey: string): Promise<RpcSession> {
|
async getSession(senderKey: string): Promise<RpcSession> {
|
||||||
let session = this.sessions.get(senderKey);
|
let session = this.sessions.get(senderKey);
|
||||||
if (session && session.isAlive()) {
|
if (session?.isAlive()) {
|
||||||
this.resetIdleTimer(senderKey);
|
this.resetIdleTimer(senderKey);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +405,7 @@ export class RpcSessionManager {
|
||||||
|
|
||||||
/** Kill all sessions. */
|
/** Kill all sessions. */
|
||||||
killAll(): void {
|
killAll(): void {
|
||||||
for (const [key, session] of this.sessions) {
|
for (const session of this.sessions.values()) {
|
||||||
session.cleanup();
|
session.cleanup();
|
||||||
}
|
}
|
||||||
this.sessions.clear();
|
this.sessions.clear();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ChildProcess, spawn } from "node:child_process";
|
import { type ChildProcess, spawn } from "node:child_process";
|
||||||
import type { IncomingAttachment, RunResult } from "../types.ts";
|
import type { IncomingAttachment, RunResult } from "../types.js";
|
||||||
|
|
||||||
export interface RunOptions {
|
export interface RunOptions {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* For adapters without sendTyping, this is a no-op.
|
* For adapters without sendTyping, this is a no-op.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ChannelAdapter } from "../types.ts";
|
import type { ChannelAdapter } from "../types.js";
|
||||||
|
|
||||||
const TYPING_INTERVAL_MS = 4_000;
|
const TYPING_INTERVAL_MS = 4_000;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getAgentDir, SettingsManager } from "@mariozechner/pi-coding-agent";
|
import { getAgentDir, SettingsManager } from "@mariozechner/pi-coding-agent";
|
||||||
import type { ChannelConfig } from "./types.ts";
|
import type { ChannelConfig } from "./types.js";
|
||||||
|
|
||||||
const SETTINGS_KEY = "pi-channels";
|
const SETTINGS_KEY = "pi-channels";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
import type { ChatBridge } from "./bridge/bridge.ts";
|
import type { ChatBridge } from "./bridge/bridge.js";
|
||||||
import type { ChannelRegistry } from "./registry.ts";
|
import type { ChannelRegistry } from "./registry.js";
|
||||||
import type { ChannelAdapter, ChannelMessage, IncomingMessage } from "./types.ts";
|
import type { ChannelAdapter, ChannelMessage, IncomingMessage } from "./types.js";
|
||||||
|
|
||||||
/** Reference to the active bridge, set by index.ts after construction. */
|
/** Reference to the active bridge, set by index.ts after construction. */
|
||||||
let activeBridge: ChatBridge | null = null;
|
let activeBridge: ChatBridge | null = null;
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
import { ChatBridge } from "./bridge/bridge.ts";
|
import { ChatBridge } from "./bridge/bridge.js";
|
||||||
import { loadConfig } from "./config.ts";
|
import { loadConfig } from "./config.js";
|
||||||
import { registerChannelEvents, setBridge } from "./events.ts";
|
import { registerChannelEvents, setBridge } from "./events.js";
|
||||||
import { createLogger } from "./logger.ts";
|
import { createLogger } from "./logger.js";
|
||||||
import { ChannelRegistry } from "./registry.ts";
|
import { ChannelRegistry } from "./registry.js";
|
||||||
import { registerChannelTool } from "./tool.ts";
|
import { registerChannelTool } from "./tool.js";
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
const log = createLogger(pi);
|
const log = createLogger(pi);
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
* pi-channels — Adapter registry + route resolution.
|
* pi-channels — Adapter registry + route resolution.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSlackAdapter } from "./adapters/slack.ts";
|
import { createSlackAdapter } from "./adapters/slack.js";
|
||||||
import { createTelegramAdapter } from "./adapters/telegram.ts";
|
import { createTelegramAdapter } from "./adapters/telegram.js";
|
||||||
import { createWebhookAdapter } from "./adapters/webhook.ts";
|
import { createWebhookAdapter } from "./adapters/webhook.js";
|
||||||
import type {
|
import type {
|
||||||
AdapterConfig,
|
AdapterConfig,
|
||||||
AdapterDirection,
|
AdapterDirection,
|
||||||
|
|
@ -13,7 +13,7 @@ import type {
|
||||||
ChannelMessage,
|
ChannelMessage,
|
||||||
IncomingMessage,
|
IncomingMessage,
|
||||||
OnIncomingMessage,
|
OnIncomingMessage,
|
||||||
} from "./types.ts";
|
} from "./types.js";
|
||||||
|
|
||||||
// ── Built-in adapter factories ──────────────────────────────────
|
// ── Built-in adapter factories ──────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import { StringEnum } from "@mariozechner/pi-ai";
|
import { StringEnum } from "@mariozechner/pi-ai";
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import type { ChannelRegistry } from "./registry.ts";
|
import type { ChannelRegistry } from "./registry.js";
|
||||||
|
|
||||||
interface ChannelToolParams {
|
interface ChannelToolParams {
|
||||||
action: "send" | "list" | "test";
|
action: "send" | "list" | "test";
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { execCommand, type SpawnOptions, type TerminalAdapter } from "../utils/terminal-adapter";
|
import type { SpawnOptions, TerminalAdapter } from "../utils/terminal-adapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context needed for iTerm2 spawning (tracks last pane for layout)
|
* Context needed for iTerm2 spawning (tracks last pane for layout)
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ describe("WezTermAdapter", () => {
|
||||||
describe("spawn", () => {
|
describe("spawn", () => {
|
||||||
it("should spawn first pane to the right with 50%", () => {
|
it("should spawn first pane to the right with 50%", () => {
|
||||||
// Mock getPanes finding only current pane
|
// Mock getPanes finding only current pane
|
||||||
mockExecCommand.mockImplementation((bin, args) => {
|
mockExecCommand.mockImplementation((_bin: string, args: string[]) => {
|
||||||
if (args.includes("list")) {
|
if (args.includes("list")) {
|
||||||
return {
|
return {
|
||||||
stdout: JSON.stringify([{ pane_id: 0, tab_id: 0 }]),
|
stdout: JSON.stringify([{ pane_id: 0, tab_id: 0 }]),
|
||||||
|
|
@ -69,7 +69,7 @@ describe("WezTermAdapter", () => {
|
||||||
|
|
||||||
it("should spawn subsequent panes by splitting the sidebar", () => {
|
it("should spawn subsequent panes by splitting the sidebar", () => {
|
||||||
// Mock getPanes finding current pane (0) and sidebar pane (1)
|
// Mock getPanes finding current pane (0) and sidebar pane (1)
|
||||||
mockExecCommand.mockImplementation((bin, args) => {
|
mockExecCommand.mockImplementation((_bin: string, args: string[]) => {
|
||||||
if (args.includes("list")) {
|
if (args.includes("list")) {
|
||||||
return {
|
return {
|
||||||
stdout: JSON.stringify([
|
stdout: JSON.stringify([
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { withLock } from "./lock";
|
import { withLock } from "./lock";
|
||||||
|
|
||||||
describe("withLock race conditions", () => {
|
describe("withLock race conditions", () => {
|
||||||
const testDir = path.join(os.tmpdir(), "pi-lock-race-test-" + Date.now());
|
const testDir = path.join(os.tmpdir(), `pi-lock-race-test-${Date.now()}`);
|
||||||
const lockPath = path.join(testDir, "test");
|
const lockPath = path.join(testDir, "test");
|
||||||
const lockFile = `${lockPath}.lock`;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
if (!fs.existsSync(testDir)) fs.mkdirSync(testDir, { recursive: true });
|
if (!fs.existsSync(testDir)) fs.mkdirSync(testDir, { recursive: true });
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { withLock } from "./lock";
|
import { withLock } from "./lock";
|
||||||
|
|
||||||
describe("withLock", () => {
|
describe("withLock", () => {
|
||||||
const testDir = path.join(os.tmpdir(), "pi-lock-test-" + Date.now());
|
const testDir = path.join(os.tmpdir(), `pi-lock-test-${Date.now()}`);
|
||||||
const lockPath = path.join(testDir, "test");
|
const lockPath = path.join(testDir, "test");
|
||||||
const lockFile = `${lockPath}.lock`;
|
const lockFile = `${lockPath}.lock`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// Project: pi-teams
|
// Project: pi-teams
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const LOCK_TIMEOUT = 5000; // 5 seconds of retrying
|
|
||||||
const STALE_LOCK_TIMEOUT = 30000; // 30 seconds for a lock to be considered stale
|
const STALE_LOCK_TIMEOUT = 30000; // 30 seconds for a lock to be considered stale
|
||||||
|
|
||||||
export async function withLock<T>(lockPath: string, fn: () => Promise<T>, retries: number = 50): Promise<T> {
|
export async function withLock<T>(lockPath: string, fn: () => Promise<T>, retries: number = 50): Promise<T> {
|
||||||
|
|
@ -18,7 +16,7 @@ export async function withLock<T>(lockPath: string, fn: () => Promise<T>, retrie
|
||||||
// Attempt to remove stale lock
|
// Attempt to remove stale lock
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(lockFile);
|
fs.unlinkSync(lockFile);
|
||||||
} catch (e) {
|
} catch (_error) {
|
||||||
// ignore, another process might have already removed it
|
// ignore, another process might have already removed it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +24,7 @@ export async function withLock<T>(lockPath: string, fn: () => Promise<T>, retrie
|
||||||
|
|
||||||
fs.writeFileSync(lockFile, process.pid.toString(), { flag: "wx" });
|
fs.writeFileSync(lockFile, process.pid.toString(), { flag: "wx" });
|
||||||
break;
|
break;
|
||||||
} catch (e) {
|
} catch (_error) {
|
||||||
retries--;
|
retries--;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +39,7 @@ export async function withLock<T>(lockPath: string, fn: () => Promise<T>, retrie
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(lockFile);
|
fs.unlinkSync(lockFile);
|
||||||
} catch (e) {
|
} catch (_error) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { appendMessage, broadcastMessage, readInbox, sendPlainMessage } from "./
|
||||||
import * as paths from "./paths";
|
import * as paths from "./paths";
|
||||||
|
|
||||||
// Mock the paths to use a temporary directory
|
// Mock the paths to use a temporary directory
|
||||||
const testDir = path.join(os.tmpdir(), "pi-teams-test-" + Date.now());
|
const testDir = path.join(os.tmpdir(), `pi-teams-test-${Date.now()}`);
|
||||||
|
|
||||||
describe("Messaging Utilities", () => {
|
describe("Messaging Utilities", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -14,11 +14,11 @@ describe("Messaging Utilities", () => {
|
||||||
fs.mkdirSync(testDir, { recursive: true });
|
fs.mkdirSync(testDir, { recursive: true });
|
||||||
|
|
||||||
// Override paths to use testDir
|
// Override paths to use testDir
|
||||||
vi.spyOn(paths, "inboxPath").mockImplementation((teamName, agentName) => {
|
vi.spyOn(paths, "inboxPath").mockImplementation((_teamName, agentName) => {
|
||||||
return path.join(testDir, "inboxes", `${agentName}.json`);
|
return path.join(testDir, "inboxes", `${agentName}.json`);
|
||||||
});
|
});
|
||||||
vi.spyOn(paths, "teamDir").mockReturnValue(testDir);
|
vi.spyOn(paths, "teamDir").mockReturnValue(testDir);
|
||||||
vi.spyOn(paths, "configPath").mockImplementation((teamName) => {
|
vi.spyOn(paths, "configPath").mockImplementation((_teamName) => {
|
||||||
return path.join(testDir, "config.json");
|
return path.join(testDir, "config.json");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,8 @@ export async function broadcastMessage(
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
console.error(`Broadcast partially failed: ${failures.length} messages could not be delivered.`);
|
console.error(`Broadcast partially failed: ${failures.length} messages could not be delivered.`);
|
||||||
// Optionally log individual errors
|
// Optionally log individual errors
|
||||||
failures.forEach((f) => console.error(`- Delivery error:`, f.reason));
|
for (const failure of failures) {
|
||||||
|
console.error("- Delivery error:", failure.reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import fs from "node:fs";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { inboxPath, sanitizeName, teamDir } from "./paths";
|
import { inboxPath, sanitizeName, teamDir } from "./paths";
|
||||||
|
|
||||||
|
|
@ -17,7 +14,6 @@ describe("Security Audit - Path Traversal (Prevention Check)", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw an error for path traversal via taskId", () => {
|
it("should throw an error for path traversal via taskId", () => {
|
||||||
const teamName = "audit-team";
|
|
||||||
const maliciousTaskId = "../../../etc/passwd";
|
const maliciousTaskId = "../../../etc/passwd";
|
||||||
// We need to import readTask/updateTask or just sanitizeName directly if we want to test the logic
|
// We need to import readTask/updateTask or just sanitizeName directly if we want to test the logic
|
||||||
// But since we already tested sanitizeName via other paths, this is just for completeness.
|
// But since we already tested sanitizeName via other paths, this is just for completeness.
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as paths from "./paths";
|
import * as paths from "./paths";
|
||||||
import { createTask, listTasks } from "./tasks";
|
import { createTask } from "./tasks";
|
||||||
|
|
||||||
const testDir = path.join(os.tmpdir(), "pi-tasks-race-test-" + Date.now());
|
const testDir = path.join(os.tmpdir(), `pi-tasks-race-test-${Date.now()}`);
|
||||||
|
|
||||||
describe("Tasks Race Condition Bug", () => {
|
describe("Tasks Race Condition Bug", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,9 @@ import path from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as paths from "./paths";
|
import * as paths from "./paths";
|
||||||
import { createTask, evaluatePlan, listTasks, readTask, submitPlan, updateTask } from "./tasks";
|
import { createTask, evaluatePlan, listTasks, readTask, submitPlan, updateTask } from "./tasks";
|
||||||
import * as teams from "./teams";
|
|
||||||
|
|
||||||
// Mock the paths to use a temporary directory
|
// Mock the paths to use a temporary directory
|
||||||
const testDir = path.join(os.tmpdir(), "pi-teams-test-" + Date.now());
|
const testDir = path.join(os.tmpdir(), `pi-teams-test-${Date.now()}`);
|
||||||
|
|
||||||
describe("Tasks Utilities", () => {
|
describe("Tasks Utilities", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { teamExists } from "./teams";
|
||||||
export function getTaskId(teamName: string): string {
|
export function getTaskId(teamName: string): string {
|
||||||
const dir = taskDir(teamName);
|
const dir = taskDir(teamName);
|
||||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
||||||
const ids = files.map((f) => parseInt(path.parse(f).name, 10)).filter((id) => !isNaN(id));
|
const ids = files.map((f) => parseInt(path.parse(f).name, 10)).filter((id) => !Number.isNaN(id));
|
||||||
return ids.length > 0 ? (Math.max(...ids) + 1).toString() : "1";
|
return ids.length > 0 ? (Math.max(...ids) + 1).toString() : "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,7 +169,7 @@ export async function listTasks(teamName: string): Promise<TaskFile[]> {
|
||||||
const tasks: TaskFile[] = files
|
const tasks: TaskFile[] = files
|
||||||
.map((f) => {
|
.map((f) => {
|
||||||
const id = parseInt(path.parse(f).name, 10);
|
const id = parseInt(path.parse(f).name, 10);
|
||||||
if (isNaN(id)) return null;
|
if (Number.isNaN(id)) return null;
|
||||||
return JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
return JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
||||||
})
|
})
|
||||||
.filter((t) => t !== null);
|
.filter((t) => t !== null);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
|
||||||
import { withLock } from "./lock";
|
import { withLock } from "./lock";
|
||||||
import type { Member, TeamConfig } from "./models";
|
import type { Member, TeamConfig } from "./models";
|
||||||
import { configPath, taskDir, teamDir } from "./paths";
|
import { configPath, taskDir, teamDir } from "./paths";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue