diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index 5df8763..7592895 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -2832,6 +2832,7 @@ function RoomBoardV2({ const [filter, setFilter] = useState('all'); const [sort, setSort] = useState('recent'); const [drafts, setDrafts] = useState>({}); + const [mobileDetailOpen, setMobileDetailOpen] = useState(false); const allEntries: RoomEntryWithService[] = snapshots.flatMap((snapshot) => snapshot.entries.map((entry) => ({ @@ -2869,7 +2870,10 @@ function RoomBoardV2({ useEffect(() => { const nextJid = selectedEntry?.jid ?? null; - if (nextJid !== selectedJid) onSelectedJidChange(nextJid); + if (nextJid !== selectedJid) { + onSelectedJidChange(nextJid); + setMobileDetailOpen(false); + } }, [onSelectedJidChange, selectedEntry?.jid, selectedJid]); if (allEntries.length === 0) { @@ -2937,7 +2941,9 @@ function RoomBoardV2({ {sorted.length === 0 ? ( {t.rooms.empty} ) : ( -
+
+ {selectedEntry ? ( =8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="], + + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], + + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], + + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], @@ -236,12 +266,16 @@ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], @@ -256,6 +290,26 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/type-utils": "8.59.0", "@typescript-eslint/utils": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.0", "@typescript-eslint/types": "^8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0" } }, "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/utils": "8.59.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.0", "@typescript-eslint/tsconfig-utils": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], @@ -280,7 +334,11 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -292,6 +350,8 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.8.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ=="], @@ -302,6 +362,8 @@ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], @@ -356,6 +418,8 @@ "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -390,12 +454,28 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="], + + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], @@ -416,16 +496,28 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -444,6 +536,10 @@ "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -472,6 +568,10 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], @@ -488,6 +588,10 @@ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], @@ -508,10 +612,18 @@ "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "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=="], @@ -536,6 +648,8 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], @@ -654,6 +768,8 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], @@ -664,6 +780,8 @@ "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], @@ -680,10 +798,18 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], @@ -712,6 +838,8 @@ "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "prettier": ["prettier@3.8.1", "", { "bin": "bin/prettier.cjs" }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], @@ -722,6 +850,8 @@ "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], @@ -842,6 +972,8 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -850,10 +982,14 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript-eslint": ["typescript-eslint@8.59.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.0", "@typescript-eslint/parser": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/utils": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw=="], + "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -872,6 +1008,8 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], @@ -888,12 +1026,16 @@ "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=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "yaml": ["yaml@2.8.2", "", { "bin": "bin.mjs" }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], @@ -904,6 +1046,16 @@ "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "pino-pretty/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], @@ -924,6 +1076,10 @@ "vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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": ["jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "vitest/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "vitest/vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..4ea3dbf --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,71 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: [ + 'dist/**', + 'node_modules/**', + 'apps/dashboard/dist/**', + 'runners/*/dist/**', + 'runners/*/node_modules/**', + 'store/**', + 'store-*/**', + 'data/**', + 'data-*/**', + 'coverage/**', + 'tmp/**', + 'cache/**', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{ts,tsx,js,jsx}'], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + ...globals.es2022, + Bun: 'readonly', + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-empty-object-type': 'warn', + complexity: ['warn', { max: 35 }], + 'max-depth': ['warn', 5], + 'max-lines-per-function': [ + 'warn', + { + max: 180, + skipBlankLines: true, + skipComments: true, + }, + ], + 'max-params': ['warn', 7], + 'no-case-declarations': 'warn', + 'no-console': 'off', + 'no-control-regex': 'off', + 'no-unsafe-finally': 'warn', + 'no-useless-assignment': 'warn', + 'no-useless-escape': 'warn', + 'preserve-caught-error': 'warn', + }, + }, + { + files: ['**/*.test.ts', '**/*.test.tsx', 'scripts/**/*.ts'], + rules: { + 'max-lines-per-function': 'off', + }, + }, +); diff --git a/package.json b/package.json index e70e16b..ba4c290 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,13 @@ "test": "vitest run", "typecheck": "tsc --noEmit", "typecheck:all": "bun run install:runners && bunx tsc --noEmit -p tsconfig.check.json && bunx tsc --noEmit -p apps/dashboard/tsconfig.json && bunx tsc --noEmit -p runners/shared/tsconfig.json && bunx tsc --noEmit -p runners/agent-runner/tsconfig.json && bunx tsc --noEmit -p runners/codex-runner/tsconfig.json", - "format:fix": "prettier --write \"{src,setup,shared,test,scripts,apps}/**/*.{ts,tsx,js,jsx,css,html}\" \"runners/**/*.{ts,js}\" \"vitest*.ts\"", - "format:check": "prettier --check \"{src,setup,shared,test,scripts,apps}/**/*.{ts,tsx,js,jsx,css,html}\" \"runners/**/*.{ts,js}\" \"vitest*.ts\"", - "check": "bun run format:check && bun run typecheck:all && bun run test && bun run build:all", + "lint": "eslint \"{src,setup,scripts,apps/dashboard/src,runners}/**/*.{ts,tsx,js,jsx}\" \"vitest*.ts\" --quiet", + "lint:report": "eslint \"{src,setup,scripts,apps/dashboard/src,runners}/**/*.{ts,tsx,js,jsx}\" \"vitest*.ts\"", + "lint:complexity": "bun scripts/check-code-quality.ts", + "quality:check": "bun run lint && bun run lint:complexity", + "format:fix": "prettier --write \"{src,setup,shared,test,scripts,apps}/**/*.{ts,tsx,js,jsx,css,html}\" \"runners/**/*.{ts,js}\" \"vitest*.ts\" \"eslint.config.js\"", + "format:check": "prettier --check \"{src,setup,shared,test,scripts,apps}/**/*.{ts,tsx,js,jsx,css,html}\" \"runners/**/*.{ts,js}\" \"vitest*.ts\" \"eslint.config.js\"", + "check": "bun run format:check && bun run quality:check && bun run typecheck:all && bun run test && bun run build:all", "prepare": "husky", "setup": "bun setup/index.ts" }, @@ -43,6 +47,7 @@ "zod": "^4.3.6" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/better-sqlite3": "^7.6.13", "@types/bun": "^1.3.11", "@types/node": "^22.10.0", @@ -51,10 +56,13 @@ "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^4.0.18", "better-sqlite3": "^12.8.0", + "eslint": "^10.2.1", + "globals": "^17.5.0", "husky": "^9.1.7", "playwright": "^1.59.1", "prettier": "^3.8.1", "typescript": "^5.7.0", + "typescript-eslint": "^8.59.0", "vite": "^8.0.10", "vitest": "^4.0.18" }, diff --git a/quality/code-quality-budgets.json b/quality/code-quality-budgets.json new file mode 100644 index 0000000..ba76973 --- /dev/null +++ b/quality/code-quality-budgets.json @@ -0,0 +1,248 @@ +{ + "defaults": { + "maxLines": 900, + "maxFunctionLines": 180, + "maxComplexity": 40, + "maxNesting": 6 + }, + "testDefaults": { + "maxLines": 1600, + "maxFunctionLines": 260, + "maxComplexity": 55, + "maxNesting": 7 + }, + "overrides": { + "apps/dashboard/src/App.tsx": { + "maxLines": 4612, + "maxFunctionLines": 583, + "maxComplexity": 79, + "maxNesting": 4, + "owner": "dashboard shell hotspot" + }, + "apps/dashboard/src/i18n.ts": { + "maxLines": 1330, + "owner": "dashboard i18n hotspot" + }, + "apps/dashboard/src/styles.css": { + "maxLines": 4875, + "owner": "dashboard stylesheet hotspot" + }, + "runners/agent-runner/src/index.ts": { + "maxFunctionLines": 506, + "maxComplexity": 81, + "owner": "agent runner hotspot" + }, + "runners/agent-runner/test/reviewer-runtime.test.ts": { + "maxFunctionLines": 271, + "owner": "agent runner test hotspot" + }, + "runners/shared/test/agent-protocol.test.ts": { + "maxFunctionLines": 279, + "owner": "runner shared test hotspot" + }, + "setup/migrate-room-registrations.test.ts": { + "maxFunctionLines": 393, + "owner": "setup test hotspot" + }, + "setup/room-registration-state.test.ts": { + "maxFunctionLines": 292, + "owner": "setup test hotspot" + }, + "setup/verify-state.test.ts": { + "maxFunctionLines": 297, + "owner": "setup test hotspot" + }, + "src/agent-runner-environment.test.ts": { + "maxFunctionLines": 261, + "owner": "agent runner test hotspot" + }, + "src/agent-runner-process.ts": { + "maxFunctionLines": 387, + "owner": "agent runner process hotspot" + }, + "src/agent-runner.test.ts": { + "maxFunctionLines": 548, + "owner": "agent runner test hotspot" + }, + "src/channels/discord.test.ts": { + "maxFunctionLines": 816, + "owner": "discord test hotspot" + }, + "src/channels/discord.ts": { + "maxLines": 903, + "maxFunctionLines": 191, + "owner": "discord channel hotspot" + }, + "src/codex-warmup.test.ts": { + "maxFunctionLines": 277, + "owner": "codex warmup test hotspot" + }, + "src/config/load-config.ts": { + "maxFunctionLines": 187, + "owner": "config loading hotspot" + }, + "src/db/base-schema.ts": { + "maxFunctionLines": 436, + "owner": "db schema hotspot" + }, + "src/db/paired-state.ts": { + "maxLines": 951, + "maxFunctionLines": 217, + "owner": "paired db state hotspot" + }, + "src/db/paired-turn-provenance-schema.ts": { + "maxLines": 2062, + "maxFunctionLines": 343, + "owner": "paired turn provenance schema hotspot" + }, + "src/db.test.ts": { + "maxLines": 9380, + "maxFunctionLines": 4990, + "maxComplexity": 29, + "maxNesting": 2, + "owner": "db test hotspot" + }, + "src/duplicate-suppression.test.ts": { + "maxFunctionLines": 310, + "owner": "duplicate suppression test hotspot" + }, + "src/group-queue.test.ts": { + "maxFunctionLines": 720, + "owner": "group queue test hotspot" + }, + "src/group-queue.ts": { + "maxLines": 929, + "owner": "group queue hotspot" + }, + "src/index.ts": { + "maxFunctionLines": 314, + "owner": "main service entrypoint hotspot" + }, + "src/ipc-task-processor.ts": { + "maxFunctionLines": 566, + "maxComplexity": 107, + "owner": "ipc task processor hotspot" + }, + "src/message-agent-executor-attempt-runner.ts": { + "maxFunctionLines": 275, + "maxComplexity": 47, + "owner": "message executor attempt runner hotspot" + }, + "src/message-agent-executor-lifecycle.ts": { + "maxFunctionLines": 417, + "owner": "message executor lifecycle hotspot" + }, + "src/message-agent-executor-paired.ts": { + "maxFunctionLines": 440, + "owner": "message executor paired hotspot" + }, + "src/message-agent-executor-target.ts": { + "maxFunctionLines": 274, + "maxComplexity": 48, + "owner": "message executor target hotspot" + }, + "src/message-agent-executor.test.ts": { + "maxLines": 3847, + "maxFunctionLines": 2500, + "owner": "message executor test hotspot" + }, + "src/message-agent-executor.ts": { + "maxFunctionLines": 239, + "owner": "message executor hotspot" + }, + "src/message-runtime-handoffs.ts": { + "maxFunctionLines": 207, + "owner": "message runtime handoffs hotspot" + }, + "src/message-runtime-queue.test.ts": { + "maxFunctionLines": 366, + "owner": "message runtime queue test hotspot" + }, + "src/message-runtime-queue.ts": { + "maxFunctionLines": 228, + "owner": "message runtime queue hotspot" + }, + "src/message-runtime-rules.test.ts": { + "maxFunctionLines": 447, + "owner": "message runtime rules test hotspot" + }, + "src/message-runtime.test.ts": { + "maxLines": 5840, + "maxFunctionLines": 5506, + "maxComplexity": 7, + "maxNesting": 1, + "owner": "message runtime test hotspot" + }, + "src/message-runtime.ts": { + "maxFunctionLines": 620, + "owner": "message runtime hotspot" + }, + "src/message-turn-controller.test.ts": { + "maxFunctionLines": 730, + "owner": "message turn controller test hotspot" + }, + "src/message-turn-controller.ts": { + "maxLines": 948, + "maxFunctionLines": 204, + "owner": "message turn controller hotspot" + }, + "src/paired-execution-context.test.ts": { + "maxFunctionLines": 1269, + "owner": "paired execution context test hotspot" + }, + "src/paired-execution-context.ts": { + "maxFunctionLines": 206, + "maxComplexity": 43, + "owner": "paired execution context hotspot" + }, + "src/paired-follow-up-scheduler.test.ts": { + "maxFunctionLines": 830, + "owner": "paired follow-up scheduler test hotspot" + }, + "src/paired-workspace-manager.test.ts": { + "maxFunctionLines": 1199, + "owner": "paired workspace manager test hotspot" + }, + "src/paired-workspace-manager.ts": { + "maxLines": 1056, + "owner": "paired workspace manager hotspot" + }, + "src/service-routing.test.ts": { + "maxFunctionLines": 272, + "owner": "service routing test hotspot" + }, + "src/streamed-output-evaluator.test.ts": { + "maxFunctionLines": 465, + "owner": "streamed output evaluator test hotspot" + }, + "src/task-scheduler.test.ts": { + "maxLines": 1968, + "maxFunctionLines": 1790, + "owner": "task scheduler test hotspot" + }, + "src/task-scheduler.ts": { + "maxFunctionLines": 367, + "owner": "task scheduler hotspot" + }, + "src/verification.test.ts": { + "maxFunctionLines": 534, + "owner": "verification test hotspot" + }, + "src/web-dashboard-data.test.ts": { + "maxFunctionLines": 504, + "owner": "web dashboard data test hotspot" + }, + "src/web-dashboard-server.test.ts": { + "maxLines": 1654, + "maxFunctionLines": 1577, + "owner": "web dashboard server test hotspot" + }, + "src/web-dashboard-server.ts": { + "maxLines": 1759, + "maxFunctionLines": 1007, + "maxComplexity": 158, + "maxNesting": 3, + "owner": "web dashboard server hotspot" + } + } +} diff --git a/runners/agent-runner/src/ipc-message.ts b/runners/agent-runner/src/ipc-message.ts index 0640212..5124925 100644 --- a/runners/agent-runner/src/ipc-message.ts +++ b/runners/agent-runner/src/ipc-message.ts @@ -1,3 +1,8 @@ +import { + normalizeEjclawStructuredOutput, + type RunnerOutputAttachment, +} from 'ejclaw-runners-shared'; + export interface SendMessageIpcPayloadInput { chatJid: string; text: string; @@ -8,17 +13,36 @@ export interface SendMessageIpcPayloadInput { timestamp?: string; } +export interface SendMessageIpcPayload { + type: 'message'; + chatJid: string; + text: string; + sender?: string; + senderRole?: string; + runId?: string; + groupFolder: string; + timestamp: string; + attachments?: RunnerOutputAttachment[]; +} + export function buildSendMessageIpcPayload( input: SendMessageIpcPayloadInput, -): Record { +): SendMessageIpcPayload { + const normalized = normalizeEjclawStructuredOutput(input.text); + const output = + normalized.output?.visibility === 'public' ? normalized.output : null; + const text = output?.text ?? normalized.result ?? ''; + const attachments = output?.attachments ?? []; + return { type: 'message', chatJid: input.chatJid, - text: input.text, + text, sender: input.sender || undefined, senderRole: input.senderRole || undefined, runId: input.runId || undefined, groupFolder: input.groupFolder, timestamp: input.timestamp || new Date().toISOString(), + ...(attachments.length > 0 ? { attachments } : {}), }; } diff --git a/runners/agent-runner/test/ipc-message.test.ts b/runners/agent-runner/test/ipc-message.test.ts index b2df47f..3d176ec 100644 --- a/runners/agent-runner/test/ipc-message.test.ts +++ b/runners/agent-runner/test/ipc-message.test.ts @@ -37,4 +37,52 @@ describe('agent runner IPC message payload', () => { }).senderRole, ).toBeUndefined(); }); + + it('normalizes structured EJClaw envelopes instead of leaking raw JSON', () => { + expect( + buildSendMessageIpcPayload({ + chatJid: 'dc:123', + text: JSON.stringify({ + ejclaw: { + visibility: 'public', + text: '스크린샷입니다.', + verdict: 'done', + attachments: [ + { + path: '/tmp/ejclaw-room-mobile-list-390.png', + name: 'room.png', + mime: 'image/png', + }, + ], + }, + }), + groupFolder: 'discord-review', + timestamp: '2026-04-04T13:45:00.000Z', + }), + ).toEqual({ + type: 'message', + chatJid: 'dc:123', + text: '스크린샷입니다.', + groupFolder: 'discord-review', + timestamp: '2026-04-04T13:45:00.000Z', + attachments: [ + { + path: '/tmp/ejclaw-room-mobile-list-390.png', + name: 'room.png', + mime: 'image/png', + }, + ], + }); + }); + + it('turns silent EJClaw envelopes into empty no-op messages', () => { + expect( + buildSendMessageIpcPayload({ + chatJid: 'dc:123', + text: '{"ejclaw":{"visibility":"silent","verdict":"silent"}}', + groupFolder: 'discord-review', + timestamp: '2026-04-04T13:45:00.000Z', + }).text, + ).toBe(''); + }); }); diff --git a/scripts/check-code-quality.ts b/scripts/check-code-quality.ts new file mode 100644 index 0000000..e7c9aea --- /dev/null +++ b/scripts/check-code-quality.ts @@ -0,0 +1,132 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { relative, resolve } from 'node:path'; + +import { + analyzeCodeQuality, + evaluateCodeQuality, + type CodeQualityBudget, + type CodeQualityFinding, + type CodeQualityMetrics, +} from './quality/code-quality.js'; + +interface QualityBudgetConfig { + defaults: CodeQualityBudget; + overrides?: Record; + testDefaults?: CodeQualityBudget; +} + +const ROOT = resolve(import.meta.dir, '..'); +const CONFIG_PATH = resolve(ROOT, 'quality/code-quality-budgets.json'); +const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.css']); +const IGNORED_PARTS = new Set([ + 'dist', + 'node_modules', + 'coverage', + 'store', + 'data', +]); + +const config = JSON.parse( + readFileSync(CONFIG_PATH, 'utf8'), +) as QualityBudgetConfig; +const reportOnly = process.argv.includes('--report'); +const metrics = collectMetrics(); +const findings = metrics.flatMap((item) => + evaluateCodeQuality(item, budgetForFile(item.filePath, config)), +); + +if (reportOnly) { + for (const item of metrics + .filter((metric) => config.overrides?.[metric.filePath]) + .sort((a, b) => b.lineCount - a.lineCount)) { + console.log( + [ + item.filePath, + `lines=${item.lineCount}`, + `fnLines=${item.maxFunctionLines}`, + `complexity=${item.maxComplexity}`, + `nesting=${item.maxNesting}`, + ].join(' '), + ); + } +} + +if (findings.length > 0) { + console.error(formatFindings(findings)); + process.exit(1); +} + +console.log( + `code quality OK (${metrics.length} files, ${Object.keys(config.overrides ?? {}).length} ratcheted hotspots)`, +); + +function collectMetrics(): CodeQualityMetrics[] { + return trackedFiles() + .filter((filePath) => shouldAnalyze(filePath)) + .map((filePath) => + analyzeCodeQuality( + filePath, + readFileSync(resolve(ROOT, filePath), 'utf8'), + ), + ); +} + +function trackedFiles(): string[] { + return execFileSync('git', ['ls-files'], { + cwd: ROOT, + encoding: 'utf8', + }) + .split('\n') + .filter(Boolean); +} + +function shouldAnalyze(filePath: string): boolean { + if (filePath.endsWith('.d.ts')) return false; + const parts = filePath.split('/'); + if (parts.some((part) => IGNORED_PARTS.has(part))) return false; + return SOURCE_EXTENSIONS.has(extensionFor(filePath)); +} + +function extensionFor(filePath: string): string { + const match = /\.[^.]+$/.exec(filePath); + return match?.[0] ?? ''; +} + +function budgetForFile( + filePath: string, + budgetConfig: QualityBudgetConfig, +): CodeQualityBudget { + const base = isTestFile(filePath) + ? (budgetConfig.testDefaults ?? budgetConfig.defaults) + : budgetConfig.defaults; + return { + ...base, + ...budgetConfig.overrides?.[filePath], + }; +} + +function isTestFile(filePath: string): boolean { + return ( + filePath.includes('/test/') || + filePath.endsWith('.test.ts') || + filePath.endsWith('.test.tsx') || + filePath.endsWith('.spec.ts') || + filePath.endsWith('.spec.tsx') + ); +} + +function formatFindings(findings: CodeQualityFinding[]): string { + const lines = findings.map((finding) => { + const label = finding.owner + ? `${finding.filePath} (${finding.owner})` + : finding.filePath; + return `- ${label}: ${finding.metric} ${finding.actual} > ${finding.budget}`; + }); + return [ + 'code quality budget failed:', + ...lines, + '', + `Update the code to reduce the metric, or adjust ${relative(ROOT, CONFIG_PATH)} only when intentionally ratcheting a new baseline.`, + ].join('\n'); +} diff --git a/scripts/quality/code-quality.test.ts b/scripts/quality/code-quality.test.ts new file mode 100644 index 0000000..78c8beb --- /dev/null +++ b/scripts/quality/code-quality.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest'; + +import { analyzeCodeQuality, evaluateCodeQuality } from './code-quality.js'; + +describe('code quality evaluator', () => { + it('measures line count and function metrics from TypeScript source', () => { + const metrics = analyzeCodeQuality( + 'sample.ts', + [ + 'export function choose(value: number) {', + ' if (value > 10) {', + ' return value;', + ' }', + ' return value > 0 ? value : 0;', + '}', + ].join('\n'), + ); + + expect(metrics.lineCount).toBe(6); + expect(metrics.nonEmptyLineCount).toBe(6); + expect(metrics.maxFunctionLines).toBe(6); + expect(metrics.maxComplexity).toBeGreaterThanOrEqual(3); + expect(metrics.maxNesting).toBeGreaterThanOrEqual(1); + }); + + it('does not let nested functions inflate the parent function budget', () => { + const metrics = analyzeCodeQuality( + 'nested.ts', + [ + 'export function outer() {', + ' const inner = () => {', + ' if (true) return 1;', + ' return 0;', + ' };', + ' return inner();', + '}', + ].join('\n'), + ); + + expect(metrics.maxComplexity).toBe(2); + }); + + it('returns budget findings for metrics over the configured limit', () => { + const metrics = analyzeCodeQuality( + 'oversized.ts', + ['export function f() {', ' return 1;', '}'].join('\n'), + ); + + expect( + evaluateCodeQuality(metrics, { + maxComplexity: 1, + maxFunctionLines: 2, + maxLines: 2, + maxNesting: 0, + owner: 'test', + }), + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ metric: 'lineCount' }), + expect.objectContaining({ metric: 'maxFunctionLines' }), + ]), + ); + }); +}); diff --git a/scripts/quality/code-quality.ts b/scripts/quality/code-quality.ts new file mode 100644 index 0000000..5fd8900 --- /dev/null +++ b/scripts/quality/code-quality.ts @@ -0,0 +1,202 @@ +import ts from 'typescript'; + +export interface CodeQualityBudget { + maxComplexity?: number; + maxFunctionLines?: number; + maxLines?: number; + maxNesting?: number; + owner?: string; +} + +export interface CodeQualityMetrics { + filePath: string; + lineCount: number; + maxComplexity: number; + maxFunctionLines: number; + maxNesting: number; + nonEmptyLineCount: number; +} + +export interface CodeQualityFinding { + actual: number; + budget: number; + filePath: string; + metric: keyof Pick< + CodeQualityMetrics, + 'lineCount' | 'maxComplexity' | 'maxFunctionLines' | 'maxNesting' + >; + owner?: string; +} + +interface FunctionMetrics { + complexity: number; + lines: number; + nesting: number; +} + +export function analyzeCodeQuality( + filePath: string, + sourceText: string, +): CodeQualityMetrics { + const lines = sourceText.split(/\r?\n/); + const textMetrics = { + filePath, + lineCount: lines.length, + nonEmptyLineCount: lines.filter((line) => line.trim().length > 0).length, + }; + if (!/\.(?:c|m)?[jt]sx?$/.test(filePath)) { + return { + ...textMetrics, + maxComplexity: 0, + maxFunctionLines: 0, + maxNesting: 0, + }; + } + + const sourceFile = ts.createSourceFile( + filePath, + sourceText, + ts.ScriptTarget.Latest, + true, + scriptKindForPath(filePath), + ); + const functions: FunctionMetrics[] = []; + + const visit = (node: ts.Node) => { + if (isFunctionLike(node)) { + functions.push(analyzeFunction(sourceFile, node)); + } + ts.forEachChild(node, visit); + }; + ts.forEachChild(sourceFile, visit); + + return { + ...textMetrics, + maxComplexity: maxOf(functions.map((fn) => fn.complexity)), + maxFunctionLines: maxOf(functions.map((fn) => fn.lines)), + maxNesting: maxOf(functions.map((fn) => fn.nesting)), + }; +} + +export function evaluateCodeQuality( + metrics: CodeQualityMetrics, + budget: CodeQualityBudget, +): CodeQualityFinding[] { + const findings: CodeQualityFinding[] = []; + addFinding(findings, metrics, budget, 'lineCount', budget.maxLines); + addFinding( + findings, + metrics, + budget, + 'maxFunctionLines', + budget.maxFunctionLines, + ); + addFinding(findings, metrics, budget, 'maxComplexity', budget.maxComplexity); + addFinding(findings, metrics, budget, 'maxNesting', budget.maxNesting); + return findings; +} + +function addFinding( + findings: CodeQualityFinding[], + metrics: CodeQualityMetrics, + budget: CodeQualityBudget, + metric: CodeQualityFinding['metric'], + limit: number | undefined, +) { + if (limit === undefined) return; + const actual = metrics[metric]; + if (actual <= limit) return; + findings.push({ + actual, + budget: limit, + filePath: metrics.filePath, + metric, + owner: budget.owner, + }); +} + +function analyzeFunction( + sourceFile: ts.SourceFile, + node: ts.SignatureDeclaration, +): FunctionMetrics { + const startLine = sourceFile.getLineAndCharacterOfPosition( + node.getStart(sourceFile), + ).line; + const endLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; + let complexity = 1; + let maxNesting = 0; + + const walk = (current: ts.Node, nesting: number) => { + if (current !== node && isFunctionLike(current)) return; + + if (isDecisionNode(current)) { + complexity += 1; + const nextNesting = nesting + 1; + maxNesting = Math.max(maxNesting, nextNesting); + ts.forEachChild(current, (child) => walk(child, nextNesting)); + return; + } + + if ( + ts.isBinaryExpression(current) && + isLogicalOperator(current.operatorToken.kind) + ) { + complexity += 1; + } + + ts.forEachChild(current, (child) => walk(child, nesting)); + }; + + walk(node, 0); + + return { + complexity, + lines: endLine - startLine + 1, + nesting: maxNesting, + }; +} + +function isDecisionNode(node: ts.Node): boolean { + return ( + ts.isIfStatement(node) || + ts.isForStatement(node) || + ts.isForInStatement(node) || + ts.isForOfStatement(node) || + ts.isWhileStatement(node) || + ts.isDoStatement(node) || + ts.isCaseClause(node) || + ts.isCatchClause(node) || + ts.isConditionalExpression(node) + ); +} + +function isFunctionLike(node: ts.Node): node is ts.SignatureDeclaration { + return ( + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) || + ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node) + ); +} + +function isLogicalOperator(kind: ts.SyntaxKind): boolean { + return ( + kind === ts.SyntaxKind.AmpersandAmpersandToken || + kind === ts.SyntaxKind.BarBarToken || + kind === ts.SyntaxKind.QuestionQuestionToken + ); +} + +function maxOf(values: number[]): number { + return values.length === 0 ? 0 : Math.max(...values); +} + +function scriptKindForPath(filePath: string): ts.ScriptKind { + if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX; + if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX; + if (filePath.endsWith('.js')) return ts.ScriptKind.JS; + return ts.ScriptKind.TS; +} diff --git a/src/db/tasks.ts b/src/db/tasks.ts index a721521..dd47070 100644 --- a/src/db/tasks.ts +++ b/src/db/tasks.ts @@ -73,9 +73,10 @@ export function getTaskByIdFromDatabase( database: Database, id: string, ): ScheduledTask | undefined { - return database + const row = database .prepare('SELECT * FROM scheduled_tasks WHERE id = ?') - .get(id) as ScheduledTask | undefined; + .get(id) as ScheduledTask | null | undefined; + return row ?? undefined; } export function findDuplicateCiWatcherInDatabase( diff --git a/src/index.ts b/src/index.ts index b749aba..168fbec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -387,7 +387,7 @@ async function main(): Promise { editFormattedTrackedChannelMessage(channels, jid, messageId, rawText), }); startIpcWatcher({ - sendMessage: async (jid, text, senderRole, runId) => { + sendMessage: async (jid, text, senderRole, runId, attachments) => { if ( runId && (senderRole === 'reviewer' || senderRole === 'arbiter') && @@ -419,7 +419,7 @@ async function main(): Promise { }, 'IPC relay routed message to channel', ); - await route.channel.sendMessage(jid, text); + await route.channel.sendMessage(jid, text, { attachments }); if ( (senderRole === 'reviewer' || senderRole === 'arbiter') && isTerminalStatusMessage(text) diff --git a/src/ipc-auth.test.ts b/src/ipc-auth.test.ts index d79495a..8a8d73a 100644 --- a/src/ipc-auth.test.ts +++ b/src/ipc-auth.test.ts @@ -556,6 +556,7 @@ describe('IPC message authorization', () => { 'review text', 'reviewer', 'run-reviewer-ipc', + undefined, ); expect(result).toEqual( expect.objectContaining({ @@ -565,6 +566,39 @@ describe('IPC message authorization', () => { ); }); + it('forwards structured attachments through authorized IPC messages', async () => { + const sendMessage = vi.fn(async () => {}); + const attachments = [ + { + path: '/tmp/ejclaw-room-mobile-list-390.png', + name: 'room.png', + mime: 'image/png', + }, + ]; + + const result = await forwardAuthorizedIpcMessage( + { + type: 'message', + chatJid: 'other@g.us', + text: '스크린샷입니다.', + attachments, + }, + 'other-group', + false, + groups, + sendMessage, + ); + + expect(sendMessage).toHaveBeenCalledWith( + 'other@g.us', + '스크린샷입니다.', + undefined, + undefined, + attachments, + ); + expect(result.outcome).toBe('sent'); + }); + it('injects authorized inbound messages for the source group without routing them as outbound sends', async () => { const sendMessage = vi.fn(async () => {}); const injectInboundMessage = vi.fn(async () => {}); diff --git a/src/ipc-message-forwarding.ts b/src/ipc-message-forwarding.ts index 6a4f8fb..f82f567 100644 --- a/src/ipc-message-forwarding.ts +++ b/src/ipc-message-forwarding.ts @@ -14,6 +14,7 @@ export async function forwardAuthorizedIpcMessage( text: string, senderRole?: string, runId?: string, + attachments?: import('./types.js').OutboundAttachment[], ) => Promise, injectInboundMessage?: (payload: { chatJid: string; @@ -79,7 +80,13 @@ export async function forwardAuthorizedIpcMessage( }; } - await sendMessage(msg.chatJid, msg.text, msg.senderRole, msg.runId); + await sendMessage( + msg.chatJid, + msg.text, + msg.senderRole, + msg.runId, + msg.attachments, + ); return { outcome: 'sent', chatJid: msg.chatJid, diff --git a/src/ipc-types.ts b/src/ipc-types.ts index fb979a8..8a3af35 100644 --- a/src/ipc-types.ts +++ b/src/ipc-types.ts @@ -3,6 +3,7 @@ import type { AssignRoomInput } from './db.js'; import type { AgentType, MessageSourceKind, + OutboundAttachment, PairedTask, RegisteredGroup, RoomMode, @@ -67,6 +68,7 @@ export interface IpcDeps { text: string, senderRole?: string, runId?: string, + attachments?: OutboundAttachment[], ) => Promise; nudgeScheduler?: () => void; roomBindings: () => Record; @@ -100,6 +102,7 @@ export interface IpcMessagePayload { timestamp?: string; treatAsHuman?: boolean; sourceKind?: MessageSourceKind; + attachments?: OutboundAttachment[]; } export interface IpcMessageForwardResult { diff --git a/src/outbound-attachments.test.ts b/src/outbound-attachments.test.ts index 85c0fa3..d0e1bcd 100644 --- a/src/outbound-attachments.test.ts +++ b/src/outbound-attachments.test.ts @@ -12,6 +12,7 @@ const ONE_PIXEL_PNG = Buffer.from( ); const cleanupDirs: string[] = []; +const cleanupFiles: string[] = []; function makeTempDir(baseDir: string, prefix: string): string { const dir = fs.mkdtempSync(path.join(baseDir, prefix)); @@ -33,6 +34,9 @@ afterEach(() => { for (const dir of cleanupDirs.splice(0)) { fs.rmSync(dir, { force: true, recursive: true }); } + for (const file of cleanupFiles.splice(0)) { + fs.rmSync(file, { force: true }); + } }); describe('validateOutboundAttachments', () => { @@ -57,6 +61,31 @@ describe('validateOutboundAttachments', () => { ]); }); + it('accepts direct EJClaw screenshot files under the temp directory', () => { + const imagePath = path.join( + os.tmpdir(), + `ejclaw-room-mobile-list-${Date.now()}.png`, + ); + fs.writeFileSync(imagePath, ONE_PIXEL_PNG); + cleanupFiles.push(imagePath); + + const result = validateOutboundAttachments([ + { + path: imagePath, + name: 'room-list.png', + mime: 'image/png', + }, + ]); + + expect(result.rejected).toEqual([]); + expect(result.files).toEqual([ + { + attachment: fs.realpathSync(imagePath), + name: 'room-list.png', + }, + ]); + }); + it('requires workspace paths to be explicitly allowlisted', () => { const dir = makeTempDir(process.cwd(), '.ejclaw-attachment-'); const imagePath = writeFile(dir, 'workspace-shot.png', ONE_PIXEL_PNG); diff --git a/src/outbound-attachments.ts b/src/outbound-attachments.ts index fffd8e3..ab97124 100644 --- a/src/outbound-attachments.ts +++ b/src/outbound-attachments.ts @@ -21,6 +21,7 @@ const DEFAULT_TEMP_ATTACHMENT_DIR_PREFIXES = [ 'ejclaw-attachment-', 'ejclaw-discord-image-', ] as const; +const DEFAULT_TEMP_ATTACHMENT_FILE_PREFIXES = ['ejclaw-'] as const; function unique(values: Array): string[] { return [ @@ -83,6 +84,21 @@ function isWithinDefaultTempAttachmentDir( ); } +function isDefaultTempAttachmentFile( + realPath: string, + tempDir: string | null, +): boolean { + if (!tempDir) return false; + const relative = path.relative(tempDir, realPath); + if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) { + return false; + } + if (relative.includes(path.sep)) return false; + return DEFAULT_TEMP_ATTACHMENT_FILE_PREFIXES.some((prefix) => + relative.startsWith(prefix), + ); +} + function detectImageMime(filePath: string): string | null { const handle = fs.openSync(filePath, 'r'); try { @@ -171,7 +187,8 @@ export function validateOutboundAttachments( } if ( !matchesAllowedBaseDir(realPath, baseDirs) && - !isWithinDefaultTempAttachmentDir(realPath, defaultTempDir) + !isWithinDefaultTempAttachmentDir(realPath, defaultTempDir) && + !isDefaultTempAttachmentFile(realPath, defaultTempDir) ) { rejected.push({ path: requestedPath, reason: 'outside-allowed-dirs' }); continue; diff --git a/src/web-dashboard-data.test.ts b/src/web-dashboard-data.test.ts index 6632451..7960cb4 100644 --- a/src/web-dashboard-data.test.ts +++ b/src/web-dashboard-data.test.ts @@ -356,6 +356,99 @@ describe('web dashboard data', () => { ]); }); + it('uses Discord live progress messages without exposing them as recent chat', () => { + const task = makePairedTask({ + id: 'paired-room-progress', + chat_jid: 'dc:ops', + status: 'active', + updated_at: '2026-04-26T06:10:00.000Z', + }); + const turn: PairedTurnRecord = { + turn_id: 'turn-progress', + task_id: task.id, + task_updated_at: task.updated_at, + role: 'owner', + intent_kind: 'owner-turn', + state: 'running', + executor_service_id: 'codex-main', + executor_agent_type: 'codex', + attempt_no: 1, + created_at: '2026-04-26T06:00:00.000Z', + updated_at: '2026-04-26T06:10:00.000Z', + completed_at: null, + last_error: null, + }; + const statusPrefix = '⁣⁣⁣'; + const messages: NewMessage[] = [ + { + id: 'msg-latest', + chat_jid: 'dc:ops', + sender: 'user-1', + sender_name: '눈쟁이', + content: 'recent human message', + timestamp: '2026-04-26T06:09:00.000Z', + is_from_me: false, + is_bot_message: false, + message_source_kind: 'human', + }, + ]; + const progressMessages: NewMessage[] = [ + { + id: 'msg-stale-progress', + chat_jid: 'dc:ops', + sender: 'bot-1', + sender_name: '오너', + content: `${statusPrefix}stale progress`, + timestamp: '2026-04-26T05:59:00.000Z', + is_from_me: true, + is_bot_message: true, + message_source_kind: 'bot', + }, + { + id: 'msg-live-progress', + chat_jid: 'dc:ops', + sender: 'bot-1', + sender_name: '오너', + content: `${statusPrefix}building mobile parity\n\n12s`, + timestamp: '2026-04-26T06:08:00.000Z', + is_from_me: true, + is_bot_message: true, + message_source_kind: 'bot', + }, + ]; + + const activity = buildWebDashboardRoomActivity({ + serviceId: 'codex-main', + entry: { + jid: 'dc:ops', + name: '#ops', + folder: 'ops', + agentType: 'codex', + status: 'processing', + elapsedMs: 20_000, + pendingMessages: false, + pendingTasks: 0, + }, + pairedTask: task, + turns: [turn], + attempts: [], + outputs: [], + messages, + progressMessages, + }); + + expect(activity.pairedTask?.currentTurn).toMatchObject({ + progressText: 'building mobile parity', + progressUpdatedAt: '2026-04-26T06:08:00.000Z', + }); + expect(activity.messages).toEqual([ + expect.objectContaining({ + senderName: '눈쟁이', + content: 'recent human message', + }), + ]); + }); + it('builds typed inbox items from pending rooms, paired tasks, and CI failures', () => { const snapshots: StatusSnapshot[] = [ { diff --git a/src/web-dashboard-data.ts b/src/web-dashboard-data.ts index f2c0edb..df33b09 100644 --- a/src/web-dashboard-data.ts +++ b/src/web-dashboard-data.ts @@ -1,3 +1,4 @@ +import { normalizeEjclawStructuredOutput } from './agent-protocol.js'; import { isWatchCiTask } from './task-watch-status.js'; import type { PairedTurnAttemptRecord, PairedTurnRecord } from './db.js'; import type { StatusSnapshot, UsageRowSnapshot } from './status-dashboard.js'; @@ -182,6 +183,37 @@ function buildRoomPreview(value: string, maxLength: number): string { return truncateText(redactSensitiveText(value).trim(), maxLength); } +function decodeCommonHtmlEntities(value: string): string { + return value + .replace(/"|"|"/gi, '"') + .replace(/'|'|'/gi, "'") + .replace(/<|<|</gi, '<') + .replace(/>|>|>/gi, '>') + .replace(/&|&|&/gi, '&'); +} + +function hasStructuredOutputHint(value: string): boolean { + return value.includes('"ejclaw"') || /"ejclaw"/i.test(value); +} + +function normalizeRoomMessageContent(value: string): string | null { + if (!hasStructuredOutputHint(value)) return value; + + const candidates = [value, decodeCommonHtmlEntities(value)]; + for (const candidate of candidates) { + const normalized = normalizeEjclawStructuredOutput(candidate); + if (normalized.output?.visibility === 'silent') return null; + if ( + normalized.output?.visibility === 'public' && + normalized.output.text !== candidate + ) { + return normalized.output.text; + } + } + + return value; +} + function stableHash(value: string): string { let hash = 0; for (let index = 0; index < value.length; index += 1) { @@ -405,15 +437,17 @@ export function sanitizeScheduledTask( }; } -function sanitizeRoomMessage(message: NewMessage): WebDashboardRoomMessage { +function sanitizeRoomMessage( + message: NewMessage, +): WebDashboardRoomMessage | null { + const content = normalizeRoomMessageContent(message.content ?? ''); + if (content === null) return null; + return { id: message.id, sender: message.sender, senderName: message.sender_name || message.sender, - content: buildRoomPreview( - message.content ?? '', - ROOM_MESSAGE_PREVIEW_MAX_LENGTH, - ), + content: buildRoomPreview(content, ROOM_MESSAGE_PREVIEW_MAX_LENGTH), timestamp: message.timestamp, isFromMe: !!message.is_from_me, isBotMessage: !!message.is_bot_message, @@ -431,6 +465,10 @@ function sanitizeRoomMessage(message: NewMessage): WebDashboardRoomMessage { */ const TASK_STATUS_PREFIX = '⁣⁣⁣'; +function isTaskStatusMessage(message: NewMessage): boolean { + return (message.content ?? '').startsWith(TASK_STATUS_PREFIX); +} + function turnRoleFromSenderName(name: string | null | undefined): string { const v = (name ?? '').toLowerCase(); if (v.includes('오너') || v.includes('owner')) return 'owner'; @@ -533,6 +571,7 @@ export function buildWebDashboardRoomActivity(args: { attempts: PairedTurnAttemptRecord[]; outputs: PairedTurnOutput[]; messages: NewMessage[]; + progressMessages?: NewMessage[]; outputLimit?: number; }): WebDashboardRoomActivity { const latestAttemptByTurnId = new Map(); @@ -561,7 +600,12 @@ export function buildWebDashboardRoomActivity(args: { elapsedMs: args.entry.elapsedMs, pendingMessages: args.entry.pendingMessages, pendingTasks: args.entry.pendingTasks, - messages: args.messages.map(sanitizeRoomMessage), + messages: args.messages + .filter((message) => !isTaskStatusMessage(message)) + .map(sanitizeRoomMessage) + .filter((message): message is WebDashboardRoomMessage => + Boolean(message), + ), pairedTask: args.pairedTask ? { id: args.pairedTask.id, @@ -570,7 +614,11 @@ export function buildWebDashboardRoomActivity(args: { roundTripCount: args.pairedTask.round_trip_count, updatedAt: args.pairedTask.updated_at, currentTurn: currentTurn - ? sanitizeRoomTurn(currentTurn, currentAttempt, args.messages) + ? sanitizeRoomTurn( + currentTurn, + currentAttempt, + args.progressMessages ?? args.messages, + ) : null, outputs: args.outputs.slice(-outputLimit).map(sanitizeRoomTurnOutput), } diff --git a/src/web-dashboard-room-envelope.test.ts b/src/web-dashboard-room-envelope.test.ts new file mode 100644 index 0000000..3d55198 --- /dev/null +++ b/src/web-dashboard-room-envelope.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest'; + +import type { NewMessage } from './types.js'; +import { buildWebDashboardRoomActivity } from './web-dashboard-data.js'; + +describe('web dashboard room envelope rendering', () => { + it('unwraps EJClaw structured envelopes in room messages', () => { + const envelope = JSON.stringify({ + ejclaw: { + visibility: 'public', + text: 'PR #52 모바일/룸 parity 검증 스크린샷입니다.', + verdict: 'done', + attachments: [ + { + path: '/tmp/ejclaw-room-mobile-list-390.png', + name: 'room.png', + mime: 'image/png', + }, + ], + }, + }); + const htmlEscapedEnvelope = envelope.replace(/"/g, '"'); + const messages: NewMessage[] = [ + makeMessage('msg-json', envelope, 'owner'), + makeMessage('msg-html-json', htmlEscapedEnvelope, 'reviewer'), + makeMessage( + 'msg-silent', + '{"ejclaw":{"visibility":"silent","verdict":"silent"}}', + 'arbiter', + ), + ]; + + const activity = buildWebDashboardRoomActivity({ + serviceId: 'codex-main', + entry: { + jid: 'dc:ops', + name: '#ops', + folder: 'ops', + agentType: 'codex', + status: 'processing', + elapsedMs: 15_000, + pendingMessages: true, + pendingTasks: 1, + }, + pairedTask: null, + turns: [], + attempts: [], + outputs: [], + messages, + }); + + expect(activity.messages).toHaveLength(2); + expect(activity.messages.map((message) => message.content)).toEqual([ + 'PR #52 모바일/룸 parity 검증 스크린샷입니다.', + 'PR #52 모바일/룸 parity 검증 스크린샷입니다.', + ]); + expect(JSON.stringify(activity.messages)).not.toContain('"ejclaw"'); + }); +}); + +function makeMessage( + id: string, + content: string, + senderName: string, +): NewMessage { + return { + id, + chat_jid: 'dc:ops', + sender: senderName, + sender_name: senderName, + content, + timestamp: '2026-04-26T05:29:00.000Z', + is_from_me: false, + is_bot_message: true, + message_source_kind: 'bot', + }; +} diff --git a/src/web-dashboard-server.test.ts b/src/web-dashboard-server.test.ts index 2615947..92ebe81 100644 --- a/src/web-dashboard-server.test.ts +++ b/src/web-dashboard-server.test.ts @@ -1506,6 +1506,22 @@ describe('web dashboard server handler', () => { message_source_kind: 'human', }, ]; + const statusPrefix = '⁣⁣⁣'; + const progressMessages: NewMessage[] = [ + ...messages, + { + id: 'msg-progress', + chat_jid: 'dc:ops', + sender: 'reviewer-bot', + sender_name: '리뷰어', + content: `${statusPrefix}checking current output\n\n9s`, + timestamp: '2026-04-26T05:20:00.000Z', + is_from_me: true, + is_bot_message: true, + message_source_kind: 'bot', + }, + ]; + const requestedMessageLimits: number[] = []; const handler = createWebDashboardHandler({ readStatusSnapshots: () => [ { @@ -1535,7 +1551,10 @@ describe('web dashboard server handler', () => { getPairedTurnAttempts: (turnId) => turnId === turns[0]!.turn_id ? attempts : [], getPairedTurnOutputs: () => outputs, - getRecentChatMessages: () => messages, + getRecentChatMessages: (_jid, limit) => { + requestedMessageLimits.push(limit ?? 20); + return limit && limit > 8 ? progressMessages : messages; + }, }); const response = await handler( @@ -1555,6 +1574,8 @@ describe('web dashboard server handler', () => { state: string; attemptNo: number; lastError: string; + progressText: string; + progressUpdatedAt: string; }; outputs: Array<{ outputText: string; turnNumber: number }>; }; @@ -1568,12 +1589,16 @@ describe('web dashboard server handler', () => { state: 'running', attemptNo: 2, lastError: 'OPENAI_API_KEY=', + progressText: 'checking current output', + progressUpdatedAt: '2026-04-26T05:20:00.000Z', }); expect(body.pairedTask.outputs).toMatchObject([ { turnNumber: 1, outputText: 'owner final output' }, ]); + expect(requestedMessageLimits).toEqual([8, 40]); expect(body.messages[0]?.content).toContain('BOT_TOKEN='); expect(body.messages[0]?.senderName).toBe('눈쟁이'); + expect(body.messages).toHaveLength(1); }); it('returns 404 for missing room timelines', async () => { diff --git a/src/web-dashboard-server.ts b/src/web-dashboard-server.ts index d92dd6f..626cd17 100644 --- a/src/web-dashboard-server.ts +++ b/src/web-dashboard-server.ts @@ -781,6 +781,9 @@ export function createWebDashboardHandler( for (const [jid, { snapshot, entry }] of uniqueByJid) { const pairedTask = loadLatestPairedTaskForChat(jid) ?? null; const messages = loadRecentChatMessages(jid, 8); + const progressMessages = pairedTask + ? loadRecentChatMessages(jid, 40) + : messages; const outputs = loadRecentPairedTurnOutputsForChat(jid, 8); if (!pairedTask && messages.length === 0 && outputs.length === 0) continue; @@ -796,6 +799,7 @@ export function createWebDashboardHandler( attempts: [], outputs, messages, + progressMessages, outputLimit: 8, }); } @@ -1171,6 +1175,7 @@ export function createWebDashboardHandler( attempts, outputs: pairedTask ? loadPairedTurnOutputs(pairedTask.id) : [], messages: loadRecentChatMessages(timelineRoomJid, 8), + progressMessages: loadRecentChatMessages(timelineRoomJid, 40), }), ); } diff --git a/vitest.config.ts b/vitest.config.ts index ee0c290..7ced00e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ }, test: { setupFiles: ['test/vitest-env.ts'], + testTimeout: 15_000, include: [ 'src/**/*.test.ts', 'setup/**/*.test.ts',