diff --git a/bun.lock b/bun.lock index 87de32bf4..4df4c79e9 100644 --- a/bun.lock +++ b/bun.lock @@ -5,19 +5,22 @@ "": { "name": "@jackwener/opencli", "dependencies": { - "chalk": "^5.3.0", + "@mozilla/readability": "^0.6.0", "cli-table3": "^0.6.5", "commander": "^14.0.3", "js-yaml": "^4.1.0", "turndown": "^7.2.2", - "undici": "^7.24.6", + "turndown-plugin-gfm": "^1.0.2", + "undici": "^8.0.2", "ws": "^8.18.0", }, "devDependencies": { "@types/js-yaml": "^4.0.9", - "@types/node": "^22.13.10", + "@types/jsdom": "^27.0.0", + "@types/node": "^25.5.2", "@types/turndown": "^5.0.6", "@types/ws": "^8.5.13", + "jsdom": "^29.0.2", "tsx": "^4.19.3", "typescript": "^6.0.2", "vitepress": "^1.6.4", @@ -62,6 +65,14 @@ "@algolia/requester-node-http": ["@algolia/requester-node-http@5.49.2", "", { "dependencies": { "@algolia/client-common": "5.49.2" } }, "sha512-UuihBGHafG/ENsrcTGAn5rsOffrCIRuHMOsD85fZGLEY92ate+BMTUqxz60dv5zerh8ZumN4bRm8eW2z9L11jA=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="], + + "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -70,8 +81,22 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.2.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.0", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.3", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + "@docsearch/css": ["@docsearch/css@3.8.2", "", {}, "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ=="], "@docsearch/js": ["@docsearch/js@3.8.2", "", { "dependencies": { "@docsearch/react": "3.8.2", "preact": "^10.0.0" } }, "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ=="], @@ -136,6 +161,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="], + "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="], + "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.74", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-yqaohfY6jnYjTVpuTkaBQHrWbdUrQyWXhau0r/0EZiNWYXPX/P8WWwl1DoLH5CbvDjjcWQw5J0zADhgCUklOqA=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], @@ -144,6 +171,8 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], + "@mozilla/readability": ["@mozilla/readability@0.6.0", "", {}, "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], @@ -260,6 +289,8 @@ "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/jsdom": ["@types/jsdom@27.0.0", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw=="], + "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], @@ -268,7 +299,9 @@ "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], - "@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], "@types/turndown": ["@types/turndown@5.0.6", "", {}, "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg=="], @@ -336,14 +369,14 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], @@ -358,8 +391,14 @@ "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -370,7 +409,7 @@ "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="], - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], @@ -394,14 +433,20 @@ "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsdom": ["jsdom@29.0.2", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -426,12 +471,16 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="], "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], @@ -452,6 +501,8 @@ "oniguruma-to-es": ["oniguruma-to-es@3.1.1", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], @@ -466,12 +517,16 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], @@ -480,6 +535,8 @@ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="], "shiki": ["shiki@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/langs": "2.5.0", "@shikijs/themes": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ=="], @@ -504,6 +561,8 @@ "superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -514,6 +573,14 @@ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tldts": ["tldts@7.0.28", "", { "dependencies": { "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw=="], + + "tldts-core": ["tldts-core@7.0.28", "", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="], + + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -522,11 +589,13 @@ "turndown": ["turndown@7.2.2", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ=="], + "turndown-plugin-gfm": ["turndown-plugin-gfm@1.0.2", "", {}, "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg=="], + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], - "undici": ["undici@7.24.6", "", {}, "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA=="], + "undici": ["undici@8.1.0", "", {}, "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -550,22 +619,46 @@ "vue": ["vue@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/compiler-sfc": "3.5.30", "@vue/runtime-dom": "3.5.30", "@vue/server-renderer": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" } }, "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg=="], + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": "cli.js" }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@types/ws/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], + "@vitest/mocker/vite": ["vite@8.0.2", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.11", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@vitejs/devtools", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "yaml"], "bin": "bin/vite.js" }, "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA=="], + "@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "jsdom/parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], + + "jsdom/undici": ["undici@7.24.6", "", {}, "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA=="], + "vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], "vitest/vite": ["vite@8.0.2", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.11", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@vitejs/devtools", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "yaml"], "bin": "bin/vite.js" }, "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA=="], + "@types/ws/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "jsdom/parse5/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], diff --git a/docs/guide/browser-bridge.md b/docs/guide/browser-bridge.md index 591f2ef83..cdecb61f9 100644 --- a/docs/guide/browser-bridge.md +++ b/docs/guide/browser-bridge.md @@ -48,6 +48,46 @@ Key rules: - `tab select ` makes that tab the default target for later untargeted `opencli browser ...` commands. - `tab close ` removes the tab; if it was the current default target, the stored default is cleared. +## Multiple Chrome Profiles + +If you install the Browser Bridge extension in more than one Chrome profile (e.g. `Work` and `Personal`), all of them stay connected to the same daemon simultaneously. Commands route by profile so each CLI invocation lands in the Chrome profile you intended instead of silently hitting whichever extension connected last. + +### Label a profile + +Each extension generates a unique `profileId` the first time it runs. The popup shows a default label (`Profile-`); click the pencil icon on the chip to rename it to something short like `work` or `home`. That label is what you use in the CLI. + +### Select which profile a command runs in + +Resolution order (highest priority first): + +1. `--profile ` flag on the individual command +2. `OPENCLI_PROFILE` environment variable (per-shell) +3. `opencli profile use ` persistent default (`~/.opencli/config.json`) +4. Automatic routing when exactly one profile is connected (backwards-compatible) + +```bash +opencli profile list # See connected profiles +opencli profile use work # Persist a default +opencli profile current # Show the resolved default +opencli --profile personal reddit saved # Override for one command +``` + +### Concurrent sessions on different profiles + +Use `OPENCLI_PROFILE` (per-shell env) when running two terminals / Claude Code sessions / Codex sessions at the same time. Each session targets its own profile without fighting over a shared default. + +```bash +# Terminal 1 +export OPENCLI_PROFILE=work +opencli reddit saved + +# Terminal 2 — independent, concurrent +export OPENCLI_PROFILE=personal +opencli reddit saved +``` + +Both commands reach their own Chrome profile's automation window; cookies, session state, and logins stay fully isolated. + ## How It Works ``` diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index 47393caf7..f05d269aa 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -7,6 +7,23 @@ - Ensure the opencli Browser Bridge extension is installed and **enabled** in `chrome://extensions`. - Run `opencli doctor` to diagnose connectivity. +### "Multiple profiles connected. Pick one..." + +You have the Browser Bridge extension installed in more than one Chrome profile and no default has been set. Choose a profile in one of three ways: + +- Per-command: `opencli --profile ...` +- Per-shell: `export OPENCLI_PROFILE=` +- Persistent default: `opencli profile use ` + +Run `opencli profile list` to see connected labels, and use the extension popup's pencil icon to rename profiles to short, memorable labels (`work`, `home`, etc.). + +### "Profile 'X' not connected" + +The label or profileId you specified is not in the daemon's active set. Common causes: + +- The target Chrome profile is not currently running, or its extension is disabled in `chrome://extensions`. +- The label does not match. Run `opencli profile list` to see what's actually connected; rename via the extension popup if needed. + ### Empty data or 'Unauthorized' error - Your login session in Chrome might have expired. Open a normal Chrome tab, navigate to the target site, and log in or refresh the page. diff --git a/docs/zh/guide/browser-bridge.md b/docs/zh/guide/browser-bridge.md index aaadbf3cb..1e776c2f5 100644 --- a/docs/zh/guide/browser-bridge.md +++ b/docs/zh/guide/browser-bridge.md @@ -46,6 +46,46 @@ opencli browser tab close - `tab select ` 会把该 tab 设为后续未显式指定 target 的 `opencli browser ...` 命令默认目标。 - `tab close ` 会关闭该 tab;如果它正好是当前默认目标,会一并清掉这条默认绑定。 +## 多 Chrome Profile + +如果你在多个 Chrome profile(例如 `Work` 和 `Personal`)里都装了 Browser Bridge 扩展,它们会同时连到同一个 daemon。命令按 profile 路由,每条 CLI 调用都会命中你指定的那个浏览器,而不是静默落到最后连上来的那个。 + +### 给 profile 命名 + +每个扩展首次启动会生成唯一的 `profileId`。popup 默认显示 `Profile-<短 hash>`,点 chip 上的铅笔图标可以改成 `work`、`home` 这样的短名。CLI 里引用的就是这个 label。 + +### 选择命令跑在哪个 profile + +优先级(从高到低): + +1. 单条命令上的 `--profile ` 参数 +2. `OPENCLI_PROFILE` 环境变量(shell 级) +3. `opencli profile use ` 持久化默认(`~/.opencli/config.json`) +4. 仅一个 profile 在线时的自动路由(向后兼容) + +```bash +opencli profile list # 查看已连接的 profile +opencli profile use work # 持久化默认 +opencli profile current # 查看当前默认来源 +opencli --profile personal reddit saved # 单条命令覆盖 +``` + +### 多个 session 同时操作不同 profile + +用 `OPENCLI_PROFILE`(进程级环境变量)——两个 terminal / Claude Code session / Codex session 各自指向不同 profile,不会互相覆盖共享默认。 + +```bash +# Terminal 1 +export OPENCLI_PROFILE=work +opencli reddit saved + +# Terminal 2 —— 并发独立 +export OPENCLI_PROFILE=personal +opencli reddit saved +``` + +两条命令分别进入各自 Chrome profile 的自动化窗口,cookie、会话状态、登录信息完全隔离。 + ## Daemon 生命周期 Daemon 在首次运行浏览器命令时自动启动,之后保持常驻运行。 diff --git a/extension/dist/background.js b/extension/dist/background.js index 0cb0a2762..73c7320b0 100644 --- a/extension/dist/background.js +++ b/extension/dist/background.js @@ -1,1184 +1,1244 @@ -//#region src/protocol.ts -/** Default daemon port */ -var DAEMON_PORT = 19825; -var DAEMON_HOST = "localhost"; -var DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`; -/** Lightweight health-check endpoint — probed before each WebSocket attempt. */ -var DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`; -/** Base reconnect delay for extension WebSocket (ms) */ -var WS_RECONNECT_BASE_DELAY = 2e3; -/** Max reconnect delay (ms) — kept short since daemon is long-lived */ -var WS_RECONNECT_MAX_DELAY = 5e3; -//#endregion -//#region src/cdp.ts -/** -* CDP execution via chrome.debugger API. -* -* chrome.debugger only needs the "debugger" permission — no host_permissions. -* It can attach to any http/https tab. Avoid chrome:// and chrome-extension:// -* tabs (resolveTabId in background.ts filters them). -*/ -var attached = /* @__PURE__ */ new Set(); -var networkCaptures = /* @__PURE__ */ new Map(); -/** Check if a URL can be attached via CDP — only allow http(s) and blank pages. */ +const DAEMON_PORT = 19825; +const DAEMON_HOST = "localhost"; +const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`; +const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`; +const WS_RECONNECT_BASE_DELAY = 2e3; +const WS_RECONNECT_MAX_DELAY = 5e3; + +const attached = /* @__PURE__ */ new Set(); +const tabFrameContexts = /* @__PURE__ */ new Map(); +const CDP_RESPONSE_BODY_CAPTURE_LIMIT = 8 * 1024 * 1024; +const CDP_REQUEST_BODY_CAPTURE_LIMIT = 1 * 1024 * 1024; +const networkCaptures = /* @__PURE__ */ new Map(); function isDebuggableUrl$1(url) { - if (!url) return true; - return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:"); + if (!url) return true; + return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:"); } async function ensureAttached(tabId, aggressiveRetry = false) { - try { - const tab = await chrome.tabs.get(tabId); - if (!isDebuggableUrl$1(tab.url)) { - attached.delete(tabId); - throw new Error(`Cannot debug tab ${tabId}: URL is ${tab.url ?? "unknown"}`); - } - } catch (e) { - if (e instanceof Error && e.message.startsWith("Cannot debug tab")) throw e; - attached.delete(tabId); - throw new Error(`Tab ${tabId} no longer exists`); - } - if (attached.has(tabId)) try { - await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", { - expression: "1", - returnByValue: true - }); - return; - } catch { - attached.delete(tabId); - } - const MAX_ATTACH_RETRIES = aggressiveRetry ? 5 : 2; - const RETRY_DELAY_MS = aggressiveRetry ? 1500 : 500; - let lastError = ""; - for (let attempt = 1; attempt <= MAX_ATTACH_RETRIES; attempt++) try { - try { - await chrome.debugger.detach({ tabId }); - } catch {} - await chrome.debugger.attach({ tabId }, "1.3"); - lastError = ""; - break; - } catch (e) { - lastError = e instanceof Error ? e.message : String(e); - if (attempt < MAX_ATTACH_RETRIES) { - console.warn(`[opencli] attach attempt ${attempt}/${MAX_ATTACH_RETRIES} failed: ${lastError}, retrying in ${RETRY_DELAY_MS}ms...`); - await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); - try { - const tab = await chrome.tabs.get(tabId); - if (!isDebuggableUrl$1(tab.url)) { - lastError = `Tab URL changed to ${tab.url} during retry`; - break; - } - } catch { - lastError = `Tab ${tabId} no longer exists`; - } - } - } - if (lastError) { - let finalUrl = "unknown"; - let finalWindowId = "unknown"; - try { - const tab = await chrome.tabs.get(tabId); - finalUrl = tab.url ?? "undefined"; - finalWindowId = String(tab.windowId); - } catch {} - console.warn(`[opencli] attach failed for tab ${tabId}: url=${finalUrl}, windowId=${finalWindowId}, error=${lastError}`); - const hint = lastError.includes("chrome-extension://") ? ". Tip: another Chrome extension may be interfering — try disabling other extensions" : ""; - throw new Error(`attach failed: ${lastError}${hint}`); - } - attached.add(tabId); - try { - await chrome.debugger.sendCommand({ tabId }, "Runtime.enable"); - } catch {} + try { + const tab = await chrome.tabs.get(tabId); + if (!isDebuggableUrl$1(tab.url)) { + attached.delete(tabId); + throw new Error(`Cannot debug tab ${tabId}: URL is ${tab.url ?? "unknown"}`); + } + } catch (e) { + if (e instanceof Error && e.message.startsWith("Cannot debug tab")) throw e; + attached.delete(tabId); + throw new Error(`Tab ${tabId} no longer exists`); + } + if (attached.has(tabId)) { + try { + await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", { + expression: "1", + returnByValue: true + }); + return; + } catch { + attached.delete(tabId); + } + } + const MAX_ATTACH_RETRIES = aggressiveRetry ? 5 : 2; + const RETRY_DELAY_MS = aggressiveRetry ? 1500 : 500; + let lastError = ""; + for (let attempt = 1; attempt <= MAX_ATTACH_RETRIES; attempt++) { + try { + try { + await chrome.debugger.detach({ tabId }); + } catch { + } + await chrome.debugger.attach({ tabId }, "1.3"); + lastError = ""; + break; + } catch (e) { + lastError = e instanceof Error ? e.message : String(e); + if (attempt < MAX_ATTACH_RETRIES) { + console.warn(`[opencli] attach attempt ${attempt}/${MAX_ATTACH_RETRIES} failed: ${lastError}, retrying in ${RETRY_DELAY_MS}ms...`); + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); + try { + const tab = await chrome.tabs.get(tabId); + if (!isDebuggableUrl$1(tab.url)) { + lastError = `Tab URL changed to ${tab.url} during retry`; + break; + } + } catch { + lastError = `Tab ${tabId} no longer exists`; + } + } + } + } + if (lastError) { + let finalUrl = "unknown"; + let finalWindowId = "unknown"; + try { + const tab = await chrome.tabs.get(tabId); + finalUrl = tab.url ?? "undefined"; + finalWindowId = String(tab.windowId); + } catch { + } + console.warn(`[opencli] attach failed for tab ${tabId}: url=${finalUrl}, windowId=${finalWindowId}, error=${lastError}`); + const hint = lastError.includes("chrome-extension://") ? ". Tip: another Chrome extension may be interfering — try disabling other extensions" : ""; + throw new Error(`attach failed: ${lastError}${hint}`); + } + attached.add(tabId); + try { + await chrome.debugger.sendCommand({ tabId }, "Runtime.enable"); + } catch { + } } async function evaluate(tabId, expression, aggressiveRetry = false) { - const MAX_EVAL_RETRIES = aggressiveRetry ? 3 : 2; - for (let attempt = 1; attempt <= MAX_EVAL_RETRIES; attempt++) try { - await ensureAttached(tabId, aggressiveRetry); - const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", { - expression, - returnByValue: true, - awaitPromise: true - }); - if (result.exceptionDetails) { - const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error"; - throw new Error(errMsg); - } - return result.result?.value; - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - const isNavigateError = msg.includes("Inspected target navigated") || msg.includes("Target closed"); - if ((isNavigateError || msg.includes("attach failed") || msg.includes("Debugger is not attached") || msg.includes("chrome-extension://")) && attempt < MAX_EVAL_RETRIES) { - attached.delete(tabId); - const retryMs = isNavigateError ? 200 : 500; - await new Promise((resolve) => setTimeout(resolve, retryMs)); - continue; - } - throw e; - } - throw new Error("evaluate: max retries exhausted"); + const MAX_EVAL_RETRIES = aggressiveRetry ? 3 : 2; + for (let attempt = 1; attempt <= MAX_EVAL_RETRIES; attempt++) { + try { + await ensureAttached(tabId, aggressiveRetry); + const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", { + expression, + returnByValue: true, + awaitPromise: true + }); + if (result.exceptionDetails) { + const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error"; + throw new Error(errMsg); + } + return result.result?.value; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + const isNavigateError = msg.includes("Inspected target navigated") || msg.includes("Target closed"); + const isAttachError = isNavigateError || msg.includes("attach failed") || msg.includes("Debugger is not attached") || msg.includes("chrome-extension://"); + if (isAttachError && attempt < MAX_EVAL_RETRIES) { + attached.delete(tabId); + const retryMs = isNavigateError ? 200 : 500; + await new Promise((resolve) => setTimeout(resolve, retryMs)); + continue; + } + throw e; + } + } + throw new Error("evaluate: max retries exhausted"); } -var evaluateAsync = evaluate; -/** -* Capture a screenshot via CDP Page.captureScreenshot. -* Returns base64-encoded image data. -*/ +const evaluateAsync = evaluate; async function screenshot(tabId, options = {}) { - await ensureAttached(tabId); - const format = options.format ?? "png"; - if (options.fullPage) { - const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics"); - const size = metrics.cssContentSize || metrics.contentSize; - if (size) await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", { - mobile: false, - width: Math.ceil(size.width), - height: Math.ceil(size.height), - deviceScaleFactor: 1 - }); - } - try { - const params = { format }; - if (format === "jpeg" && options.quality !== void 0) params.quality = Math.max(0, Math.min(100, options.quality)); - return (await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params)).data; - } finally { - if (options.fullPage) await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => {}); - } + await ensureAttached(tabId); + const format = options.format ?? "png"; + if (options.fullPage) { + const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics"); + const size = metrics.cssContentSize || metrics.contentSize; + if (size) { + await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", { + mobile: false, + width: Math.ceil(size.width), + height: Math.ceil(size.height), + deviceScaleFactor: 1 + }); + } + } + try { + const params = { format }; + if (format === "jpeg" && options.quality !== void 0) { + params.quality = Math.max(0, Math.min(100, options.quality)); + } + const result = await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params); + return result.data; + } finally { + if (options.fullPage) { + await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => { + }); + } + } } -/** -* Set local file paths on a file input element via CDP DOM.setFileInputFiles. -* This bypasses the need to send large base64 payloads through the message channel — -* Chrome reads the files directly from the local filesystem. -* -* @param tabId - Target tab ID -* @param files - Array of absolute local file paths -* @param selector - CSS selector to find the file input (optional, defaults to first file input) -*/ async function setFileInputFiles(tabId, files, selector) { - await ensureAttached(tabId); - await chrome.debugger.sendCommand({ tabId }, "DOM.enable"); - const doc = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument"); - const query = selector || "input[type=\"file\"]"; - const result = await chrome.debugger.sendCommand({ tabId }, "DOM.querySelector", { - nodeId: doc.root.nodeId, - selector: query - }); - if (!result.nodeId) throw new Error(`No element found matching selector: ${query}`); - await chrome.debugger.sendCommand({ tabId }, "DOM.setFileInputFiles", { - files, - nodeId: result.nodeId - }); + await ensureAttached(tabId); + await chrome.debugger.sendCommand({ tabId }, "DOM.enable"); + const doc = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument"); + const query = selector || 'input[type="file"]'; + const result = await chrome.debugger.sendCommand({ tabId }, "DOM.querySelector", { + nodeId: doc.root.nodeId, + selector: query + }); + if (!result.nodeId) { + throw new Error(`No element found matching selector: ${query}`); + } + await chrome.debugger.sendCommand({ tabId }, "DOM.setFileInputFiles", { + files, + nodeId: result.nodeId + }); } async function insertText(tabId, text) { - await ensureAttached(tabId); - await chrome.debugger.sendCommand({ tabId }, "Input.insertText", { text }); + await ensureAttached(tabId); + await chrome.debugger.sendCommand({ tabId }, "Input.insertText", { text }); +} +function registerFrameTracking() { + chrome.debugger.onEvent.addListener((source, method, params) => { + const tabId = source.tabId; + if (!tabId) return; + if (method === "Runtime.executionContextCreated") { + const context = params.context; + if (!context?.auxData?.frameId || context.auxData.isDefault !== true) return; + const frameId = context.auxData.frameId; + if (!tabFrameContexts.has(tabId)) { + tabFrameContexts.set(tabId, /* @__PURE__ */ new Map()); + } + tabFrameContexts.get(tabId).set(frameId, context.id); + } + if (method === "Runtime.executionContextDestroyed") { + const ctxId = params.executionContextId; + const contexts = tabFrameContexts.get(tabId); + if (contexts) { + for (const [fid, cid] of contexts) { + if (cid === ctxId) { + contexts.delete(fid); + break; + } + } + } + } + if (method === "Runtime.executionContextsCleared") { + tabFrameContexts.delete(tabId); + } + }); + chrome.tabs.onRemoved.addListener((tabId) => { + tabFrameContexts.delete(tabId); + }); +} +async function getFrameTree(tabId) { + await ensureAttached(tabId); + return chrome.debugger.sendCommand({ tabId }, "Page.getFrameTree"); +} +async function evaluateInFrame(tabId, expression, frameId, aggressiveRetry = false) { + await ensureAttached(tabId, aggressiveRetry); + await chrome.debugger.sendCommand({ tabId }, "Runtime.enable").catch(() => { + }); + const contexts = tabFrameContexts.get(tabId); + const contextId = contexts?.get(frameId); + if (contextId === void 0) { + throw new Error(`No execution context found for frame ${frameId}. The frame may not be loaded yet.`); + } + const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", { + expression, + contextId, + returnByValue: true, + awaitPromise: true + }); + if (result.exceptionDetails) { + const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error"; + throw new Error(errMsg); + } + return result.result?.value; } function normalizeCapturePatterns(pattern) { - return String(pattern || "").split("|").map((part) => part.trim()).filter(Boolean); + return String(pattern || "").split("|").map((part) => part.trim()).filter(Boolean); } function shouldCaptureUrl(url, patterns) { - if (!url) return false; - if (!patterns.length) return true; - return patterns.some((pattern) => url.includes(pattern)); + if (!url) return false; + if (!patterns.length) return true; + return patterns.some((pattern) => url.includes(pattern)); } function normalizeHeaders(headers) { - if (!headers || typeof headers !== "object") return {}; - const out = {}; - for (const [key, value] of Object.entries(headers)) out[String(key)] = String(value); - return out; + if (!headers || typeof headers !== "object") return {}; + const out = {}; + for (const [key, value] of Object.entries(headers)) { + out[String(key)] = String(value); + } + return out; } function getOrCreateNetworkCaptureEntry(tabId, requestId, fallback) { - const state = networkCaptures.get(tabId); - if (!state) return null; - const existingIndex = state.requestToIndex.get(requestId); - if (existingIndex !== void 0) return state.entries[existingIndex] || null; - const url = fallback?.url || ""; - if (!shouldCaptureUrl(url, state.patterns)) return null; - const entry = { - kind: "cdp", - url, - method: fallback?.method || "GET", - requestHeaders: fallback?.requestHeaders || {}, - timestamp: Date.now() - }; - state.entries.push(entry); - state.requestToIndex.set(requestId, state.entries.length - 1); - return entry; + const state = networkCaptures.get(tabId); + if (!state) return null; + const existingIndex = state.requestToIndex.get(requestId); + if (existingIndex !== void 0) { + return state.entries[existingIndex] || null; + } + const url = fallback?.url || ""; + if (!shouldCaptureUrl(url, state.patterns)) return null; + const entry = { + kind: "cdp", + url, + method: fallback?.method || "GET", + requestHeaders: fallback?.requestHeaders || {}, + timestamp: Date.now() + }; + state.entries.push(entry); + state.requestToIndex.set(requestId, state.entries.length - 1); + return entry; } async function startNetworkCapture(tabId, pattern) { - await ensureAttached(tabId); - await chrome.debugger.sendCommand({ tabId }, "Network.enable"); - networkCaptures.set(tabId, { - patterns: normalizeCapturePatterns(pattern), - entries: [], - requestToIndex: /* @__PURE__ */ new Map() - }); + await ensureAttached(tabId); + await chrome.debugger.sendCommand({ tabId }, "Network.enable"); + networkCaptures.set(tabId, { + patterns: normalizeCapturePatterns(pattern), + entries: [], + requestToIndex: /* @__PURE__ */ new Map() + }); } async function readNetworkCapture(tabId) { - const state = networkCaptures.get(tabId); - if (!state) return []; - const entries = state.entries.slice(); - state.entries = []; - state.requestToIndex.clear(); - return entries; + const state = networkCaptures.get(tabId); + if (!state) return []; + const entries = state.entries.slice(); + state.entries = []; + state.requestToIndex.clear(); + return entries; } function hasActiveNetworkCapture(tabId) { - return networkCaptures.has(tabId); + return networkCaptures.has(tabId); } async function detach(tabId) { - if (!attached.has(tabId)) return; - attached.delete(tabId); - networkCaptures.delete(tabId); - try { - await chrome.debugger.detach({ tabId }); - } catch {} + if (!attached.has(tabId)) return; + attached.delete(tabId); + networkCaptures.delete(tabId); + tabFrameContexts.delete(tabId); + try { + await chrome.debugger.detach({ tabId }); + } catch { + } } function registerListeners() { - chrome.tabs.onRemoved.addListener((tabId) => { - attached.delete(tabId); - networkCaptures.delete(tabId); - }); - chrome.debugger.onDetach.addListener((source) => { - if (source.tabId) { - attached.delete(source.tabId); - networkCaptures.delete(source.tabId); - } - }); - chrome.tabs.onUpdated.addListener(async (tabId, info) => { - if (info.url && !isDebuggableUrl$1(info.url)) await detach(tabId); - }); - chrome.debugger.onEvent.addListener(async (source, method, params) => { - const tabId = source.tabId; - if (!tabId) return; - const state = networkCaptures.get(tabId); - if (!state) return; - if (method === "Network.requestWillBeSent") { - const requestId = String(params?.requestId || ""); - const request = params?.request; - const entry = getOrCreateNetworkCaptureEntry(tabId, requestId, { - url: request?.url, - method: request?.method, - requestHeaders: normalizeHeaders(request?.headers) - }); - if (!entry) return; - entry.requestBodyKind = request?.hasPostData ? "string" : "empty"; - entry.requestBodyPreview = String(request?.postData || "").slice(0, 4e3); - try { - const postData = await chrome.debugger.sendCommand({ tabId }, "Network.getRequestPostData", { requestId }); - if (postData?.postData) { - entry.requestBodyKind = "string"; - entry.requestBodyPreview = postData.postData.slice(0, 4e3); - } - } catch {} - return; - } - if (method === "Network.responseReceived") { - const requestId = String(params?.requestId || ""); - const response = params?.response; - const entry = getOrCreateNetworkCaptureEntry(tabId, requestId, { url: response?.url }); - if (!entry) return; - entry.responseStatus = response?.status; - entry.responseContentType = response?.mimeType || ""; - entry.responseHeaders = normalizeHeaders(response?.headers); - return; - } - if (method === "Network.loadingFinished") { - const requestId = String(params?.requestId || ""); - const stateEntryIndex = state.requestToIndex.get(requestId); - if (stateEntryIndex === void 0) return; - const entry = state.entries[stateEntryIndex]; - if (!entry) return; - try { - const body = await chrome.debugger.sendCommand({ tabId }, "Network.getResponseBody", { requestId }); - if (typeof body?.body === "string") entry.responsePreview = body.base64Encoded ? `base64:${body.body.slice(0, 4e3)}` : body.body.slice(0, 4e3); - } catch {} - } - }); + chrome.tabs.onRemoved.addListener((tabId) => { + attached.delete(tabId); + networkCaptures.delete(tabId); + tabFrameContexts.delete(tabId); + }); + chrome.debugger.onDetach.addListener((source) => { + if (source.tabId) { + attached.delete(source.tabId); + networkCaptures.delete(source.tabId); + tabFrameContexts.delete(source.tabId); + } + }); + chrome.tabs.onUpdated.addListener(async (tabId, info) => { + if (info.url && !isDebuggableUrl$1(info.url)) { + await detach(tabId); + } + }); + chrome.debugger.onEvent.addListener(async (source, method, params) => { + const tabId = source.tabId; + if (!tabId) return; + const state = networkCaptures.get(tabId); + if (!state) return; + if (method === "Network.requestWillBeSent") { + const requestId = String(params?.requestId || ""); + const request = params?.request; + const entry = getOrCreateNetworkCaptureEntry(tabId, requestId, { + url: request?.url, + method: request?.method, + requestHeaders: normalizeHeaders(request?.headers) + }); + if (!entry) return; + entry.requestBodyKind = request?.hasPostData ? "string" : "empty"; + { + const raw = String(request?.postData || ""); + const fullSize = raw.length; + const truncated = fullSize > CDP_REQUEST_BODY_CAPTURE_LIMIT; + entry.requestBodyPreview = truncated ? raw.slice(0, CDP_REQUEST_BODY_CAPTURE_LIMIT) : raw; + entry.requestBodyFullSize = fullSize; + entry.requestBodyTruncated = truncated; + } + try { + const postData = await chrome.debugger.sendCommand({ tabId }, "Network.getRequestPostData", { requestId }); + if (postData?.postData) { + const raw = postData.postData; + const fullSize = raw.length; + const truncated = fullSize > CDP_REQUEST_BODY_CAPTURE_LIMIT; + entry.requestBodyKind = "string"; + entry.requestBodyPreview = truncated ? raw.slice(0, CDP_REQUEST_BODY_CAPTURE_LIMIT) : raw; + entry.requestBodyFullSize = fullSize; + entry.requestBodyTruncated = truncated; + } + } catch { + } + return; + } + if (method === "Network.responseReceived") { + const requestId = String(params?.requestId || ""); + const response = params?.response; + const entry = getOrCreateNetworkCaptureEntry(tabId, requestId, { + url: response?.url + }); + if (!entry) return; + entry.responseStatus = response?.status; + entry.responseContentType = response?.mimeType || ""; + entry.responseHeaders = normalizeHeaders(response?.headers); + return; + } + if (method === "Network.loadingFinished") { + const requestId = String(params?.requestId || ""); + const stateEntryIndex = state.requestToIndex.get(requestId); + if (stateEntryIndex === void 0) return; + const entry = state.entries[stateEntryIndex]; + if (!entry) return; + try { + const body = await chrome.debugger.sendCommand({ tabId }, "Network.getResponseBody", { requestId }); + if (typeof body?.body === "string") { + const fullSize = body.body.length; + const truncated = fullSize > CDP_RESPONSE_BODY_CAPTURE_LIMIT; + const stored = truncated ? body.body.slice(0, CDP_RESPONSE_BODY_CAPTURE_LIMIT) : body.body; + entry.responsePreview = body.base64Encoded ? `base64:${stored}` : stored; + entry.responseBodyFullSize = fullSize; + entry.responseBodyTruncated = truncated; + } + } catch { + } + } + }); } -//#endregion -//#region src/identity.ts -/** -* Page identity mapping — targetId ↔ tabId. -* -* targetId is the cross-layer page identity (CDP target UUID). -* tabId is an internal Chrome Tabs API routing detail — never exposed outside the extension. -* -* Lifecycle: -* - Cache populated lazily via chrome.debugger.getTargets() -* - Evicted on tab close (chrome.tabs.onRemoved) -* - Miss triggers full refresh; refresh miss → hard error (no guessing) -*/ -var targetToTab = /* @__PURE__ */ new Map(); -var tabToTarget = /* @__PURE__ */ new Map(); -/** -* Resolve targetId for a given tabId. -* Returns cached value if available; on miss, refreshes from chrome.debugger.getTargets(). -* Throws if no targetId can be found (page may have been destroyed). -*/ + +const targetToTab = /* @__PURE__ */ new Map(); +const tabToTarget = /* @__PURE__ */ new Map(); async function resolveTargetId(tabId) { - const cached = tabToTarget.get(tabId); - if (cached) return cached; - await refreshMappings(); - const result = tabToTarget.get(tabId); - if (!result) throw new Error(`No targetId for tab ${tabId} — page may have been closed`); - return result; + const cached = tabToTarget.get(tabId); + if (cached) return cached; + await refreshMappings(); + const result = tabToTarget.get(tabId); + if (!result) throw new Error(`No targetId for tab ${tabId} — page may have been closed`); + return result; } -/** -* Resolve tabId for a given targetId. -* Returns cached value if available; on miss, refreshes from chrome.debugger.getTargets(). -* Throws if no tabId can be found — never falls back to guessing. -*/ async function resolveTabId$1(targetId) { - const cached = targetToTab.get(targetId); - if (cached !== void 0) return cached; - await refreshMappings(); - const result = targetToTab.get(targetId); - if (result === void 0) throw new Error(`Page not found: ${targetId} — stale page identity`); - return result; + const cached = targetToTab.get(targetId); + if (cached !== void 0) return cached; + await refreshMappings(); + const result = targetToTab.get(targetId); + if (result === void 0) throw new Error(`Page not found: ${targetId} — stale page identity`); + return result; } -/** -* Remove mappings for a closed tab. -* Called from chrome.tabs.onRemoved listener. -*/ function evictTab(tabId) { - const targetId = tabToTarget.get(tabId); - if (targetId) targetToTab.delete(targetId); - tabToTarget.delete(tabId); + const targetId = tabToTarget.get(tabId); + if (targetId) targetToTab.delete(targetId); + tabToTarget.delete(tabId); } -/** -* Full refresh of targetId ↔ tabId mappings from chrome.debugger.getTargets(). -*/ async function refreshMappings() { - const targets = await chrome.debugger.getTargets(); - targetToTab.clear(); - tabToTarget.clear(); - for (const t of targets) if (t.type === "page" && t.tabId !== void 0) { - targetToTab.set(t.id, t.tabId); - tabToTarget.set(t.tabId, t.id); - } + const targets = await chrome.debugger.getTargets(); + targetToTab.clear(); + tabToTarget.clear(); + for (const t of targets) { + if (t.type === "page" && t.tabId !== void 0) { + targetToTab.set(t.id, t.tabId); + tabToTarget.set(t.tabId, t.id); + } + } +} + +let ws = null; +let reconnectTimer = null; +let reconnectAttempts = 0; +let profileId = null; +let profileLabel = null; +async function loadProfileIdentity() { + const stored = await chrome.storage.local.get(["profileId", "profileLabel"]); + if (stored.profileId && typeof stored.profileId === "string") { + profileId = stored.profileId; + profileLabel = typeof stored.profileLabel === "string" && stored.profileLabel ? stored.profileLabel : `Profile-${profileId.slice(0, 8)}`; + return; + } + const id = crypto.randomUUID(); + const label = `Profile-${id.slice(0, 8)}`; + await chrome.storage.local.set({ profileId: id, profileLabel: label }); + profileId = id; + profileLabel = label; +} +function defaultLabelFor(id) { + return `Profile-${id.slice(0, 8)}`; } -//#endregion -//#region src/background.ts -var ws = null; -var reconnectTimer = null; -var reconnectAttempts = 0; -var _origLog = console.log.bind(console); -var _origWarn = console.warn.bind(console); -var _origError = console.error.bind(console); +async function renameProfile(label) { + if (!profileId) await loadProfileIdentity(); + const cleaned = label?.trim(); + const finalLabel = cleaned && cleaned.length > 0 ? cleaned.slice(0, 60) : defaultLabelFor(profileId); + await chrome.storage.local.set({ profileLabel: finalLabel }); + profileLabel = finalLabel; + if (ws?.readyState === WebSocket.OPEN) { + try { + ws.send(JSON.stringify({ + type: "hello", + version: chrome.runtime.getManifest().version, + compatRange: ">=1.7.0", + profileId, + profileLabel + })); + } catch (err) { + console.warn("[opencli] Failed to re-announce renamed label:", err); + } + } + return finalLabel; +} +const _origLog = console.log.bind(console); +const _origWarn = console.warn.bind(console); +const _origError = console.error.bind(console); function forwardLog(level, args) { - if (!ws || ws.readyState !== WebSocket.OPEN) return; - try { - const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" "); - ws.send(JSON.stringify({ - type: "log", - level, - msg, - ts: Date.now() - })); - } catch {} + if (!ws || ws.readyState !== WebSocket.OPEN) return; + try { + const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" "); + ws.send(JSON.stringify({ type: "log", level, msg, ts: Date.now() })); + } catch { + } } console.log = (...args) => { - _origLog(...args); - forwardLog("info", args); + _origLog(...args); + forwardLog("info", args); }; console.warn = (...args) => { - _origWarn(...args); - forwardLog("warn", args); + _origWarn(...args); + forwardLog("warn", args); }; console.error = (...args) => { - _origError(...args); - forwardLog("error", args); + _origError(...args); + forwardLog("error", args); }; -/** -* Probe the daemon via its /ping HTTP endpoint before attempting a WebSocket -* connection. fetch() failures are silently catchable; new WebSocket() is not -* — Chrome logs ERR_CONNECTION_REFUSED to the extension error page before any -* JS handler can intercept it. By keeping the probe inside connect() every -* call site remains unchanged and the guard can never be accidentally skipped. -*/ async function connect() { - if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return; - try { - if (!(await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1e3) })).ok) return; - } catch { - return; - } - try { - ws = new WebSocket(DAEMON_WS_URL); - } catch { - scheduleReconnect(); - return; - } - ws.onopen = () => { - console.log("[opencli] Connected to daemon"); - reconnectAttempts = 0; - if (reconnectTimer) { - clearTimeout(reconnectTimer); - reconnectTimer = null; - } - ws?.send(JSON.stringify({ - type: "hello", - version: chrome.runtime.getManifest().version, - compatRange: ">=1.7.0" - })); - }; - ws.onmessage = async (event) => { - try { - const result = await handleCommand(JSON.parse(event.data)); - ws?.send(JSON.stringify(result)); - } catch (err) { - console.error("[opencli] Message handling error:", err); - } - }; - ws.onclose = () => { - console.log("[opencli] Disconnected from daemon"); - ws = null; - scheduleReconnect(); - }; - ws.onerror = () => { - ws?.close(); - }; + if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return; + if (!profileId) await loadProfileIdentity(); + try { + const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1e3) }); + if (!res.ok) return; + } catch { + return; + } + let thisWs; + try { + thisWs = new WebSocket(DAEMON_WS_URL); + ws = thisWs; + } catch { + scheduleReconnect(); + return; + } + thisWs.onopen = () => { + if (ws !== thisWs) return; + console.log("[opencli] Connected to daemon"); + reconnectAttempts = 0; + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + thisWs.send(JSON.stringify({ + type: "hello", + version: chrome.runtime.getManifest().version, + compatRange: ">=1.7.0", + profileId, + profileLabel + })); + }; + thisWs.onmessage = async (event) => { + try { + const command = JSON.parse(event.data); + const result = await handleCommand(command); + thisWs.send(JSON.stringify(result)); + } catch (err) { + console.error("[opencli] Message handling error:", err); + } + }; + thisWs.onclose = () => { + if (ws !== thisWs) return; + console.log("[opencli] Disconnected from daemon"); + ws = null; + scheduleReconnect(); + }; + thisWs.onerror = () => { + thisWs.close(); + }; } -/** -* After MAX_EAGER_ATTEMPTS (reaching 60s backoff), stop scheduling reconnects. -* The keepalive alarm (~24s) will still call connect() periodically, but at a -* much lower frequency — reducing console noise when the daemon is not running. -*/ -var MAX_EAGER_ATTEMPTS = 6; +const MAX_EAGER_ATTEMPTS = 6; function scheduleReconnect() { - if (reconnectTimer) return; - reconnectAttempts++; - if (reconnectAttempts > MAX_EAGER_ATTEMPTS) return; - const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY); - reconnectTimer = setTimeout(() => { - reconnectTimer = null; - connect(); - }, delay); + if (reconnectTimer) return; + reconnectAttempts++; + if (reconnectAttempts > MAX_EAGER_ATTEMPTS) return; + const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY); + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + void connect(); + }, delay); } -var automationSessions = /* @__PURE__ */ new Map(); -var IDLE_TIMEOUT_DEFAULT = 3e4; -var IDLE_TIMEOUT_INTERACTIVE = 6e5; -/** Per-workspace custom timeout overrides set via command.idleTimeout */ -var workspaceTimeoutOverrides = /* @__PURE__ */ new Map(); +const automationSessions = /* @__PURE__ */ new Map(); +const IDLE_TIMEOUT_DEFAULT = 3e4; +const IDLE_TIMEOUT_INTERACTIVE = 6e5; +const workspaceTimeoutOverrides = /* @__PURE__ */ new Map(); function getIdleTimeout(workspace) { - const override = workspaceTimeoutOverrides.get(workspace); - if (override !== void 0) return override; - if (workspace.startsWith("browser:") || workspace.startsWith("operate:")) return IDLE_TIMEOUT_INTERACTIVE; - return IDLE_TIMEOUT_DEFAULT; + const override = workspaceTimeoutOverrides.get(workspace); + if (override !== void 0) return override; + if (workspace.startsWith("browser:") || workspace.startsWith("operate:")) { + return IDLE_TIMEOUT_INTERACTIVE; + } + return IDLE_TIMEOUT_DEFAULT; } -var windowFocused = false; +let windowFocused = false; function getWorkspaceKey(workspace) { - return workspace?.trim() || "default"; + return workspace?.trim() || "default"; } function resetWindowIdleTimer(workspace) { - const session = automationSessions.get(workspace); - if (!session) return; - if (session.idleTimer) clearTimeout(session.idleTimer); - const timeout = getIdleTimeout(workspace); - session.idleDeadlineAt = Date.now() + timeout; - session.idleTimer = setTimeout(async () => { - const current = automationSessions.get(workspace); - if (!current) return; - if (!current.owned) { - console.log(`[opencli] Borrowed workspace ${workspace} detached from window ${current.windowId} (idle timeout)`); - workspaceTimeoutOverrides.delete(workspace); - automationSessions.delete(workspace); - return; - } - try { - await chrome.windows.remove(current.windowId); - console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout, ${timeout / 1e3}s)`); - } catch {} - workspaceTimeoutOverrides.delete(workspace); - automationSessions.delete(workspace); - }, timeout); + const session = automationSessions.get(workspace); + if (!session) return; + if (session.idleTimer) clearTimeout(session.idleTimer); + const timeout = getIdleTimeout(workspace); + session.idleDeadlineAt = Date.now() + timeout; + session.idleTimer = setTimeout(async () => { + const current = automationSessions.get(workspace); + if (!current) return; + if (!current.owned) { + console.log(`[opencli] Borrowed workspace ${workspace} detached from window ${current.windowId} (idle timeout)`); + workspaceTimeoutOverrides.delete(workspace); + automationSessions.delete(workspace); + return; + } + try { + await chrome.windows.remove(current.windowId); + console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout, ${timeout / 1e3}s)`); + } catch { + } + workspaceTimeoutOverrides.delete(workspace); + automationSessions.delete(workspace); + }, timeout); } -/** Get or create the dedicated automation window. -* @param initialUrl — if provided (http/https), used as the initial page instead of about:blank. -* This avoids an extra blank-page→target-domain navigation on first command. -*/ async function getAutomationWindow(workspace, initialUrl) { - const existing = automationSessions.get(workspace); - if (existing) try { - await chrome.windows.get(existing.windowId); - return existing.windowId; - } catch { - automationSessions.delete(workspace); - } - const startUrl = initialUrl && isSafeNavigationUrl(initialUrl) ? initialUrl : BLANK_PAGE; - const win = await chrome.windows.create({ - url: startUrl, - focused: windowFocused, - width: 1280, - height: 900, - type: "normal" - }); - const session = { - windowId: win.id, - idleTimer: null, - idleDeadlineAt: Date.now() + getIdleTimeout(workspace), - owned: true, - preferredTabId: null - }; - automationSessions.set(workspace, session); - console.log(`[opencli] Created automation window ${session.windowId} (${workspace}, start=${startUrl})`); - resetWindowIdleTimer(workspace); - const tabs = await chrome.tabs.query({ windowId: win.id }); - if (tabs[0]?.id) await new Promise((resolve) => { - const timeout = setTimeout(resolve, 500); - const listener = (tabId, info) => { - if (tabId === tabs[0].id && info.status === "complete") { - chrome.tabs.onUpdated.removeListener(listener); - clearTimeout(timeout); - resolve(); - } - }; - if (tabs[0].status === "complete") { - clearTimeout(timeout); - resolve(); - } else chrome.tabs.onUpdated.addListener(listener); - }); - return session.windowId; + const existing = automationSessions.get(workspace); + if (existing) { + try { + await chrome.windows.get(existing.windowId); + return existing.windowId; + } catch { + automationSessions.delete(workspace); + } + } + const startUrl = initialUrl && isSafeNavigationUrl(initialUrl) ? initialUrl : BLANK_PAGE; + const win = await chrome.windows.create({ + url: startUrl, + focused: windowFocused, + width: 1280, + height: 900, + type: "normal" + }); + const session = { + windowId: win.id, + idleTimer: null, + idleDeadlineAt: Date.now() + getIdleTimeout(workspace), + owned: true, + preferredTabId: null + }; + automationSessions.set(workspace, session); + console.log(`[opencli] Created automation window ${session.windowId} (${workspace}, start=${startUrl})`); + resetWindowIdleTimer(workspace); + const tabs = await chrome.tabs.query({ windowId: win.id }); + if (tabs[0]?.id) { + await new Promise((resolve) => { + const timeout = setTimeout(resolve, 500); + const listener = (tabId, info) => { + if (tabId === tabs[0].id && info.status === "complete") { + chrome.tabs.onUpdated.removeListener(listener); + clearTimeout(timeout); + resolve(); + } + }; + if (tabs[0].status === "complete") { + clearTimeout(timeout); + resolve(); + } else { + chrome.tabs.onUpdated.addListener(listener); + } + }); + } + return session.windowId; } chrome.windows.onRemoved.addListener(async (windowId) => { - for (const [workspace, session] of automationSessions.entries()) if (session.windowId === windowId) { - console.log(`[opencli] Automation window closed (${workspace})`); - if (session.idleTimer) clearTimeout(session.idleTimer); - automationSessions.delete(workspace); - workspaceTimeoutOverrides.delete(workspace); - } + for (const [workspace, session] of automationSessions.entries()) { + if (session.windowId === windowId) { + console.log(`[opencli] Automation window closed (${workspace})`); + if (session.idleTimer) clearTimeout(session.idleTimer); + automationSessions.delete(workspace); + workspaceTimeoutOverrides.delete(workspace); + } + } }); chrome.tabs.onRemoved.addListener((tabId) => { - evictTab(tabId); + evictTab(tabId); }); -var initialized = false; +let initialized = false; function initialize() { - if (initialized) return; - initialized = true; - chrome.alarms.create("keepalive", { periodInMinutes: .4 }); - registerListeners(); - connect(); - console.log("[opencli] OpenCLI extension initialized"); + if (initialized) return; + initialized = true; + chrome.alarms.create("keepalive", { periodInMinutes: 0.4 }); + registerListeners(); + registerFrameTracking(); + void connect(); + console.log("[opencli] OpenCLI extension initialized"); } chrome.runtime.onInstalled.addListener(() => { - initialize(); + initialize(); }); chrome.runtime.onStartup.addListener(() => { - initialize(); + initialize(); }); chrome.alarms.onAlarm.addListener((alarm) => { - if (alarm.name === "keepalive") connect(); + if (alarm.name === "keepalive") void connect(); }); chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { - if (msg?.type === "getStatus") sendResponse({ - connected: ws?.readyState === WebSocket.OPEN, - reconnecting: reconnectTimer !== null - }); - return false; + if (msg?.type === "getStatus") { + sendResponse({ + connected: ws?.readyState === WebSocket.OPEN, + reconnecting: reconnectTimer !== null, + profileLabel + }); + return false; + } + if (msg?.type === "renameProfile") { + const newLabel = typeof msg.label === "string" ? msg.label : null; + renameProfile(newLabel).then((finalLabel) => sendResponse({ ok: true, profileLabel: finalLabel })).catch((err) => sendResponse({ ok: false, error: String(err) })); + return true; + } + return false; }); async function handleCommand(cmd) { - const workspace = getWorkspaceKey(cmd.workspace); - windowFocused = cmd.windowFocused === true; - if (cmd.idleTimeout != null && cmd.idleTimeout > 0) workspaceTimeoutOverrides.set(workspace, cmd.idleTimeout * 1e3); - resetWindowIdleTimer(workspace); - try { - switch (cmd.action) { - case "exec": return await handleExec(cmd, workspace); - case "navigate": return await handleNavigate(cmd, workspace); - case "tabs": return await handleTabs(cmd, workspace); - case "cookies": return await handleCookies(cmd); - case "screenshot": return await handleScreenshot(cmd, workspace); - case "close-window": return await handleCloseWindow(cmd, workspace); - case "cdp": return await handleCdp(cmd, workspace); - case "sessions": return await handleSessions(cmd); - case "set-file-input": return await handleSetFileInput(cmd, workspace); - case "insert-text": return await handleInsertText(cmd, workspace); - case "bind-current": return await handleBindCurrent(cmd, workspace); - case "network-capture-start": return await handleNetworkCaptureStart(cmd, workspace); - case "network-capture-read": return await handleNetworkCaptureRead(cmd, workspace); - default: return { - id: cmd.id, - ok: false, - error: `Unknown action: ${cmd.action}` - }; - } - } catch (err) { - return { - id: cmd.id, - ok: false, - error: err instanceof Error ? err.message : String(err) - }; - } + const workspace = getWorkspaceKey(cmd.workspace); + windowFocused = cmd.windowFocused === true; + if (cmd.idleTimeout != null && cmd.idleTimeout > 0) { + workspaceTimeoutOverrides.set(workspace, cmd.idleTimeout * 1e3); + } + resetWindowIdleTimer(workspace); + try { + switch (cmd.action) { + case "exec": + return await handleExec(cmd, workspace); + case "navigate": + return await handleNavigate(cmd, workspace); + case "tabs": + return await handleTabs(cmd, workspace); + case "cookies": + return await handleCookies(cmd); + case "screenshot": + return await handleScreenshot(cmd, workspace); + case "close-window": + return await handleCloseWindow(cmd, workspace); + case "cdp": + return await handleCdp(cmd, workspace); + case "sessions": + return await handleSessions(cmd); + case "set-file-input": + return await handleSetFileInput(cmd, workspace); + case "insert-text": + return await handleInsertText(cmd, workspace); + case "bind-current": + return await handleBindCurrent(cmd, workspace); + case "network-capture-start": + return await handleNetworkCaptureStart(cmd, workspace); + case "network-capture-read": + return await handleNetworkCaptureRead(cmd, workspace); + case "frames": + return await handleFrames(cmd, workspace); + default: + return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` }; + } + } catch (err) { + return { + id: cmd.id, + ok: false, + error: err instanceof Error ? err.message : String(err) + }; + } } -/** Internal blank page used when no user URL is provided. */ -var BLANK_PAGE = "about:blank"; -/** Check if a URL can be attached via CDP — only allow http(s) and blank pages. */ +const BLANK_PAGE = "about:blank"; function isDebuggableUrl(url) { - if (!url) return true; - return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:"); + if (!url) return true; + return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:"); } -/** Check if a URL is safe for user-facing navigation (http/https only). */ function isSafeNavigationUrl(url) { - return url.startsWith("http://") || url.startsWith("https://"); + return url.startsWith("http://") || url.startsWith("https://"); } -/** Minimal URL normalization for same-page comparison: root slash + default port only. */ function normalizeUrlForComparison(url) { - if (!url) return ""; - try { - const parsed = new URL(url); - if (parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80") parsed.port = ""; - const pathname = parsed.pathname === "/" ? "" : parsed.pathname; - return `${parsed.protocol}//${parsed.host}${pathname}${parsed.search}${parsed.hash}`; - } catch { - return url; - } + if (!url) return ""; + try { + const parsed = new URL(url); + if (parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80") { + parsed.port = ""; + } + const pathname = parsed.pathname === "/" ? "" : parsed.pathname; + return `${parsed.protocol}//${parsed.host}${pathname}${parsed.search}${parsed.hash}`; + } catch { + return url; + } } function isTargetUrl(currentUrl, targetUrl) { - return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl); + return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl); } function matchesDomain(url, domain) { - if (!url) return false; - try { - const parsed = new URL(url); - return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`); - } catch { - return false; - } + if (!url) return false; + try { + const parsed = new URL(url); + return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`); + } catch { + return false; + } } function matchesBindCriteria(tab, cmd) { - if (!tab.id || !isDebuggableUrl(tab.url)) return false; - if (cmd.matchDomain && !matchesDomain(tab.url, cmd.matchDomain)) return false; - if (cmd.matchPathPrefix) try { - if (!new URL(tab.url).pathname.startsWith(cmd.matchPathPrefix)) return false; - } catch { - return false; - } - return true; + if (!tab.id || !isDebuggableUrl(tab.url)) return false; + if (cmd.matchDomain && !matchesDomain(tab.url, cmd.matchDomain)) return false; + if (cmd.matchPathPrefix) { + try { + const parsed = new URL(tab.url); + if (!parsed.pathname.startsWith(cmd.matchPathPrefix)) return false; + } catch { + return false; + } + } + return true; +} +function getUrlOrigin(url) { + if (!url) return null; + try { + return new URL(url).origin; + } catch { + return null; + } +} +function enumerateCrossOriginFrames(tree) { + const frames = []; + function collect(node, accessibleOrigin) { + for (const child of node.childFrames || []) { + const frame = child.frame; + const frameUrl = frame.url || frame.unreachableUrl || ""; + const frameOrigin = getUrlOrigin(frameUrl); + if (accessibleOrigin && frameOrigin && frameOrigin === accessibleOrigin) { + collect(child, frameOrigin); + continue; + } + frames.push({ + index: frames.length, + frameId: frame.id, + url: frameUrl, + name: frame.name || "" + }); + } + } + const rootFrame = tree?.frameTree?.frame; + const rootUrl = rootFrame?.url || rootFrame?.unreachableUrl || ""; + collect(tree.frameTree, getUrlOrigin(rootUrl)); + return frames; } function setWorkspaceSession(workspace, session) { - const existing = automationSessions.get(workspace); - if (existing?.idleTimer) clearTimeout(existing.idleTimer); - automationSessions.set(workspace, { - ...session, - idleTimer: null, - idleDeadlineAt: Date.now() + getIdleTimeout(workspace) - }); + const existing = automationSessions.get(workspace); + if (existing?.idleTimer) clearTimeout(existing.idleTimer); + automationSessions.set(workspace, { + ...session, + idleTimer: null, + idleDeadlineAt: Date.now() + getIdleTimeout(workspace) + }); } -/** -* Resolve tabId from command's page (targetId). -* Returns undefined if no page identity is provided. -*/ async function resolveCommandTabId(cmd) { - if (cmd.page) return resolveTabId$1(cmd.page); + if (cmd.page) return resolveTabId$1(cmd.page); + return void 0; } -/** -* Resolve target tab in the automation window, returning both the tabId and -* the Tab object (when available) so callers can skip a redundant chrome.tabs.get(). -*/ async function resolveTab(tabId, workspace, initialUrl) { - if (tabId !== void 0) try { - const tab = await chrome.tabs.get(tabId); - const session = automationSessions.get(workspace); - const matchesSession = session ? session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId : false; - if (isDebuggableUrl(tab.url) && matchesSession) return { - tabId, - tab - }; - if (session && !matchesSession && session.preferredTabId === null && isDebuggableUrl(tab.url)) { - console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId}, moving back to ${session.windowId}`); - try { - await chrome.tabs.move(tabId, { - windowId: session.windowId, - index: -1 - }); - const moved = await chrome.tabs.get(tabId); - if (moved.windowId === session.windowId && isDebuggableUrl(moved.url)) return { - tabId, - tab: moved - }; - } catch (moveErr) { - console.warn(`[opencli] Failed to move tab back: ${moveErr}`); - } - } else if (!isDebuggableUrl(tab.url)) console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`); - } catch { - console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`); - } - const existingSession = automationSessions.get(workspace); - if (existingSession?.preferredTabId !== null) try { - const preferredTab = await chrome.tabs.get(existingSession.preferredTabId); - if (isDebuggableUrl(preferredTab.url)) return { - tabId: preferredTab.id, - tab: preferredTab - }; - } catch { - automationSessions.delete(workspace); - } - const windowId = await getAutomationWindow(workspace, initialUrl); - const tabs = await chrome.tabs.query({ windowId }); - const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url)); - if (debuggableTab?.id) return { - tabId: debuggableTab.id, - tab: debuggableTab - }; - const reuseTab = tabs.find((t) => t.id); - if (reuseTab?.id) { - await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE }); - await new Promise((resolve) => setTimeout(resolve, 300)); - try { - const updated = await chrome.tabs.get(reuseTab.id); - if (isDebuggableUrl(updated.url)) return { - tabId: reuseTab.id, - tab: updated - }; - console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`); - } catch {} - } - const newTab = await chrome.tabs.create({ - windowId, - url: BLANK_PAGE, - active: true - }); - if (!newTab.id) throw new Error("Failed to create tab in automation window"); - return { - tabId: newTab.id, - tab: newTab - }; + if (tabId !== void 0) { + try { + const tab = await chrome.tabs.get(tabId); + const session = automationSessions.get(workspace); + const matchesSession = session ? session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId : false; + if (isDebuggableUrl(tab.url) && matchesSession) return { tabId, tab }; + if (session && !matchesSession && session.preferredTabId === null && isDebuggableUrl(tab.url)) { + console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId}, moving back to ${session.windowId}`); + try { + await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 }); + const moved = await chrome.tabs.get(tabId); + if (moved.windowId === session.windowId && isDebuggableUrl(moved.url)) { + return { tabId, tab: moved }; + } + } catch (moveErr) { + console.warn(`[opencli] Failed to move tab back: ${moveErr}`); + } + } else if (!isDebuggableUrl(tab.url)) { + console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`); + } + } catch { + console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`); + } + } + const existingSession = automationSessions.get(workspace); + if (existingSession?.preferredTabId !== null) { + try { + const preferredTab = await chrome.tabs.get(existingSession.preferredTabId); + if (isDebuggableUrl(preferredTab.url)) return { tabId: preferredTab.id, tab: preferredTab }; + } catch { + automationSessions.delete(workspace); + } + } + const windowId = await getAutomationWindow(workspace, initialUrl); + const tabs = await chrome.tabs.query({ windowId }); + const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url)); + if (debuggableTab?.id) return { tabId: debuggableTab.id, tab: debuggableTab }; + const reuseTab = tabs.find((t) => t.id); + if (reuseTab?.id) { + await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE }); + await new Promise((resolve) => setTimeout(resolve, 300)); + try { + const updated = await chrome.tabs.get(reuseTab.id); + if (isDebuggableUrl(updated.url)) return { tabId: reuseTab.id, tab: updated }; + console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`); + } catch { + } + } + const newTab = await chrome.tabs.create({ windowId, url: BLANK_PAGE, active: true }); + if (!newTab.id) throw new Error("Failed to create tab in automation window"); + return { tabId: newTab.id, tab: newTab }; } -/** Build a page-scoped success result with targetId resolved from tabId */ async function pageScopedResult(id, tabId, data) { - return { - id, - ok: true, - data, - page: await resolveTargetId(tabId) - }; + const page = await resolveTargetId(tabId); + return { id, ok: true, data, page }; } -/** Convenience wrapper returning just the tabId (used by most handlers) */ async function resolveTabId(tabId, workspace, initialUrl) { - return (await resolveTab(tabId, workspace, initialUrl)).tabId; + const resolved = await resolveTab(tabId, workspace, initialUrl); + return resolved.tabId; } async function listAutomationTabs(workspace) { - const session = automationSessions.get(workspace); - if (!session) return []; - if (session.preferredTabId !== null) try { - return [await chrome.tabs.get(session.preferredTabId)]; - } catch { - automationSessions.delete(workspace); - return []; - } - try { - return await chrome.tabs.query({ windowId: session.windowId }); - } catch { - automationSessions.delete(workspace); - return []; - } + const session = automationSessions.get(workspace); + if (!session) return []; + if (session.preferredTabId !== null) { + try { + return [await chrome.tabs.get(session.preferredTabId)]; + } catch { + automationSessions.delete(workspace); + return []; + } + } + try { + return await chrome.tabs.query({ windowId: session.windowId }); + } catch { + automationSessions.delete(workspace); + return []; + } } async function listAutomationWebTabs(workspace) { - return (await listAutomationTabs(workspace)).filter((tab) => isDebuggableUrl(tab.url)); + const tabs = await listAutomationTabs(workspace); + return tabs.filter((tab) => isDebuggableUrl(tab.url)); } async function handleExec(cmd, workspace) { - if (!cmd.code) return { - id: cmd.id, - ok: false, - error: "Missing code" - }; - const tabId = await resolveTabId(await resolveCommandTabId(cmd), workspace); - try { - const aggressive = workspace.startsWith("browser:") || workspace.startsWith("operate:"); - const data = await evaluateAsync(tabId, cmd.code, aggressive); - return pageScopedResult(cmd.id, tabId, data); - } catch (err) { - return { - id: cmd.id, - ok: false, - error: err instanceof Error ? err.message : String(err) - }; - } + if (!cmd.code) return { id: cmd.id, ok: false, error: "Missing code" }; + const cmdTabId = await resolveCommandTabId(cmd); + const tabId = await resolveTabId(cmdTabId, workspace); + try { + const aggressive = workspace.startsWith("browser:") || workspace.startsWith("operate:"); + if (cmd.frameIndex != null) { + const tree = await getFrameTree(tabId); + const frames = enumerateCrossOriginFrames(tree); + if (cmd.frameIndex < 0 || cmd.frameIndex >= frames.length) { + return { id: cmd.id, ok: false, error: `Frame index ${cmd.frameIndex} out of range (${frames.length} cross-origin frames available)` }; + } + const data2 = await evaluateInFrame(tabId, cmd.code, frames[cmd.frameIndex].frameId, aggressive); + return pageScopedResult(cmd.id, tabId, data2); + } + const data = await evaluateAsync(tabId, cmd.code, aggressive); + return pageScopedResult(cmd.id, tabId, data); + } catch (err) { + return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; + } +} +async function handleFrames(cmd, workspace) { + const cmdTabId = await resolveCommandTabId(cmd); + const tabId = await resolveTabId(cmdTabId, workspace); + try { + const tree = await getFrameTree(tabId); + return { id: cmd.id, ok: true, data: enumerateCrossOriginFrames(tree) }; + } catch (err) { + return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; + } } async function handleNavigate(cmd, workspace) { - if (!cmd.url) return { - id: cmd.id, - ok: false, - error: "Missing url" - }; - if (!isSafeNavigationUrl(cmd.url)) return { - id: cmd.id, - ok: false, - error: "Blocked URL scheme -- only http:// and https:// are allowed" - }; - const resolved = await resolveTab(await resolveCommandTabId(cmd), workspace, cmd.url); - const tabId = resolved.tabId; - const beforeTab = resolved.tab ?? await chrome.tabs.get(tabId); - const beforeNormalized = normalizeUrlForComparison(beforeTab.url); - const targetUrl = cmd.url; - if (beforeTab.status === "complete" && isTargetUrl(beforeTab.url, targetUrl)) return pageScopedResult(cmd.id, tabId, { - title: beforeTab.title, - url: beforeTab.url, - timedOut: false - }); - if (!hasActiveNetworkCapture(tabId)) await detach(tabId); - await chrome.tabs.update(tabId, { url: targetUrl }); - let timedOut = false; - await new Promise((resolve) => { - let settled = false; - let checkTimer = null; - let timeoutTimer = null; - const finish = () => { - if (settled) return; - settled = true; - chrome.tabs.onUpdated.removeListener(listener); - if (checkTimer) clearTimeout(checkTimer); - if (timeoutTimer) clearTimeout(timeoutTimer); - resolve(); - }; - const isNavigationDone = (url) => { - return isTargetUrl(url, targetUrl) || normalizeUrlForComparison(url) !== beforeNormalized; - }; - const listener = (id, info, tab) => { - if (id !== tabId) return; - if (info.status === "complete" && isNavigationDone(tab.url ?? info.url)) finish(); - }; - chrome.tabs.onUpdated.addListener(listener); - checkTimer = setTimeout(async () => { - try { - const currentTab = await chrome.tabs.get(tabId); - if (currentTab.status === "complete" && isNavigationDone(currentTab.url)) finish(); - } catch {} - }, 100); - timeoutTimer = setTimeout(() => { - timedOut = true; - console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`); - finish(); - }, 15e3); - }); - let tab = await chrome.tabs.get(tabId); - const session = automationSessions.get(workspace); - if (session && tab.windowId !== session.windowId) { - console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId} during navigation, moving back to ${session.windowId}`); - try { - await chrome.tabs.move(tabId, { - windowId: session.windowId, - index: -1 - }); - tab = await chrome.tabs.get(tabId); - } catch (moveErr) { - console.warn(`[opencli] Failed to recover drifted tab: ${moveErr}`); - } - } - return pageScopedResult(cmd.id, tabId, { - title: tab.title, - url: tab.url, - timedOut - }); + if (!cmd.url) return { id: cmd.id, ok: false, error: "Missing url" }; + if (!isSafeNavigationUrl(cmd.url)) { + return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" }; + } + const cmdTabId = await resolveCommandTabId(cmd); + const resolved = await resolveTab(cmdTabId, workspace, cmd.url); + const tabId = resolved.tabId; + const beforeTab = resolved.tab ?? await chrome.tabs.get(tabId); + const beforeNormalized = normalizeUrlForComparison(beforeTab.url); + const targetUrl = cmd.url; + if (beforeTab.status === "complete" && isTargetUrl(beforeTab.url, targetUrl)) { + return pageScopedResult(cmd.id, tabId, { title: beforeTab.title, url: beforeTab.url, timedOut: false }); + } + if (!hasActiveNetworkCapture(tabId)) { + await detach(tabId); + } + await chrome.tabs.update(tabId, { url: targetUrl }); + let timedOut = false; + await new Promise((resolve) => { + let settled = false; + let checkTimer = null; + let timeoutTimer = null; + const finish = () => { + if (settled) return; + settled = true; + chrome.tabs.onUpdated.removeListener(listener); + if (checkTimer) clearTimeout(checkTimer); + if (timeoutTimer) clearTimeout(timeoutTimer); + resolve(); + }; + const isNavigationDone = (url) => { + return isTargetUrl(url, targetUrl) || normalizeUrlForComparison(url) !== beforeNormalized; + }; + const listener = (id, info, tab2) => { + if (id !== tabId) return; + if (info.status === "complete" && isNavigationDone(tab2.url ?? info.url)) { + finish(); + } + }; + chrome.tabs.onUpdated.addListener(listener); + checkTimer = setTimeout(async () => { + try { + const currentTab = await chrome.tabs.get(tabId); + if (currentTab.status === "complete" && isNavigationDone(currentTab.url)) { + finish(); + } + } catch { + } + }, 100); + timeoutTimer = setTimeout(() => { + timedOut = true; + console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`); + finish(); + }, 15e3); + }); + let tab = await chrome.tabs.get(tabId); + const session = automationSessions.get(workspace); + if (session && tab.windowId !== session.windowId) { + console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId} during navigation, moving back to ${session.windowId}`); + try { + await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 }); + tab = await chrome.tabs.get(tabId); + } catch (moveErr) { + console.warn(`[opencli] Failed to recover drifted tab: ${moveErr}`); + } + } + return pageScopedResult(cmd.id, tabId, { title: tab.title, url: tab.url, timedOut }); } async function handleTabs(cmd, workspace) { - switch (cmd.op) { - case "list": { - const tabs = await listAutomationWebTabs(workspace); - const data = await Promise.all(tabs.map(async (t, i) => { - let page; - try { - page = t.id ? await resolveTargetId(t.id) : void 0; - } catch {} - return { - index: i, - page, - url: t.url, - title: t.title, - active: t.active - }; - })); - return { - id: cmd.id, - ok: true, - data - }; - } - case "new": { - if (cmd.url && !isSafeNavigationUrl(cmd.url)) return { - id: cmd.id, - ok: false, - error: "Blocked URL scheme -- only http:// and https:// are allowed" - }; - const windowId = await getAutomationWindow(workspace); - const tab = await chrome.tabs.create({ - windowId, - url: cmd.url ?? BLANK_PAGE, - active: true - }); - if (!tab.id) return { - id: cmd.id, - ok: false, - error: "Failed to create tab" - }; - return pageScopedResult(cmd.id, tab.id, { url: tab.url }); - } - case "close": { - if (cmd.index !== void 0) { - const target = (await listAutomationWebTabs(workspace))[cmd.index]; - if (!target?.id) return { - id: cmd.id, - ok: false, - error: `Tab index ${cmd.index} not found` - }; - const closedPage = await resolveTargetId(target.id).catch(() => void 0); - await chrome.tabs.remove(target.id); - await detach(target.id); - return { - id: cmd.id, - ok: true, - data: { closed: closedPage } - }; - } - const tabId = await resolveTabId(await resolveCommandTabId(cmd), workspace); - const closedPage = await resolveTargetId(tabId).catch(() => void 0); - await chrome.tabs.remove(tabId); - await detach(tabId); - return { - id: cmd.id, - ok: true, - data: { closed: closedPage } - }; - } - case "select": { - if (cmd.index === void 0 && cmd.page === void 0) return { - id: cmd.id, - ok: false, - error: "Missing index or page" - }; - const cmdTabId = await resolveCommandTabId(cmd); - if (cmdTabId !== void 0) { - const session = automationSessions.get(workspace); - let tab; - try { - tab = await chrome.tabs.get(cmdTabId); - } catch { - return { - id: cmd.id, - ok: false, - error: `Page no longer exists` - }; - } - if (!session || tab.windowId !== session.windowId) return { - id: cmd.id, - ok: false, - error: `Page is not in the automation window` - }; - await chrome.tabs.update(cmdTabId, { active: true }); - return pageScopedResult(cmd.id, cmdTabId, { selected: true }); - } - const target = (await listAutomationWebTabs(workspace))[cmd.index]; - if (!target?.id) return { - id: cmd.id, - ok: false, - error: `Tab index ${cmd.index} not found` - }; - await chrome.tabs.update(target.id, { active: true }); - return pageScopedResult(cmd.id, target.id, { selected: true }); - } - default: return { - id: cmd.id, - ok: false, - error: `Unknown tabs op: ${cmd.op}` - }; - } + switch (cmd.op) { + case "list": { + const tabs = await listAutomationWebTabs(workspace); + const data = await Promise.all(tabs.map(async (t, i) => { + let page; + try { + page = t.id ? await resolveTargetId(t.id) : void 0; + } catch { + } + return { index: i, page, url: t.url, title: t.title, active: t.active }; + })); + return { id: cmd.id, ok: true, data }; + } + case "new": { + if (cmd.url && !isSafeNavigationUrl(cmd.url)) { + return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" }; + } + const windowId = await getAutomationWindow(workspace); + const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? BLANK_PAGE, active: true }); + if (!tab.id) return { id: cmd.id, ok: false, error: "Failed to create tab" }; + return pageScopedResult(cmd.id, tab.id, { url: tab.url }); + } + case "close": { + if (cmd.index !== void 0) { + const tabs = await listAutomationWebTabs(workspace); + const target = tabs[cmd.index]; + if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` }; + const closedPage2 = await resolveTargetId(target.id).catch(() => void 0); + await chrome.tabs.remove(target.id); + await detach(target.id); + return { id: cmd.id, ok: true, data: { closed: closedPage2 } }; + } + const cmdTabId = await resolveCommandTabId(cmd); + const tabId = await resolveTabId(cmdTabId, workspace); + const closedPage = await resolveTargetId(tabId).catch(() => void 0); + await chrome.tabs.remove(tabId); + await detach(tabId); + return { id: cmd.id, ok: true, data: { closed: closedPage } }; + } + case "select": { + if (cmd.index === void 0 && cmd.page === void 0) + return { id: cmd.id, ok: false, error: "Missing index or page" }; + const cmdTabId = await resolveCommandTabId(cmd); + if (cmdTabId !== void 0) { + const session = automationSessions.get(workspace); + let tab; + try { + tab = await chrome.tabs.get(cmdTabId); + } catch { + return { id: cmd.id, ok: false, error: `Page no longer exists` }; + } + if (!session || tab.windowId !== session.windowId) { + return { id: cmd.id, ok: false, error: `Page is not in the automation window` }; + } + await chrome.tabs.update(cmdTabId, { active: true }); + return pageScopedResult(cmd.id, cmdTabId, { selected: true }); + } + const tabs = await listAutomationWebTabs(workspace); + const target = tabs[cmd.index]; + if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` }; + await chrome.tabs.update(target.id, { active: true }); + return pageScopedResult(cmd.id, target.id, { selected: true }); + } + default: + return { id: cmd.id, ok: false, error: `Unknown tabs op: ${cmd.op}` }; + } } async function handleCookies(cmd) { - if (!cmd.domain && !cmd.url) return { - id: cmd.id, - ok: false, - error: "Cookie scope required: provide domain or url to avoid dumping all cookies" - }; - const details = {}; - if (cmd.domain) details.domain = cmd.domain; - if (cmd.url) details.url = cmd.url; - const data = (await chrome.cookies.getAll(details)).map((c) => ({ - name: c.name, - value: c.value, - domain: c.domain, - path: c.path, - secure: c.secure, - httpOnly: c.httpOnly, - expirationDate: c.expirationDate - })); - return { - id: cmd.id, - ok: true, - data - }; + if (!cmd.domain && !cmd.url) { + return { id: cmd.id, ok: false, error: "Cookie scope required: provide domain or url to avoid dumping all cookies" }; + } + const details = {}; + if (cmd.domain) details.domain = cmd.domain; + if (cmd.url) details.url = cmd.url; + const cookies = await chrome.cookies.getAll(details); + const data = cookies.map((c) => ({ + name: c.name, + value: c.value, + domain: c.domain, + path: c.path, + secure: c.secure, + httpOnly: c.httpOnly, + expirationDate: c.expirationDate + })); + return { id: cmd.id, ok: true, data }; } async function handleScreenshot(cmd, workspace) { - const tabId = await resolveTabId(await resolveCommandTabId(cmd), workspace); - try { - const data = await screenshot(tabId, { - format: cmd.format, - quality: cmd.quality, - fullPage: cmd.fullPage - }); - return pageScopedResult(cmd.id, tabId, data); - } catch (err) { - return { - id: cmd.id, - ok: false, - error: err instanceof Error ? err.message : String(err) - }; - } + const cmdTabId = await resolveCommandTabId(cmd); + const tabId = await resolveTabId(cmdTabId, workspace); + try { + const data = await screenshot(tabId, { + format: cmd.format, + quality: cmd.quality, + fullPage: cmd.fullPage + }); + return pageScopedResult(cmd.id, tabId, data); + } catch (err) { + return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; + } } -/** CDP methods permitted via the 'cdp' passthrough action. */ -var CDP_ALLOWLIST = new Set([ - "Accessibility.getFullAXTree", - "DOM.getDocument", - "DOM.getBoxModel", - "DOM.getContentQuads", - "DOM.querySelectorAll", - "DOM.scrollIntoViewIfNeeded", - "DOMSnapshot.captureSnapshot", - "Input.dispatchMouseEvent", - "Input.dispatchKeyEvent", - "Input.insertText", - "Page.getLayoutMetrics", - "Page.captureScreenshot", - "Runtime.enable", - "Emulation.setDeviceMetricsOverride", - "Emulation.clearDeviceMetricsOverride" +const CDP_ALLOWLIST = /* @__PURE__ */ new Set([ + // Agent DOM context + "Accessibility.getFullAXTree", + "DOM.getDocument", + "DOM.getBoxModel", + "DOM.getContentQuads", + "DOM.querySelectorAll", + "DOM.scrollIntoViewIfNeeded", + "DOMSnapshot.captureSnapshot", + // Native input events + "Input.dispatchMouseEvent", + "Input.dispatchKeyEvent", + "Input.insertText", + // Page metrics & screenshots + "Page.getLayoutMetrics", + "Page.captureScreenshot", + "Page.getFrameTree", + // Runtime.enable needed for CDP attach setup (Runtime.evaluate goes through 'exec' action) + "Runtime.enable", + // Emulation (used by screenshot full-page) + "Emulation.setDeviceMetricsOverride", + "Emulation.clearDeviceMetricsOverride" ]); async function handleCdp(cmd, workspace) { - if (!cmd.cdpMethod) return { - id: cmd.id, - ok: false, - error: "Missing cdpMethod" - }; - if (!CDP_ALLOWLIST.has(cmd.cdpMethod)) return { - id: cmd.id, - ok: false, - error: `CDP method not permitted: ${cmd.cdpMethod}` - }; - const tabId = await resolveTabId(await resolveCommandTabId(cmd), workspace); - try { - await ensureAttached(tabId, workspace.startsWith("browser:") || workspace.startsWith("operate:")); - const data = await chrome.debugger.sendCommand({ tabId }, cmd.cdpMethod, cmd.cdpParams ?? {}); - return pageScopedResult(cmd.id, tabId, data); - } catch (err) { - return { - id: cmd.id, - ok: false, - error: err instanceof Error ? err.message : String(err) - }; - } + if (!cmd.cdpMethod) return { id: cmd.id, ok: false, error: "Missing cdpMethod" }; + if (!CDP_ALLOWLIST.has(cmd.cdpMethod)) { + return { id: cmd.id, ok: false, error: `CDP method not permitted: ${cmd.cdpMethod}` }; + } + const cmdTabId = await resolveCommandTabId(cmd); + const tabId = await resolveTabId(cmdTabId, workspace); + try { + const aggressive = workspace.startsWith("browser:") || workspace.startsWith("operate:"); + await ensureAttached(tabId, aggressive); + const data = await chrome.debugger.sendCommand( + { tabId }, + cmd.cdpMethod, + cmd.cdpParams ?? {} + ); + return pageScopedResult(cmd.id, tabId, data); + } catch (err) { + return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; + } } async function handleCloseWindow(cmd, workspace) { - const session = automationSessions.get(workspace); - if (session) { - if (session.owned) try { - await chrome.windows.remove(session.windowId); - } catch {} - if (session.idleTimer) clearTimeout(session.idleTimer); - workspaceTimeoutOverrides.delete(workspace); - automationSessions.delete(workspace); - } - return { - id: cmd.id, - ok: true, - data: { closed: true } - }; + const session = automationSessions.get(workspace); + if (session) { + if (session.owned) { + try { + await chrome.windows.remove(session.windowId); + } catch { + } + } + if (session.idleTimer) clearTimeout(session.idleTimer); + workspaceTimeoutOverrides.delete(workspace); + automationSessions.delete(workspace); + } + return { id: cmd.id, ok: true, data: { closed: true } }; } async function handleSetFileInput(cmd, workspace) { - if (!cmd.files || !Array.isArray(cmd.files) || cmd.files.length === 0) return { - id: cmd.id, - ok: false, - error: "Missing or empty files array" - }; - const tabId = await resolveTabId(await resolveCommandTabId(cmd), workspace); - try { - await setFileInputFiles(tabId, cmd.files, cmd.selector); - return pageScopedResult(cmd.id, tabId, { count: cmd.files.length }); - } catch (err) { - return { - id: cmd.id, - ok: false, - error: err instanceof Error ? err.message : String(err) - }; - } + if (!cmd.files || !Array.isArray(cmd.files) || cmd.files.length === 0) { + return { id: cmd.id, ok: false, error: "Missing or empty files array" }; + } + const cmdTabId = await resolveCommandTabId(cmd); + const tabId = await resolveTabId(cmdTabId, workspace); + try { + await setFileInputFiles(tabId, cmd.files, cmd.selector); + return pageScopedResult(cmd.id, tabId, { count: cmd.files.length }); + } catch (err) { + return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; + } } async function handleInsertText(cmd, workspace) { - if (typeof cmd.text !== "string") return { - id: cmd.id, - ok: false, - error: "Missing text payload" - }; - const tabId = await resolveTabId(await resolveCommandTabId(cmd), workspace); - try { - await insertText(tabId, cmd.text); - return pageScopedResult(cmd.id, tabId, { inserted: true }); - } catch (err) { - return { - id: cmd.id, - ok: false, - error: err instanceof Error ? err.message : String(err) - }; - } + if (typeof cmd.text !== "string") { + return { id: cmd.id, ok: false, error: "Missing text payload" }; + } + const cmdTabId = await resolveCommandTabId(cmd); + const tabId = await resolveTabId(cmdTabId, workspace); + try { + await insertText(tabId, cmd.text); + return pageScopedResult(cmd.id, tabId, { inserted: true }); + } catch (err) { + return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; + } } async function handleNetworkCaptureStart(cmd, workspace) { - const tabId = await resolveTabId(await resolveCommandTabId(cmd), workspace); - try { - await startNetworkCapture(tabId, cmd.pattern); - return pageScopedResult(cmd.id, tabId, { started: true }); - } catch (err) { - return { - id: cmd.id, - ok: false, - error: err instanceof Error ? err.message : String(err) - }; - } + const cmdTabId = await resolveCommandTabId(cmd); + const tabId = await resolveTabId(cmdTabId, workspace); + try { + await startNetworkCapture(tabId, cmd.pattern); + return pageScopedResult(cmd.id, tabId, { started: true }); + } catch (err) { + return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; + } } async function handleNetworkCaptureRead(cmd, workspace) { - const tabId = await resolveTabId(await resolveCommandTabId(cmd), workspace); - try { - const data = await readNetworkCapture(tabId); - return pageScopedResult(cmd.id, tabId, data); - } catch (err) { - return { - id: cmd.id, - ok: false, - error: err instanceof Error ? err.message : String(err) - }; - } + const cmdTabId = await resolveCommandTabId(cmd); + const tabId = await resolveTabId(cmdTabId, workspace); + try { + const data = await readNetworkCapture(tabId); + return pageScopedResult(cmd.id, tabId, data); + } catch (err) { + return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; + } } async function handleSessions(cmd) { - const now = Date.now(); - const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({ - workspace, - windowId: session.windowId, - tabCount: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length, - idleMsRemaining: Math.max(0, session.idleDeadlineAt - now) - }))); - return { - id: cmd.id, - ok: true, - data - }; + const now = Date.now(); + const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({ + workspace, + windowId: session.windowId, + tabCount: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length, + idleMsRemaining: Math.max(0, session.idleDeadlineAt - now) + }))); + return { id: cmd.id, ok: true, data }; } async function handleBindCurrent(cmd, workspace) { - const activeTabs = await chrome.tabs.query({ - active: true, - lastFocusedWindow: true - }); - const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true }); - const allTabs = await chrome.tabs.query({}); - const boundTab = activeTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? fallbackTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? allTabs.find((tab) => matchesBindCriteria(tab, cmd)); - if (!boundTab?.id) return { - id: cmd.id, - ok: false, - error: cmd.matchDomain || cmd.matchPathPrefix ? `No visible tab matching ${cmd.matchDomain ?? "domain"}${cmd.matchPathPrefix ? ` ${cmd.matchPathPrefix}` : ""}` : "No active debuggable tab found" - }; - setWorkspaceSession(workspace, { - windowId: boundTab.windowId, - owned: false, - preferredTabId: boundTab.id - }); - resetWindowIdleTimer(workspace); - console.log(`[opencli] Workspace ${workspace} explicitly bound to tab ${boundTab.id} (${boundTab.url})`); - return pageScopedResult(cmd.id, boundTab.id, { - url: boundTab.url, - title: boundTab.title, - workspace - }); + const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true }); + const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true }); + const allTabs = await chrome.tabs.query({}); + const boundTab = activeTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? fallbackTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? allTabs.find((tab) => matchesBindCriteria(tab, cmd)); + if (!boundTab?.id) { + return { + id: cmd.id, + ok: false, + error: cmd.matchDomain || cmd.matchPathPrefix ? `No visible tab matching ${cmd.matchDomain ?? "domain"}${cmd.matchPathPrefix ? ` ${cmd.matchPathPrefix}` : ""}` : "No active debuggable tab found" + }; + } + setWorkspaceSession(workspace, { + windowId: boundTab.windowId, + owned: false, + preferredTabId: boundTab.id + }); + resetWindowIdleTimer(workspace); + console.log(`[opencli] Workspace ${workspace} explicitly bound to tab ${boundTab.id} (${boundTab.url})`); + return pageScopedResult(cmd.id, boundTab.id, { + url: boundTab.url, + title: boundTab.title, + workspace + }); } -//#endregion diff --git a/extension/manifest.json b/extension/manifest.json index d8190dfe9..8d3c22f66 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -8,7 +8,8 @@ "tabs", "cookies", "activeTab", - "alarms" + "alarms", + "storage" ], "host_permissions": [ "" diff --git a/extension/popup.html b/extension/popup.html index 02ca1b972..acb01cfd9 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -5,7 +5,7 @@