From b183805dc7973ca31d3c2456f8a05563dce603df Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Mon, 22 Jun 2026 02:27:14 -0400 Subject: [PATCH 1/9] chore: update node engine to 24.15.0, refresh package dependencies, and adjust project metadata --- package.json | 17 ++- pnpm-lock.yaml | 286 ++++++++++++++++++++++++-------------------- tests/index.test.ts | 4 +- 3 files changed, 168 insertions(+), 139 deletions(-) diff --git a/package.json b/package.json index 8d0c02a..69f2b7e 100644 --- a/package.json +++ b/package.json @@ -38,26 +38,25 @@ ] }, "engines": { - "node": ">=22.19" + "node": ">=24.15.0" }, "files": [ "src", "docs/assets", + "LICENSE", + "CHANGELOG.md", "README.md" ], - "publishConfig": { - "access": "public" - }, "peerDependencies": { "@earendil-works/pi-coding-agent": "*", "@earendil-works/pi-tui": "*" }, "devDependencies": { - "@biomejs/biome": "^2.4.16", - "@earendil-works/pi-coding-agent": "^0.79.3", - "@earendil-works/pi-tui": "^0.79.3", - "@types/node": "^25.9.1", + "@biomejs/biome": "^2.5.0", + "@earendil-works/pi-coding-agent": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", + "@types/node": "^25.9.3", "typescript": "^6.0.3", - "vitest": "^4.1.7" + "vitest": "^4.1.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de2f1dd..3d4195e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,23 +9,23 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: ^2.4.16 - version: 2.4.16 + specifier: ^2.5.0 + version: 2.5.0 '@earendil-works/pi-coding-agent': - specifier: ^0.79.3 - version: 0.79.3(ws@8.21.0)(zod@4.4.3) + specifier: ^0.79.8 + version: 0.79.9(ws@8.21.0)(zod@4.4.3) '@earendil-works/pi-tui': - specifier: ^0.79.3 - version: 0.79.3 + specifier: ^0.79.8 + version: 0.79.9 '@types/node': - specifier: ^25.9.1 - version: 25.9.1 + specifier: ^25.9.3 + version: 25.9.4 typescript: specifier: ^6.0.3 version: 6.0.3 vitest: - specifier: ^4.1.7 - version: 4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)(yaml@2.9.0)) + specifier: ^4.1.8 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@25.9.4)(vite@8.0.14(@types/node@25.9.4)(jiti@2.7.0)(yaml@2.9.0)) packages: @@ -143,79 +143,79 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.4.16': - resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==} + '@biomejs/biome@2.5.0': + resolution: {integrity: sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.4.16': - resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==} + '@biomejs/cli-darwin-arm64@2.5.0': + resolution: {integrity: sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.4.16': - resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==} + '@biomejs/cli-darwin-x64@2.5.0': + resolution: {integrity: sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.4.16': - resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==} + '@biomejs/cli-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.4.16': - resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==} + '@biomejs/cli-linux-arm64@2.5.0': + resolution: {integrity: sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.4.16': - resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==} + '@biomejs/cli-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.4.16': - resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==} + '@biomejs/cli-linux-x64@2.5.0': + resolution: {integrity: sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.4.16': - resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==} + '@biomejs/cli-win32-arm64@2.5.0': + resolution: {integrity: sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.4.16': - resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==} + '@biomejs/cli-win32-x64@2.5.0': + resolution: {integrity: sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] - '@earendil-works/pi-agent-core@0.79.3': - resolution: {integrity: sha512-Ksvnu6CpQLYGbCSgnQEetzliI7yb+QkqtSlmmunJ69QluT45kd3DjQZRNHfRLk++Dd02Y8QvsRKMopSJCcWoWw==} + '@earendil-works/pi-agent-core@0.79.9': + resolution: {integrity: sha512-GsFbPR85nhncKoU9++fTKa11PzwUkAmkrKXo97dBOzi10Td72rVV6vmfxKjwPLHTzRbJMa0byr12YhODZ59yLA==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.79.3': - resolution: {integrity: sha512-lMSput/haP5uZAGbXhS5rAYd3GB7GYdJkoAUxg3VFummBeqGqGqllaTWrbHFN12kVGyVfWHhdySNXkiqVh65Iw==} + '@earendil-works/pi-ai@0.79.9': + resolution: {integrity: sha512-fHmgNMONwCCE7bQAKbcz76sgm3iQuA7km1mpIc4H5xXd9+zhPh/faULz6ARkgjQE0EufHnfZPJY39+lNf8Sa9g==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-coding-agent@0.79.3': - resolution: {integrity: sha512-B3rAyw4/f1l3EYjQpCKpevzuBIPjYor6fHxPHQpLMdn/NTv3536i1P4ZrsyFkKpXqJWH66xKK5hyf8sqRe3dGA==} + '@earendil-works/pi-coding-agent@0.79.9': + resolution: {integrity: sha512-8TZ796Zn0NE4vmhxG9hv4ZtJDGJzhqMjlmFg8ZkUKxfqB7LJa4ums2jSJKtnyAZfAamN6VzqzN0A82RNDqv8Ag==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-tui@0.79.3': - resolution: {integrity: sha512-cpmkEM1aEuGUx6YZM36VlzpulwLzqD5T2cUEkGHndDTNGEbnn5sj/9SYm+QBfKjvZsWoHfZuFBnu4+hh96/FbA==} + '@earendil-works/pi-tui@0.79.9': + resolution: {integrity: sha512-XcqfoGyoX64OSMQklMR1vG2MRi1TCPSUERRCmPDvYKCK6LtZoBqJ6idNCLRklNYveCj4gHDOzOsLhGqn04jmYw==} engines: {node: '>=22.19.0'} '@emnapi/core@1.10.0': @@ -307,8 +307,13 @@ packages: resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} engines: {node: '>= 10'} - '@mistralai/mistralai@2.2.1': - resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==} + '@mistralai/mistralai@2.2.6': + resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} @@ -319,6 +324,14 @@ packages: '@nodable/entities@2.1.1': resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + '@oxc-project/types@0.132.0': resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} @@ -508,17 +521,17 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/node@25.9.1': - resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/node@25.9.4': + resolution: {integrity: sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==} '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@vitest/expect@4.1.7': - resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} - '@vitest/mocker@4.1.7': - resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -528,20 +541,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.7': - resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} - '@vitest/runner@4.1.7': - resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} - '@vitest/snapshot@4.1.7': - resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} - '@vitest/spy@4.1.7': - resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} - '@vitest/utils@4.1.7': - resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} @@ -802,9 +815,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - marked@15.0.12: - resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} - engines: {node: '>= 18'} + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} hasBin: true minimatch@10.2.5: @@ -903,6 +916,11 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -962,8 +980,8 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - undici@8.3.0: - resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==} + undici@8.5.0: + resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} engines: {node: '>=22.19.0'} vite@8.0.14: @@ -1009,20 +1027,20 @@ packages: yaml: optional: true - vitest@4.1.7: - resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.7 - '@vitest/browser-preview': 4.1.7 - '@vitest/browser-webdriverio': 4.1.7 - '@vitest/coverage-istanbul': 4.1.7 - '@vitest/coverage-v8': 4.1.7 - '@vitest/ui': 4.1.7 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1326,44 +1344,44 @@ snapshots: '@babel/runtime@7.29.7': {} - '@biomejs/biome@2.4.16': + '@biomejs/biome@2.5.0': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.16 - '@biomejs/cli-darwin-x64': 2.4.16 - '@biomejs/cli-linux-arm64': 2.4.16 - '@biomejs/cli-linux-arm64-musl': 2.4.16 - '@biomejs/cli-linux-x64': 2.4.16 - '@biomejs/cli-linux-x64-musl': 2.4.16 - '@biomejs/cli-win32-arm64': 2.4.16 - '@biomejs/cli-win32-x64': 2.4.16 - - '@biomejs/cli-darwin-arm64@2.4.16': + '@biomejs/cli-darwin-arm64': 2.5.0 + '@biomejs/cli-darwin-x64': 2.5.0 + '@biomejs/cli-linux-arm64': 2.5.0 + '@biomejs/cli-linux-arm64-musl': 2.5.0 + '@biomejs/cli-linux-x64': 2.5.0 + '@biomejs/cli-linux-x64-musl': 2.5.0 + '@biomejs/cli-win32-arm64': 2.5.0 + '@biomejs/cli-win32-x64': 2.5.0 + + '@biomejs/cli-darwin-arm64@2.5.0': optional: true - '@biomejs/cli-darwin-x64@2.4.16': + '@biomejs/cli-darwin-x64@2.5.0': optional: true - '@biomejs/cli-linux-arm64-musl@2.4.16': + '@biomejs/cli-linux-arm64-musl@2.5.0': optional: true - '@biomejs/cli-linux-arm64@2.4.16': + '@biomejs/cli-linux-arm64@2.5.0': optional: true - '@biomejs/cli-linux-x64-musl@2.4.16': + '@biomejs/cli-linux-x64-musl@2.5.0': optional: true - '@biomejs/cli-linux-x64@2.4.16': + '@biomejs/cli-linux-x64@2.5.0': optional: true - '@biomejs/cli-win32-arm64@2.4.16': + '@biomejs/cli-win32-arm64@2.5.0': optional: true - '@biomejs/cli-win32-x64@2.4.16': + '@biomejs/cli-win32-x64@2.5.0': optional: true - '@earendil-works/pi-agent-core@0.79.3(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.79.9(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.79.3(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.79.9(ws@8.21.0)(zod@4.4.3) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -1375,12 +1393,13 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.79.3(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-ai@0.79.9(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 '@google/genai': 1.52.0 - '@mistralai/mistralai': 2.2.1 + '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -1395,11 +1414,11 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.79.3(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-coding-agent@0.79.9(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.79.3(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-ai': 0.79.3(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-tui': 0.79.3 + '@earendil-works/pi-agent-core': 0.79.9(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.79.9(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-tui': 0.79.9 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cross-spawn: 7.0.6 @@ -1411,8 +1430,9 @@ snapshots: jiti: 2.7.0 minimatch: 10.2.5 proper-lockfile: 4.1.2 + semver: 7.8.0 typebox: 1.1.38 - undici: 8.3.0 + undici: 8.5.0 yaml: 2.9.0 optionalDependencies: '@mariozechner/clipboard': 0.3.9 @@ -1424,10 +1444,10 @@ snapshots: - ws - zod - '@earendil-works/pi-tui@0.79.3': + '@earendil-works/pi-tui@0.79.9': dependencies: get-east-asian-width: 1.6.0 - marked: 15.0.12 + marked: 18.0.5 '@emnapi/core@1.10.0': dependencies: @@ -1502,11 +1522,14 @@ snapshots: '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 optional: true - '@mistralai/mistralai@2.2.1': + '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': dependencies: + '@opentelemetry/semantic-conventions': 1.41.1 ws: 8.21.0 zod: 4.4.3 zod-to-json-schema: 3.25.2(zod@4.4.3) + optionalDependencies: + '@opentelemetry/api': 1.9.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -1520,6 +1543,10 @@ snapshots: '@nodable/entities@2.1.1': {} + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/semantic-conventions@1.41.1': {} + '@oxc-project/types@0.132.0': {} '@protobufjs/aspromise@1.1.2': {} @@ -1667,50 +1694,50 @@ snapshots: '@types/estree@1.0.9': {} - '@types/node@25.9.1': + '@types/node@25.9.4': dependencies: undici-types: 7.24.6 '@types/retry@0.12.0': {} - '@vitest/expect@4.1.7': + '@vitest/expect@4.1.9': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.9(vite@8.0.14(@types/node@25.9.4)(jiti@2.7.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.7 + '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@25.9.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.9.4)(jiti@2.7.0)(yaml@2.9.0) - '@vitest/pretty-format@4.1.7': + '@vitest/pretty-format@4.1.9': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.7': + '@vitest/runner@4.1.9': dependencies: - '@vitest/utils': 4.1.7 + '@vitest/utils': 4.1.9 pathe: 2.0.3 - '@vitest/snapshot@4.1.7': + '@vitest/snapshot@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.7': {} + '@vitest/spy@4.1.9': {} - '@vitest/utils@4.1.7': + '@vitest/utils@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.7 + '@vitest/pretty-format': 4.1.9 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -1938,7 +1965,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - marked@15.0.12: {} + marked@18.0.5: {} minimatch@10.2.5: dependencies: @@ -2011,7 +2038,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.1 - '@types/node': 25.9.1 + '@types/node': 25.9.4 long: 5.3.2 retry@0.12.0: {} @@ -2041,6 +2068,8 @@ snapshots: safe-buffer@5.2.1: {} + semver@7.8.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -2080,9 +2109,9 @@ snapshots: undici-types@7.24.6: {} - undici@8.3.0: {} + undici@8.5.0: {} - vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)(yaml@2.9.0): + vite@8.0.14(@types/node@25.9.4)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -2090,20 +2119,20 @@ snapshots: rolldown: 1.0.2 tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.4 fsevents: 2.3.3 jiti: 2.7.0 yaml: 2.9.0 - vitest@4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@25.9.4)(vite@8.0.14(@types/node@25.9.4)(jiti@2.7.0)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.7 - '@vitest/runner': 4.1.7 - '@vitest/snapshot': 4.1.7 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.0.14(@types/node@25.9.4)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -2115,10 +2144,11 @@ snapshots: tinyexec: 1.2.3 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@25.9.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.9.4)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.1 + '@opentelemetry/api': 1.9.0 + '@types/node': 25.9.4 transitivePeerDependencies: - msw diff --git a/tests/index.test.ts b/tests/index.test.ts index 34cadb5..5187218 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -165,11 +165,11 @@ describe("package config", () => { }); }); - it("requires Node 22.19 or newer", () => { + it("requires Node 24.15.0 or newer", () => { const pkg = JSON.parse(readFileSync("package.json", "utf8")) as { engines: { node: string }; }; - expect(pkg.engines.node).toBe(">=22.19"); + expect(pkg.engines.node).toBe(">=24.15.0"); }); it("documents shared export entrypoints", () => { From dcd4a156e914039468d396f5ac5b8b7bb7ff963c Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Mon, 22 Jun 2026 02:31:43 -0400 Subject: [PATCH 2/9] chore: upgrade pnpm to 11.8.0 and Node.js to 24 in CI workflows --- .github/workflows/quality.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 1dcd141..2d83876 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -17,13 +17,13 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 11.3.0 + version: 11.8.0 run_install: false - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "22" + node-version: "24" cache: pnpm - name: Install diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 579ec0e..2902b49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,13 +18,13 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 11.3.0 + version: 11.8.0 run_install: false - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "22" + node-version: "24" cache: pnpm registry-url: "https://registry.npmjs.org" From fccf797289d3b513b12befb6a644cdda4d7dff21 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Mon, 22 Jun 2026 02:46:45 -0400 Subject: [PATCH 3/9] chore: upgrade biome schema to 2.5.0 and update configuration with vcs, assist, and linter settings --- biome.json | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/biome.json b/biome.json index 3a3b95c..a81c026 100644 --- a/biome.json +++ b/biome.json @@ -1,10 +1,13 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", - "linter": { + "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json", + "vcs": { "enabled": true, - "rules": { - "recommended": true - } + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": ["src/**/*.ts", "tests/**/*.ts", "!**/node_modules"] }, "formatter": { "enabled": true, @@ -12,13 +15,24 @@ "indentWidth": 2, "lineWidth": 100 }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended" + } + }, "javascript": { "formatter": { "quoteStyle": "double", "semicolons": "always" } }, - "files": { - "includes": ["src/**/*.ts", "tests/**/*.ts", "!**/node_modules"] + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } } } From 2b6b52152d43802dec929ee1b8711059042db31c Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Mon, 22 Jun 2026 02:43:40 -0400 Subject: [PATCH 4/9] docs: add next release docs design spec --- .../2026-06-22-next-release-docs-design.md | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-22-next-release-docs-design.md diff --git a/docs/superpowers/specs/2026-06-22-next-release-docs-design.md b/docs/superpowers/specs/2026-06-22-next-release-docs-design.md new file mode 100644 index 0000000..d0645c3 --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-next-release-docs-design.md @@ -0,0 +1,156 @@ +# Next Release Docs Design + +## Summary + +Prepare the next release by refreshing the project’s release-facing documentation around the current shipped codebase. + +This work covers three surfaces: +- `README.md`: rewrite it as a usage-first guide +- `CHANGELOG.md`: add or refresh the next release entry based on code changes since `v0.4.0` +- `LICENSE`: verify existing MIT licensing stays correctly surfaced + +## Goals + +- Make `README.md` reflect current user-facing usage rather than internal implementation details +- Ensure `CHANGELOG.md` clearly explains what changed for the next release +- Keep licensing aligned across `LICENSE`, `package.json`, and README references +- Keep the scope narrow enough to support a single follow-up implementation plan + +## Non-goals + +- Retroactively rewriting all past changelog entries +- Making feature or behavior changes just to support documentation updates +- Changing the project license +- Performing unrelated repository cleanup + +## Current context + +Current repo state already includes: +- `README.md` +- `CHANGELOG.md` +- `LICENSE` +- package version `0.4.0` + +Commits since `v0.4.0` include user-visible changes around provider toggles, richer dashboard insights, offline extraction improvements, and runtime/tooling updates. + +## Deliverable 1: README.md + +### Intent + +Rewrite `README.md` as a practical usage guide for people installing and operating `@pi-vault/pi-usage`. + +### Required structure + +1. **What it is** + - brief description of the dashboard and its value +2. **Install** + - installation command + - Pi reload step +3. **Commands** + - `/usage` + - `/usage:refresh` + - when to use each +4. **What the dashboard shows** + - offline usage aggregation + - live provider status, quota, and balance surfaces + - currently shipped insights, including project, skill, and MCP breakdowns where applicable +5. **How to use it** + - period switching + - provider tab switching + - row expansion and navigation + - keyboard shortcuts +6. **Configuration** + - provider environment variables for live usage cards + - `usage.json` configuration for provider enable/disable +7. **Changelog pointer** + - short pointer to `CHANGELOG.md` +8. **License** + - short pointer to MIT licensing + +### README style rules + +- Prefer usage-first language over architecture-first language +- Do not describe internal module names, refactors, or test/tooling details unless directly relevant to usage +- Keep examples copy-pasteable +- Only document behavior that exists in the current codebase + +### usage.json section requirements + +Document the config file at: +- `$PI_CODING_AGENT_DIR/extensions/usage.json` + +The README must make the default behavior explicit: +- missing file means all providers remain enabled +- omitted providers remain enabled +- malformed JSON is ignored and falls back to default behavior + +The README should include both: +- the effective default example: `{}` +- an explicit full example that lists the current configurable live providers with `enabled: true`: `openai-codex`, `minimax`, `stepfun`, `opencode-go`, `command-code`, and `openrouter` + +It should also include at least one focused example showing how to disable a provider such as `minimax`. + +## Deliverable 2: CHANGELOG.md + +### Intent + +Update the next release entry so it reflects code changes after `v0.4.0`, written as release notes rather than a raw commit list. + +If the final version has not yet been chosen when editing begins, use an `Unreleased` section first and convert it to the final version heading once the release number is decided. + +### Writing policy + +- Lead with user-visible changes +- Include internal/runtime changes only when they materially affect compatibility, support expectations, or release risk +- Keep older release entries mostly intact +- Allow light consistency edits to nearby text if needed, but do not perform a full historical rewrite + +### Planned content areas + +#### User-visible additions +- provider enable/disable config via `usage.json` +- richer dashboard insights, including: + - project breakdowns + - active skill breakdowns + - MCP server breakdowns + - grouped and capped insight presentation where shipped + +#### Behavior fixes and improvements +- improved offline extraction from session data +- more accurate project-name extraction +- MCP server-name extraction from tool prefixes +- builtin tool handling improvements where they affect insight accuracy +- TUI readability fixes such as spacing or grouped rendering improvements + +#### Notable internal/runtime changes +- Node engine raised to `>=24.15.0` +- dependency refreshes +- CI/runtime alignment updates + +These internal items should appear after user-facing notes and remain brief. + +### Versioning guidance + +The final version bump should be chosen after drafting the release notes. Based on currently known changes, a minor bump appears more likely than a patch because the release includes new shipped capability. + +## Deliverable 3: LICENSE + +### Intent + +Treat licensing as a verification task, not a legal/content change. + +### Requirements + +- keep the existing MIT license text +- verify `LICENSE` remains at repository root +- verify `package.json` still declares `"license": "MIT"` +- verify README badge/linking remains aligned with the root license file +- if inconsistencies exist, fix docs or metadata rather than changing the license itself + +## Success criteria + +This design is complete when the follow-up implementation produces: +- a usage-first `README.md` aligned with current shipped behavior +- a next release changelog entry that accurately summarizes changes since `v0.4.0` +- verified MIT license surfacing across repository files +- enough release-note clarity to choose the next version intentionally From f6344ec3c810e6b6fa7d89cbcb009c48ce5bacba Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Mon, 22 Jun 2026 03:19:37 -0400 Subject: [PATCH 5/9] docs: add implementation plan for refreshing release documentation --- .../2026-06-22-next-release-docs-plan.md | 430 ++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-22-next-release-docs-plan.md diff --git a/docs/superpowers/plans/2026-06-22-next-release-docs-plan.md b/docs/superpowers/plans/2026-06-22-next-release-docs-plan.md new file mode 100644 index 0000000..d006fa9 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-next-release-docs-plan.md @@ -0,0 +1,430 @@ +# Next Release Docs Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Refresh the next release’s documentation surface so `README.md`, `CHANGELOG.md`, and license metadata match the current shipped behavior. + +**Architecture:** Keep the implementation docs-only and stage it in three focused slices: first rewrite `README.md` as a usage guide, then add a release-ready `CHANGELOG.md` entry based on commits after `v0.4.0`, then verify MIT license surfacing and package contents. Avoid code changes, historical changelog rewrites, or version bumps unless explicitly requested during execution. + +**Tech Stack:** Markdown, JSON metadata, git, pnpm + +**Spec:** `docs/superpowers/specs/2026-06-22-next-release-docs-design.md` + +--- + +## File Map + +| File | Action | Responsibility | +| --- | --- | --- | +| `README.md` | Modify | Rewrite as a usage-first guide, update Node badge, add `usage.json` documentation, and remove development-focused framing | +| `CHANGELOG.md` | Modify | Add the next release notes entry summarizing shipped changes since `v0.4.0` | +| `package.json` | Verify | Confirm `license` and packaged `files` metadata stay aligned with the repo root artifacts | +| `LICENSE` | Verify | Confirm the existing MIT text remains present at the repo root | + +--- + +## Task 1: Rewrite README.md as a usage-first guide + +**Files:** +- Modify: `README.md` +- Verify against: `src/shared/types.ts`, `src/index.ts`, `src/core/usage-core.ts`, `package.json` + +- [ ] **Step 1: Replace `README.md` with the full usage-first draft** + +Overwrite `README.md` with this exact content: + +```md +# @pi-vault/pi-usage + +[![npm version](https://img.shields.io/npm/v/%40pi-vault%2Fpi-usage)](https://www.npmjs.com/package/@pi-vault/pi-usage) +[![Quality](https://github.com/pi-vault/pi-usage/actions/workflows/quality.yml/badge.svg?branch=master)](https://github.com/pi-vault/pi-usage/actions/workflows/quality.yml) +[![Node >= 24.15.0](https://img.shields.io/badge/node-%3E%3D24.15.0-339933?logo=node.js&logoColor=white)](https://nodejs.org/) +[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) + +Track Pi usage across your sessions in one dashboard. `@pi-vault/pi-usage` combines offline history with live provider snapshots so you can review costs, tokens, session activity, current quotas, and usage insights without leaving Pi. + +![Pi usage dashboard showing the aggregated usage table for the "All Time" period, current usage quota bars for OpenAI/Codex, and the keyboard navigation hints at the bottom](docs/assets/dashboard-ui.png) + +## Install + +```bash +pi install npm:@pi-vault/pi-usage +``` + +Then reload Pi: + +```bash +/reload +``` + +## Commands + +- `/usage` opens the dashboard using cached live data when available. Use it for quick inspection. +- `/usage:refresh` forces a live refresh, rescans local history, and then opens the dashboard. + +## What the dashboard shows + +### Usage statistics + +The top section aggregates local Pi session history for the selected period. + +- switch between `Today`, `This Week`, `Last Week`, and `All Time` +- expand provider rows to inspect model-level usage +- compare sessions, messages, cost, total tokens, input, output, cache reads, and cache writes +- keep a running total row for everything currently shown + +### Current usage + +The lower section shows live quota and balance information for providers you have already configured. + +- switch between `OpenAI/Codex`, `MiniMax`, `StepFun`, `OpenCode Go`, `Command Code`, and `OpenRouter` +- view rolling-window quota bars like `5h` and weekly usage +- see balance-style fields where the provider exposes them +- get inline status for live, cached, stale, local, or unavailable data + +### Insights + +Press `v` to toggle insights for the selected period. + +- review the most expensive projects in your local session history +- see active skill and MCP server breakdowns when that data is present +- keep long sections readable through grouped insight categories and capped lists with overflow summaries + +## How to use it + +### Keyboard shortcuts + +- `[Tab/Shift-Tab]` switch provider tabs +- `[Left/Right]` switch time period +- `[Up/Down]` move through rows +- `[Enter/Space]` expand or collapse provider rows +- `[v]` toggle insights +- `[q/Esc]` close the dashboard + +## Configuration + +### `usage.json` + +Create `$PI_CODING_AGENT_DIR/extensions/usage.json` to disable specific live providers. + +Default behavior: +- if the file is missing, all providers stay enabled +- if the file is `{}`, all providers stay enabled +- if a provider is omitted, that provider stays enabled +- if the JSON is malformed, `@pi-vault/pi-usage` ignores it and falls back to the default behavior + +Default example: + +```json +{} +``` + +Explicit all-providers-enabled example: + +```json +{ + "providers": { + "openai-codex": { "enabled": true }, + "minimax": { "enabled": true }, + "stepfun": { "enabled": true }, + "opencode-go": { "enabled": true }, + "command-code": { "enabled": true }, + "openrouter": { "enabled": true } + } +} +``` + +Disable MiniMax only: + +```json +{ + "providers": { + "minimax": { "enabled": false } + } +} +``` + +### Provider setup + +Offline history works without extra setup. Live provider cards appear only for providers you have both configured and left enabled. + +#### OpenAI/Codex + +Pi usage can reuse existing Pi or Codex auth. Optional overrides: + +- `OPENAI_CODEX_OAUTH_TOKEN` +- `OPENAI_CODEX_ACCESS_TOKEN` +- `CODEX_OAUTH_TOKEN` +- `CODEX_ACCESS_TOKEN` +- `OPENAI_CODEX_ACCOUNT_ID` +- `CHATGPT_ACCOUNT_ID` + +#### MiniMax + +Set one of: + +- `MINIMAX_CODING_API_KEY` +- `MINIMAX_API_KEY` + +Optional override: + +- `MINIMAX_API_HOST` + +#### StepFun + +Set one of: + +- `STEPFUN_TOKEN` +- `STEPFUN_USERNAME` and `STEPFUN_PASSWORD` + +#### OpenCode Go + +Set: + +- `OPENCODE_GO_COOKIE_HEADER` +- `OPENCODE_GO_WORKSPACE_ID` + +`OPENCODE_GO_WORKSPACE_ID` accepts either the raw `wrk_...` id or the full workspace URL. + +#### Command Code + +Set: + +- `COMMAND_CODE_COOKIE_HEADER` + +#### OpenRouter + +Set: + +- `OPENROUTER_API_KEY` + +Optional overrides: + +- `OPENROUTER_API_URL` +- `OPENROUTER_X_TITLE` +- `OPENROUTER_HTTP_REFERER` + +## Changelog + +See [`CHANGELOG.md`](CHANGELOG.md) for release-by-release notes. + +## License + +MIT — see [`LICENSE`](LICENSE). +``` + +- [ ] **Step 2: Verify README claims against the codebase** + +Run: + +```bash +rg -n '"node":|"license":|"files"' package.json +sed -n '1,20p' src/shared/types.ts +rg -n 'registerCommand\("usage|registerCommand\("usage:refresh' src/index.ts +rg -n 'join\(deps.agentDir\(\), "extensions", "usage.json"\)|return \{\}' src/core/usage-core.ts +rg -n 'treats missing config file as all providers enabled|ignores malformed config JSON' tests/usage-core.test.ts +``` + +Expected: +- `package.json` shows Node `>=24.15.0` and license `MIT` +- `src/shared/types.ts` lists `offline` plus the configurable live providers: `openai-codex`, `minimax`, `stepfun`, `opencode-go`, `command-code`, and `openrouter` +- `src/index.ts` still registers `/usage` and `/usage:refresh` +- `src/core/usage-core.ts` loads `$PI_CODING_AGENT_DIR/extensions/usage.json` and falls back to `{}` on errors +- `tests/usage-core.test.ts` explicitly covers both the missing-file and malformed-JSON behaviors + +- [ ] **Step 3: Read the rendered README sections once before moving on** + +Run: + +```bash +rg -n '^## |^### |usage.json|CHANGELOG|LICENSE|Node >=' README.md +``` + +Expected headings present: +- `## Install` +- `## Commands` +- `## What the dashboard shows` +- `## How to use it` +- `## Configuration` +- `## Changelog` +- `## License` + +Also confirm there is no `## Development` section anymore. + +- [ ] **Step 4: Commit the README refresh** + +```bash +git add README.md +git commit -m "docs: refresh README for current usage flow" +``` + +--- + +## Task 2: Add the next release entry to CHANGELOG.md + +**Files:** +- Modify: `CHANGELOG.md` +- Verify against: `git log v0.4.0..HEAD` + +- [ ] **Step 1: Insert an `Unreleased` section above `## v0.4.0`** + +Add this exact block after the intro paragraph in `CHANGELOG.md`: + +```md +## Unreleased + +- added `usage.json` provider toggles so you can disable live providers you do not want queried +- added dashboard insights for project, active skill, and MCP server breakdowns +- grouped insight rows by category and cap each insight section with an overflow summary to keep the dashboard readable +- improved offline session parsing to extract project names, active skills, MCP server names, and builtin tool usage more accurately +- fixed dashboard readability around usage statistics spacing and grouped insight rendering +- updated runtime requirements and tooling to Node `>=24.15.0`, refreshed dependencies, and aligned CI with Node 24 and pnpm 11.8.0 +``` + +Do not rewrite older version sections in this task. + +- [ ] **Step 2: Verify every bullet against the commit history** + +Run: + +```bash +git log --oneline v0.4.0..HEAD +``` + +Expected supporting commits include: +- `feat(core): add provider enable/disable toggle via extensions/usage.json` +- `feat(insights): add project breakdown by cost` +- `feat(insights): add skill and MCP server breakdowns` +- `feat(tui): render insights grouped by category` +- `feat(insights): cap project insights at 5 with overflow summary` +- `fix(offline): expand BUILTIN_TOOLS from real session data, rename cap constant` +- `chore: update node engine to 24.15.0, refresh package dependencies, and adjust project metadata` +- `chore: upgrade pnpm to 11.8.0 and Node.js to 24 in CI workflows` + +If a bullet cannot be traced to the log, fix the wording now rather than carrying a vague release note forward. + +- [ ] **Step 3: Decide whether to keep `Unreleased` or rename it during the actual release cut** + +During this plan’s implementation, leave the heading as `## Unreleased` unless the user explicitly provides the final version number. If the release number is known during execution, rename the heading to `## vX.Y.Z` only after the wording is finalized. + +- [ ] **Step 4: Read the top of the changelog after editing** + +Run: + +```bash +sed -n '1,30p' CHANGELOG.md +``` + +Expected: +- the file starts with `# Changelog` +- the intro paragraph stays intact +- `## Unreleased` appears before `## v0.4.0` +- the new bullets read cleanly and do not duplicate older entries verbatim + +- [ ] **Step 5: Commit the changelog update** + +```bash +git add CHANGELOG.md +git commit -m "docs: add unreleased release notes" +``` + +--- + +## Task 3: Verify MIT license surfacing and packaged release files + +**Files:** +- Verify: `LICENSE` +- Verify: `package.json` +- Verify: `README.md` +- Verify: `CHANGELOG.md` + +- [ ] **Step 1: Confirm MIT license metadata and root files are aligned** + +Run: + +```bash +test -f LICENSE && echo "LICENSE present" +rg -n '"license": "MIT"' package.json +rg -n '\[!\[License: MIT\]|\]\(LICENSE\)|\[`LICENSE`\]\(LICENSE\)' README.md +rg -n '"files"|LICENSE|CHANGELOG.md|README.md' package.json +``` + +Expected: +- `LICENSE present` +- `package.json` contains `"license": "MIT"` +- `README.md` includes both the MIT badge and the final license link +- `package.json` includes `LICENSE`, `CHANGELOG.md`, and `README.md` in the packaged file list + +- [ ] **Step 2: If any license surface is inconsistent, fix it immediately** + +Use these exact targets if a repair is needed: + +```json +{ + "license": "MIT", + "files": [ + "src", + "docs/assets", + "LICENSE", + "CHANGELOG.md", + "README.md" + ] +} +``` + +For README fixes, keep the license badge and final section in this form: + +```md +[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) +``` + +```md +## License + +MIT — see [`LICENSE`](LICENSE). +``` + +If all checks already pass, make no file edits in this step. + +- [ ] **Step 3: Run package and repo verification** + +Run: + +```bash +pnpm pack --dry-run +pnpm check +git diff --check +``` + +Expected: +- `pnpm pack --dry-run` succeeds and includes `LICENSE`, `CHANGELOG.md`, `README.md`, and `docs/assets/dashboard-ui.png` +- `pnpm check` passes +- `git diff --check` prints no whitespace errors + +- [ ] **Step 4: Create the final documentation commit if this task changed any files** + +If Step 2 required edits: + +```bash +git add package.json README.md LICENSE CHANGELOG.md +git commit -m "docs: align license metadata and package contents" +``` + +If Step 2 made no edits, skip this commit. + +--- + +## Self-Review Checklist + +- Spec coverage: + - README usage-only rewrite: covered by Task 1 + - `usage.json` defaults and explicit examples: covered by Task 1 + - next release notes based on post-`v0.4.0` commits: covered by Task 2 + - MIT license verification and package surfacing: covered by Task 3 +- Placeholder scan: + - no `TBD`, `TODO`, or “similar to above” placeholders remain + - every edit step includes exact markdown or command targets +- Type and naming consistency: + - provider IDs in the README match `ProviderId` names in `src/shared/types.ts` + - the changelog references shipped features only + - the plan leaves the version heading as `Unreleased` until the release number is explicitly chosen From c192c8dea90ab1a29abcc296f90062214c0cbfc2 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Mon, 22 Jun 2026 02:56:01 -0400 Subject: [PATCH 6/9] docs: refresh README for current usage flow --- README.md | 91 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 3b3d660..22ab9a1 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![npm version](https://img.shields.io/npm/v/%40pi-vault%2Fpi-usage)](https://www.npmjs.com/package/@pi-vault/pi-usage) [![Quality](https://github.com/pi-vault/pi-usage/actions/workflows/quality.yml/badge.svg?branch=master)](https://github.com/pi-vault/pi-usage/actions/workflows/quality.yml) -[![Node >= 22.19](https://img.shields.io/badge/node-%3E%3D22.19-339933?logo=node.js&logoColor=white)](https://nodejs.org/) +[![Node >= 24.15.0](https://img.shields.io/badge/node-%3E%3D24.15.0-339933?logo=node.js&logoColor=white)](https://nodejs.org/) [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) -Track Pi usage across your sessions in one dashboard. `@pi-vault/pi-usage` combines offline history with live provider snapshots so you can see costs, tokens, session activity, and rolling quota status without leaving Pi. +Track Pi usage across your sessions in one dashboard. `@pi-vault/pi-usage` combines offline history with live provider snapshots so you can review costs, tokens, session activity, current quotas, and usage insights without leaving Pi. ![Pi usage dashboard showing the aggregated usage table for the "All Time" period, current usage quota bars for OpenAI/Codex, and the keyboard navigation hints at the bottom](docs/assets/dashboard-ui.png) @@ -26,11 +26,11 @@ Then reload Pi: - `/usage` opens the dashboard using cached live data when available. Use it for quick inspection. - `/usage:refresh` forces a live refresh, rescans local history, and then opens the dashboard. -## What You Get +## What the dashboard shows ### Usage statistics -The top section aggregates all local Pi session history for the selected period. +The top section aggregates local Pi session history for the selected period. - switch between `Today`, `This Week`, `Last Week`, and `All Time` - expand provider rows to inspect model-level usage @@ -39,12 +39,22 @@ The top section aggregates all local Pi session history for the selected period. ### Current usage -The lower section shows live quota and balance information for providers you have already configured in Pi. +The lower section shows the supported providers. Configured providers can return live quota and balance data, while unconfigured ones may show `unavailable` or local fallback states. - switch between `OpenAI/Codex`, `MiniMax`, `StepFun`, `OpenCode Go`, `Command Code`, and `OpenRouter` - view rolling-window quota bars like `5h` and weekly usage - see balance-style fields where the provider exposes them -- get inline status for live, cached, stale, or fallback data +- get inline status for live, cached, stale, local, or unavailable data + +### Insights + +Press `v` to toggle insights for the selected period. + +- review the most expensive projects in your local session history +- see active skill and MCP server breakdowns when that data is present +- keep long sections readable through grouped insight categories and capped lists with overflow summaries + +## How to use it ### Keyboard shortcuts @@ -55,11 +65,54 @@ The lower section shows live quota and balance information for providers you hav - `[v]` toggle insights - `[q/Esc]` close the dashboard -## Provider Setup +## Configuration + +### `usage.json` + +Create `$PI_CODING_AGENT_DIR/extensions/usage.json` to disable specific live providers. + +Default behavior: +- if the file is missing, all providers stay enabled +- if the file is `{}`, all providers stay enabled +- if a provider is omitted, that provider stays enabled +- if the JSON is malformed, `@pi-vault/pi-usage` ignores it and falls back to the default behavior + +Default example: + +```json +{} +``` + +Explicit all-providers-enabled example: + +```json +{ + "providers": { + "openai-codex": { "enabled": true }, + "minimax": { "enabled": true }, + "stepfun": { "enabled": true }, + "opencode-go": { "enabled": true }, + "command-code": { "enabled": true }, + "openrouter": { "enabled": true } + } +} +``` -Offline history works without extra setup. Live provider cards appear only for providers you have already configured. +Disable MiniMax only: + +```json +{ + "providers": { + "minimax": { "enabled": false } + } +} +``` -### OpenAI/Codex +### Provider setup + +Offline history works without extra setup. Provider cards appear for every supported live provider unless you disable them in `usage.json`. Providers you configure can return live data; others may show `unavailable` or local fallback states. + +#### OpenAI/Codex Pi usage can reuse existing Pi or Codex auth. Optional overrides: @@ -70,7 +123,7 @@ Pi usage can reuse existing Pi or Codex auth. Optional overrides: - `OPENAI_CODEX_ACCOUNT_ID` - `CHATGPT_ACCOUNT_ID` -### MiniMax +#### MiniMax Set one of: @@ -81,14 +134,14 @@ Optional override: - `MINIMAX_API_HOST` -### StepFun +#### StepFun Set one of: - `STEPFUN_TOKEN` - `STEPFUN_USERNAME` and `STEPFUN_PASSWORD` -### OpenCode Go +#### OpenCode Go Set: @@ -97,13 +150,13 @@ Set: `OPENCODE_GO_WORKSPACE_ID` accepts either the raw `wrk_...` id or the full workspace URL. -### Command Code +#### Command Code Set: - `COMMAND_CODE_COOKIE_HEADER` -### OpenRouter +#### OpenRouter Set: @@ -115,14 +168,10 @@ Optional overrides: - `OPENROUTER_X_TITLE` - `OPENROUTER_HTTP_REFERER` -## Development +## Changelog -```bash -pnpm install -pnpm check -pnpm pack --dry-run -``` +See [`CHANGELOG.md`](CHANGELOG.md) for release-by-release notes. ## License -MIT +MIT — see [`LICENSE`](LICENSE). From 57046ac03b03867fe147363cc0c9a21edbf276c1 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Mon, 22 Jun 2026 03:23:35 -0400 Subject: [PATCH 7/9] chore: bump version to 0.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 69f2b7e..81f4590 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pi-vault/pi-usage", - "version": "0.4.0", + "version": "0.5.0", "type": "module", "description": "Pi extension that displays aggregated usage statistics across all sessions", "author": "Lanh Hoang ", From be75a51f762a7bb1fc6a4613f6aee36ef4647abb Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Mon, 22 Jun 2026 03:23:40 -0400 Subject: [PATCH 8/9] docs: add changelog entries for v0.5.0 features, improvements, and environment updates --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5066c6..ac0f002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `@pi-vault/pi-usage` are documented here. +## v0.5.0 + +- added `usage.json` provider toggles so you can disable live providers you do not want queried +- added dashboard insights for project, active skill, and MCP server breakdowns +- grouped insight rows by category and cap each insight section with an overflow summary to keep the dashboard readable +- improved offline session parsing to extract project names, active skills, MCP server names, and builtin tool usage more accurately +- fixed dashboard readability around usage statistics spacing and grouped insight rendering +- updated runtime requirements and tooling to Node `>=24.15.0`, refreshed dependencies, and aligned CI with Node 24 and pnpm 11.8.0 + ## v0.4.0 - reorganized the extension around a dedicated `UsageCore`, with the package entrypoint reduced to a thin Pi adapter From 91ad06cba14f8f281c2924e1564a551ff0d236bb Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Mon, 22 Jun 2026 03:20:43 -0400 Subject: [PATCH 9/9] chore: remove obsolete documentation plans and design specifications --- .../2026-06-21-dashboard-enhancements-plan.md | 1405 ----------------- .../plans/2026-06-21-phase-1-spacing-fix.md | 93 -- .../2026-06-21-phase-2-provider-toggle.md | 259 --- .../2026-06-21-phase-3-project-insights.md | 695 -------- .../2026-06-21-phase-4-skill-mcp-insights.md | 679 -------- .../2026-06-22-next-release-docs-plan.md | 430 ----- ...026-06-21-dashboard-enhancements-design.md | 239 --- .../2026-06-21-project-insights-cap-design.md | 46 - .../2026-06-22-next-release-docs-design.md | 156 -- 9 files changed, 4002 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-21-dashboard-enhancements-plan.md delete mode 100644 docs/superpowers/plans/2026-06-21-phase-1-spacing-fix.md delete mode 100644 docs/superpowers/plans/2026-06-21-phase-2-provider-toggle.md delete mode 100644 docs/superpowers/plans/2026-06-21-phase-3-project-insights.md delete mode 100644 docs/superpowers/plans/2026-06-21-phase-4-skill-mcp-insights.md delete mode 100644 docs/superpowers/plans/2026-06-22-next-release-docs-plan.md delete mode 100644 docs/superpowers/specs/2026-06-21-dashboard-enhancements-design.md delete mode 100644 docs/superpowers/specs/2026-06-21-project-insights-cap-design.md delete mode 100644 docs/superpowers/specs/2026-06-22-next-release-docs-design.md diff --git a/docs/superpowers/plans/2026-06-21-dashboard-enhancements-plan.md b/docs/superpowers/plans/2026-06-21-dashboard-enhancements-plan.md deleted file mode 100644 index e66ee89..0000000 --- a/docs/superpowers/plans/2026-06-21-dashboard-enhancements-plan.md +++ /dev/null @@ -1,1405 +0,0 @@ -# Dashboard Enhancements Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add provider enable/disable config, richer insights (project/skill/MCP breakdowns), and a spacing fix to the pi-usage dashboard. - -**Architecture:** Four independent phases, each producing a usable result. Phase 1 is a one-line spacing fix. Phase 2 adds config-driven provider toggling. Phases 3-4 enrich the offline scan and dashboard insights view with project, skill, and MCP server breakdowns. - -**Tech Stack:** TypeScript, Vitest, Pi extension API (`@earendil-works/pi-coding-agent`), TUI rendering - -**Spec:** `docs/superpowers/specs/2026-06-21-dashboard-enhancements-design.md` - ---- - -## File Map - -| File | Action | Responsibility | -| -------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| `src/tui/dashboard.ts` | Modify | Phase 1: spacing fix; Phase 3: category-grouped insights rendering | -| `src/shared/types.ts` | Modify | Phase 2: `UsageConfig` type; Phase 3: `category` field on `UsageInsight` | -| `src/core/usage-core.ts` | Modify | Phase 2: load config, filter disabled providers | -| `src/core/offline.ts` | Modify | Phase 3: extract CWD, enrich `UsageTurn`; Phase 3: project insights in `buildInsights()`; Phase 4: extract skills + MCP tools, add skill/MCP insights | -| `tests/dashboard.test.ts` | Modify | Phase 1: adjust for spacing; Phase 3: insights rendering tests | -| `tests/offline.test.ts` | Modify | Phase 3: project extraction + project insights tests; Phase 4: skill + MCP extraction + insights tests | -| `tests/usage-core.test.ts` | Modify | Phase 2: config loading + provider filtering tests | - ---- - -## Phase 1: Spacing Fix - -### Task 1.1: Add empty line between "Usage Statistics" title and period tabs - -**Files:** - -- Modify: `src/tui/dashboard.ts:364-377` -- Modify: `tests/dashboard.test.ts` (affected rendering assertions) - -- [ ] **Step 1: Update the rendering method** - -In `src/tui/dashboard.ts`, method `renderUsageStatistics()`, add an empty line between the section title and the tabs: - -```ts -// Find this block (lines 364-377): -private renderUsageStatistics(w: number, lines: string[]): void { - lines.push( - this.sectionTitle( - UI_STRINGS.dashboardBorderedSectionTitles.usageStatistics, - ), - ); - - lines.push( - ...this.renderTabs( - PERIODS.map((period) => PERIOD_LABELS[period]), - this.periodIndex, - w, - ), - ); - -// Replace with: -private renderUsageStatistics(w: number, lines: string[]): void { - lines.push( - this.sectionTitle( - UI_STRINGS.dashboardBorderedSectionTitles.usageStatistics, - ), - ); - lines.push(""); - - lines.push( - ...this.renderTabs( - PERIODS.map((period) => PERIOD_LABELS[period]), - this.periodIndex, - w, - ), - ); -``` - -- [ ] **Step 2: Run tests to check what breaks** - -Run: `pnpm test -- tests/dashboard.test.ts` - -Some render snapshot assertions may fail because they expect the old line ordering. Note which tests fail. - -- [ ] **Step 3: Fix any failing dashboard tests** - -If tests assert exact line positions or patterns that now shift by one line due to the empty line, update those assertions. The empty line appears between the "Usage Statistics" title and the `[All Time]` tab line in the render output. - -- [ ] **Step 4: Run full check** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Step 5: Commit** - -```bash -git add src/tui/dashboard.ts tests/dashboard.test.ts -git commit -m "fix(tui): add spacing between Usage Statistics title and period tabs" -``` - ---- - -## Phase 2: Provider Enable/Disable Toggle - -### Task 2.1: Add UsageConfig type - -**Files:** - -- Modify: `src/shared/types.ts` - -- [ ] **Step 1: Add the UsageConfig interface** - -Append after the `UsageDashboardState` interface (around line 136) in `src/shared/types.ts`: - -```ts -export interface UsageConfig { - providers?: Partial>; -} -``` - -- [ ] **Step 2: Run typecheck** - -Run: `pnpm typecheck` - -Expected: PASS (new type, no consumers yet). - -- [ ] **Step 3: Commit** - -```bash -git add src/shared/types.ts -git commit -m "feat(types): add UsageConfig interface for provider toggle" -``` - -### Task 2.2: Implement loadConfig and provider filtering - -**Files:** - -- Modify: `src/core/usage-core.ts` -- Modify: `tests/usage-core.test.ts` - -- [ ] **Step 1: Write failing tests for config loading** - -Add a new `describe("config loading", ...)` block at the end of `tests/usage-core.test.ts`: - -```ts -import { join } from "node:path"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; - -// Add this describe block after the existing test blocks: - -describe("config loading", () => { - function mkTmp(): string { - return mkdtempSync(join(tmpdir(), "pi-usage-config-")); - } - - it("loads config and filters disabled providers", async () => { - const root = mkTmp(); - const extDir = join(root, "extensions"); - mkdirSync(extDir, { recursive: true }); - writeFileSync( - join(extDir, "usage.json"), - JSON.stringify({ providers: { minimax: { enabled: false } } }), - ); - // Create a session file so bootstrap has something to scan - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - writeFileSync(join(sessions, "s.jsonl"), "", "utf8"); - - const core = createUsageCore({ - deps: createTestDeps(root), - onEmit: () => {}, - }); - await core.bootstrap(); - const state = core.getState(); - - // minimax should be excluded from providers - expect( - state.providers.find((p) => p.providerId === "minimax"), - ).toBeUndefined(); - // other providers should still be present - expect( - state.providers.find((p) => p.providerId === "openai-codex"), - ).toBeDefined(); - - rmSync(root, { recursive: true, force: true }); - }); - - it("treats missing config file as all providers enabled", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - writeFileSync(join(sessions, "s.jsonl"), "", "utf8"); - - const core = createUsageCore({ - deps: createTestDeps(root), - onEmit: () => {}, - }); - await core.bootstrap(); - const state = core.getState(); - - // All live providers should be present (though unavailable without credentials) - const ids = state.providers.map((p) => p.providerId); - expect(ids).toContain("openai-codex"); - expect(ids).toContain("minimax"); - - rmSync(root, { recursive: true, force: true }); - }); - - it("ignores malformed config JSON", async () => { - const root = mkTmp(); - const extDir = join(root, "extensions"); - mkdirSync(extDir, { recursive: true }); - writeFileSync(join(extDir, "usage.json"), "not json!!!"); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - writeFileSync(join(sessions, "s.jsonl"), "", "utf8"); - - const core = createUsageCore({ - deps: createTestDeps(root), - onEmit: () => {}, - }); - await core.bootstrap(); - const state = core.getState(); - - // Should not crash, all providers present - const ids = state.providers.map((p) => p.providerId); - expect(ids).toContain("minimax"); - - rmSync(root, { recursive: true, force: true }); - }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/usage-core.test.ts` - -Expected: FAIL — `createTestDeps` may not exist in that file (check the existing helper name; it may be `createTestDeps` or inline). Also, the config loading logic doesn't exist yet so the disabled-provider test should fail. - -- [ ] **Step 3: Implement loadConfig and filtering in usage-core.ts** - -In `src/core/usage-core.ts`, add the `loadConfig` function and update `createUsageCore`: - -```ts -// Add import at the top: -import { join } from "node:path"; -import type { UsageConfig } from "../shared/types.ts"; - -// Add this function before createUsageCore: -async function loadConfig(deps: UsageDeps): Promise { - try { - const configPath = join(deps.agentDir(), "extensions", "usage.json"); - const raw = await deps.readFile(configPath, "utf8"); - return JSON.parse(raw as string) as UsageConfig; - } catch { - return {}; - } -} -``` - -Then change `createUsageCore` to accept config and filter providers. The cleanest approach: make `createUsageCore` async or add an `init()` step. Since `bootstrap()` is already async and called before anything else, load config there and filter before the first fetch. - -Update the function: - -```ts -export function createUsageCore(options: UsageCoreOptions): UsageCore { - const { deps, onEmit } = options; - - // --- Provider registry (created once, filtered after config load) --- - let providers = createProviderRegistry(deps); - let liveProviderIds = new Set( - providers.filter((p) => p.strategy === "api").map((p) => p.id), - ); - let liveProviderSnapshotFiles = new Set( - [...liveProviderIds].map((id) => `${id}.json`), - ); - - // ... (rest of state and internal variables unchanged) ... - - async function bootstrap(): Promise { - const config = await loadConfig(deps); - if (config.providers) { - providers = providers.filter((p) => { - const setting = config.providers?.[p.id]; - return setting?.enabled !== false; - }); - liveProviderIds = new Set( - providers.filter((p) => p.strategy === "api").map((p) => p.id), - ); - liveProviderSnapshotFiles = new Set( - [...liveProviderIds].map((id) => `${id}.json`), - ); - } - await Promise.all([populateProviders(false), refreshOffline(false)]); - state.diagnostics = ["live runtime ready"]; - emit(USAGE_CORE_READY_EVENT); - } - - // ... rest unchanged ... -} -``` - -Note: change `const providers` to `let providers`, `const liveProviderIds` to `let liveProviderIds`, `const liveProviderSnapshotFiles` to `let liveProviderSnapshotFiles`. - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/usage-core.test.ts` - -Expected: PASS — all existing tests plus the new config tests. - -- [ ] **Step 5: Run full check** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/core/usage-core.ts tests/usage-core.test.ts -git commit -m "feat(core): add provider enable/disable toggle via extensions/usage.json" -``` - ---- - -## Phase 3: Insight Infrastructure + Project Breakdown - -### Task 3.1: Add category field to InsightItem - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `src/shared/types.ts` - -- [ ] **Step 1: Add category to InsightItem in offline.ts** - -In `src/core/offline.ts`, update the `InsightItem` interface (around line 287): - -```ts -export interface InsightItem { - category?: string; - label: string; - cost: number; - detail: string; -} -``` - -- [ ] **Step 2: Add category to UsageInsight in types.ts** - -In `src/shared/types.ts`, update the `UsageInsight` interface (around line 108): - -```ts -export interface UsageInsight { - category?: string; - label: string; - cost: number; - detail: string; -} -``` - -- [ ] **Step 3: Tag existing insights with category "cost"** - -In `src/core/offline.ts`, in `buildInsights()`, add `category: "cost"` to each of the five existing insight items in the return array (around line 344): - -```ts -return [ - { - category: "cost", - label: "Parallel sessions", - cost: parallelCost, - detail: `${pct(parallelCost)} cost while >=4 active`, - }, - { - category: "cost", - label: "Large context", - cost: largeContext, - detail: `${pct(largeContext)} over 150k context`, - }, - { - category: "cost", - label: "Large uncached", - cost: largeUncached, - detail: `${pct(largeUncached)} over 100k input`, - }, - { - category: "cost", - label: "Long sessions", - cost: longSessionCost, - detail: `${pct(longSessionCost)} from 8h+ sessions`, - }, - { - category: "cost", - label: "Top-5 concentration", - cost: top5, - detail: `${pct(top5)} in top 5 sessions`, - }, -]; -``` - -- [ ] **Step 4: Run tests** - -Run: `pnpm check` - -Expected: PASS — category is optional, existing tests don't assert on it. - -- [ ] **Step 5: Commit** - -```bash -git add src/core/offline.ts src/shared/types.ts -git commit -m "feat(types): add category field to InsightItem and UsageInsight" -``` - -### Task 3.2: Extract session CWD and add project field to UsageTurn - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing test for project extraction** - -Add to the `describe("offline scanner", ...)` block in `tests/offline.test.ts`: - -```ts -it("extracts project name from session header cwd", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions", "proj"); - mkdirSync(sessions, { recursive: true }); - const sessionHeader = JSON.stringify({ - type: "session", - version: 3, - id: "test-session", - timestamp: "2026-05-30T10:00:00Z", - cwd: "/Users/dev/career-ops", - }); - const message = JSON.stringify({ - type: "message", - id: "m1", - timestamp: "2026-05-30T11:00:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "MiniMax-M2.7", - usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, cost: 0.5 }, - }, - }); - writeFileSync( - join(sessions, "s.jsonl"), - `${sessionHeader}\n${message}\n`, - "utf8", - ); - const result = await scanOfflineUsage({ - ...createDefaultDeps(), - agentDir: () => root, - now: () => Date.parse("2026-05-30T12:00:00Z"), - }); - expect(result.turns).toHaveLength(1); - expect(result.turns[0].project).toBe("career-ops"); - rmSync(root, { recursive: true, force: true }); -}); - -it("falls back to undefined project when no session header", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - const message = JSON.stringify({ - type: "message", - id: "m1", - timestamp: "2026-05-30T11:00:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "m", - usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, cost: 0.1 }, - }, - }); - writeFileSync(join(sessions, "s.jsonl"), `${message}\n`, "utf8"); - const result = await scanOfflineUsage({ - ...createDefaultDeps(), - agentDir: () => root, - }); - expect(result.turns[0].project).toBeUndefined(); - rmSync(root, { recursive: true, force: true }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — `project` property doesn't exist on `UsageTurn` yet. - -- [ ] **Step 3: Implement CWD extraction** - -In `src/core/offline.ts`: - -1. Add `project` to the `UsageTurn` interface: - -```ts -export interface UsageTurn { - id: string; - sessionId: string; - timestamp: number; - provider: string; - model: string; - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - tokens: number; - cost: number; - project?: string; -} -``` - -2. Add a helper to extract project from CWD: - -```ts -function projectFromCwd(cwd: unknown): string | undefined { - if (typeof cwd !== "string" || !cwd) return undefined; - const segments = cwd.replace(/\/+$/, "").split("/"); - return segments[segments.length - 1] || undefined; -} -``` - -3. In `scanOfflineUsage()`, inside the file-reading loop (around line 234 where each JSONL file is processed line by line), track the session's CWD. Currently the code reads lines from each file. Add session-level state tracking: - -```ts -// Inside the per-file loop, before iterating lines: -let sessionProject: string | undefined; - -// When parsing each line, before the existing parseLine call: -// Check for session header to extract CWD -try { - const parsed = JSON.parse(line) as Record; - if (parsed?.type === "session" && parsed.cwd) { - sessionProject = projectFromCwd(parsed.cwd); - continue; // session headers are not turns - } -} catch { - // fall through to existing parseLine logic -} -``` - -4. After `parseLine()` returns a turn, set `turn.project = sessionProject`: - -```ts -const turn = parseLine(line, sessionId); -if (!turn) continue; -turn.project = sessionProject; -``` - -Note: The exact location depends on the current loop structure. The key change is: parse `type: "session"` entries to extract `cwd`, store as session-level state, and apply to each turn in that file. - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(offline): extract project name from session CWD" -``` - -### Task 3.3: Add project insights to buildInsights - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing test for project insights** - -Add to the `describe("insights", ...)` block in `tests/offline.test.ts`: - -```ts -it("produces project breakdown insights", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 9, - project: "career-ops", - }, - { - id: "2", - sessionId: "s2", - timestamp: 2, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 1, - project: "dotfiles", - }, - ]; - const insights = buildInsights(turns); - const projectInsights = insights.filter((i) => i.category === "project"); - expect(projectInsights.length).toBeGreaterThanOrEqual(2); - expect(projectInsights[0].label).toBe("career-ops"); - expect(projectInsights[0].detail).toContain("90.0%"); - expect(projectInsights[1].label).toBe("dotfiles"); -}); - -it("omits project insights when no projects are set", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 1, - }, - ]; - const insights = buildInsights(turns); - const projectInsights = insights.filter((i) => i.category === "project"); - expect(projectInsights).toHaveLength(0); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — no insights with `category: "project"` produced yet. - -- [ ] **Step 3: Implement project insights in buildInsights** - -In `src/core/offline.ts`, in the `buildInsights()` function, add project grouping before the return statement: - -```ts -// After the existing top5 calculation, before the return: -const byProject = new Map(); -for (const t of turns) { - if (t.project) { - byProject.set(t.project, (byProject.get(t.project) ?? 0) + t.cost); - } -} -const projectInsights: InsightItem[] = [...byProject.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([project, cost]) => ({ - category: "project", - label: project, - cost, - detail: pct(cost), - })); - -// Update the return to prepend project insights: -return [ - ...projectInsights, - // ... existing 5 cost insights (with category: "cost") ... -]; -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Update existing insight count test** - -The existing test `"produces five insight rows"` asserts `toHaveLength(5)`. Since those turns have no `project` set, it should still be 5. Verify this by running: - -Run: `pnpm test -- tests/offline.test.ts` - -If the test still passes, no change needed. If it fails because the test turns now produce project insights (unlikely since they have no `project` field), update the assertion. - -- [ ] **Step 6: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(insights): add project breakdown by cost" -``` - -### Task 3.4: Category-grouped insights rendering in dashboard - -**Files:** - -- Modify: `src/tui/dashboard.ts` -- Modify: `tests/dashboard.test.ts` - -- [ ] **Step 1: Write failing test for category-grouped rendering** - -Add a new test in the `describe("dashboard rendering", ...)` block in `tests/dashboard.test.ts`: - -```ts -it("renders insights grouped by category", () => { - const state = mkState(); - state.insights = [ - { category: "project", label: "career-ops", cost: 9, detail: "90.0%" }, - { category: "project", label: "dotfiles", cost: 1, detail: "10.0%" }, - { - category: "cost", - label: "Large context", - cost: 5, - detail: "50.0% over 150k context", - }, - ]; - const c = new UsageDashboardComponent(state, () => undefined, { - theme: noTheme, - }); - c.handleInput("v"); - const out = c.render(100).join("\n"); - expect(out).toContain("Projects"); - expect(out).toContain("career-ops"); - expect(out).toContain("90.0%"); - expect(out).toContain("Cost patterns"); - expect(out).toContain("Large context"); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pnpm test -- tests/dashboard.test.ts` - -Expected: FAIL — "Projects" category header not rendered. - -- [ ] **Step 3: Implement category-grouped rendering** - -In `src/tui/dashboard.ts`, update the insights rendering block inside `renderUsageStatistics()` (around lines 383-398). Replace the flat list rendering: - -```ts -// Find this block: -if (this.showInsights) { - lines.push( - this.sectionTitle(UI_STRINGS.dashboardBorderedSectionTitles.insights), - ); - if (this.state.insights.length === 0) { - lines.push(this.theme.dim("No insights yet.")); - } else { - for (const item of this.state.insights) { - lines.push( - this.theme.dim( - `- ${item.label}: ${formatCurrency(item.cost)} (${item.detail})`, - ), - ); - } - } - return; -} - -// Replace with: -if (this.showInsights) { - lines.push( - this.sectionTitle(UI_STRINGS.dashboardBorderedSectionTitles.insights), - ); - if (this.state.insights.length === 0) { - lines.push(this.theme.dim("No insights yet.")); - } else { - lines.push(...this.renderInsightsByCategory(w)); - } - return; -} -``` - -Then add the `renderInsightsByCategory` method to the class: - -```ts -private renderInsightsByCategory(w: number): string[] { - const lines: string[] = []; - const categoryOrder = ["project", "skill", "mcp", "cost"]; - const categoryLabels: Record = { - project: "Projects", - skill: "Skills", - mcp: "MCP servers", - cost: "Cost patterns", - }; - - // Group insights by category - const grouped = new Map(); - for (const item of this.state.insights) { - const cat = item.category ?? "cost"; - const list = grouped.get(cat) ?? []; - list.push(item); - grouped.set(cat, list); - } - - for (const cat of categoryOrder) { - const items = grouped.get(cat); - if (!items || items.length === 0) continue; - lines.push(""); - const header = categoryLabels[cat] ?? cat; - if (cat === "cost") { - lines.push(this.theme.dim(header)); - for (const item of items) { - lines.push( - this.theme.dim( - ` - ${item.label}: ${formatCurrency(item.cost)} (${item.detail})`, - ), - ); - } - } else { - const pctHeader = "% of usage"; - const maxLabelLen = Math.max(...items.map((i) => i.label.length), header.length); - const headerLine = ` ${padVisible(this.theme.dim(header), maxLabelLen + 2, "left")} ${this.theme.dim(pctHeader)}`; - lines.push(headerLine); - for (const item of items) { - const label = padVisible(this.theme.dim(item.label), maxLabelLen + 2, "left"); - lines.push(` ${label} ${this.theme.dim(item.detail)}`); - } - } - } - - return lines; -} -``` - -Import `padVisible` is already imported at the top of the file. - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/dashboard.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Verify existing insight toggle test still works** - -The test `"uses enter/space for expand, v for insights"` checks `toContain("Insights")`. The `mkState()` factory has `insights: [{ label: "x", cost: 1, detail: "y" }]` which has no category (defaults to `"cost"`). Verify this test still passes — it should since uncategorized items fall into "Cost patterns". - -- [ ] **Step 6: Run full check** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Step 7: Commit** - -```bash -git add src/tui/dashboard.ts tests/dashboard.test.ts -git commit -m "feat(tui): render insights grouped by category" -``` - ---- - -## Phase 4: Skill and MCP Server Breakdowns - -### Task 4.1: Extract skill invocations from user messages - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing test for skill extraction** - -Add to `describe("offline scanner", ...)` in `tests/offline.test.ts`: - -```ts -it("tags turns with the active skill from user messages", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - const skillMessage = JSON.stringify({ - type: "message", - id: "u1", - timestamp: "2026-05-30T10:00:00Z", - message: { - role: "user", - content: [ - { - type: "text", - text: '\nSkill content\n\nDo the thing', - }, - ], - }, - }); - const assistantTurn = JSON.stringify({ - type: "message", - id: "a1", - timestamp: "2026-05-30T10:01:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "m", - usage: { input: 10, output: 10, cacheRead: 0, cacheWrite: 0, cost: 1.0 }, - }, - }); - const secondSkill = JSON.stringify({ - type: "message", - id: "u2", - timestamp: "2026-05-30T10:02:00Z", - message: { - role: "user", - content: [ - { - type: "text", - text: '\ncontent\n', - }, - ], - }, - }); - const assistantTurn2 = JSON.stringify({ - type: "message", - id: "a2", - timestamp: "2026-05-30T10:03:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "m", - usage: { input: 10, output: 10, cacheRead: 0, cacheWrite: 0, cost: 2.0 }, - }, - }); - writeFileSync( - join(sessions, "s.jsonl"), - [skillMessage, assistantTurn, secondSkill, assistantTurn2].join("\n") + - "\n", - "utf8", - ); - const result = await scanOfflineUsage({ - ...createDefaultDeps(), - agentDir: () => root, - now: () => Date.parse("2026-05-30T12:00:00Z"), - }); - expect(result.turns).toHaveLength(2); - expect(result.turns[0].activeSkill).toBe("career-ops"); - expect(result.turns[1].activeSkill).toBe("writing-plans"); - rmSync(root, { recursive: true, force: true }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — `activeSkill` doesn't exist yet. - -- [ ] **Step 3: Implement skill extraction** - -In `src/core/offline.ts`: - -1. Add `activeSkill` to `UsageTurn`: - -```ts -export interface UsageTurn { - // ... existing fields ... - project?: string; - activeSkill?: string; -} -``` - -2. Add a skill extraction helper: - -```ts -const SKILL_NAME_RE = /; - if (row?.type !== "message") return undefined; - const message = row.message as Record | undefined; - if (message?.role !== "user") return undefined; - const content = message.content; - if (!Array.isArray(content)) return undefined; - for (const block of content) { - if ( - typeof block === "object" && - block !== null && - (block as Record).type === "text" - ) { - const text = (block as Record).text; - if (typeof text === "string") { - const match = SKILL_NAME_RE.exec(text); - if (match) return match[1]; - } - } - } - } catch { - // ignore parse errors - } - return undefined; -} -``` - -3. In the per-file scan loop, track `activeSkill` as session-level state (alongside `sessionProject`): - -```ts -let activeSkill: string | undefined; -``` - -Before calling `parseLine()`, check for skill in user messages: - -```ts -const skillName = extractSkillName(line); -if (skillName !== undefined) { - activeSkill = skillName; -} -``` - -After `parseLine()` returns a turn: - -```ts -turn.activeSkill = activeSkill; -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(offline): extract active skill from user messages" -``` - -### Task 4.2: Extract MCP tool calls from assistant messages - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing test for MCP tool extraction** - -Add to `describe("offline scanner", ...)` in `tests/offline.test.ts`: - -```ts -it("extracts MCP server names from tool call prefixes", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - const message = JSON.stringify({ - type: "message", - id: "a1", - timestamp: "2026-05-30T10:00:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "m", - content: [ - { - type: "toolCall", - id: "c1", - name: "playwright_browser_click", - arguments: {}, - }, - { type: "toolCall", id: "c2", name: "read", arguments: {} }, - { type: "toolCall", id: "c3", name: "tavily", arguments: {} }, - ], - usage: { input: 10, output: 10, cacheRead: 0, cacheWrite: 0, cost: 1.0 }, - }, - }); - writeFileSync(join(sessions, "s.jsonl"), `${message}\n`, "utf8"); - const result = await scanOfflineUsage({ - ...createDefaultDeps(), - agentDir: () => root, - now: () => Date.parse("2026-05-30T12:00:00Z"), - }); - expect(result.turns).toHaveLength(1); - // "read" is built-in so excluded; "playwright" from prefix; "tavily" is single-word non-built-in - expect(result.turns[0].mcpTools).toEqual( - expect.arrayContaining(["playwright", "tavily"]), - ); - expect(result.turns[0].mcpTools).not.toContain("read"); - rmSync(root, { recursive: true, force: true }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — `mcpTools` doesn't exist yet. - -- [ ] **Step 3: Implement MCP tool extraction** - -In `src/core/offline.ts`: - -1. Add `mcpTools` to `UsageTurn`: - -```ts -export interface UsageTurn { - // ... existing fields ... - project?: string; - activeSkill?: string; - mcpTools?: string[]; -} -``` - -2. Add the built-in tools set and the extraction helper: - -```ts -const BUILTIN_TOOLS = new Set([ - "bash", - "read", - "write", - "edit", - "web_search", - "questionnaire", - "get_subagent_result", - "ask_user_question", - "Agent", - "mcp", -]); - -function extractMcpServers( - message: Record, -): string[] | undefined { - const content = message.content; - if (!Array.isArray(content)) return undefined; - const servers = new Set(); - for (const block of content) { - if ( - typeof block === "object" && - block !== null && - (block as Record).type === "toolCall" - ) { - const name = (block as Record).name; - if (typeof name !== "string") continue; - if (BUILTIN_TOOLS.has(name)) continue; - const firstSegment = name.split("_")[0]; - if (firstSegment) servers.add(firstSegment); - } - } - return servers.size > 0 ? [...servers] : undefined; -} -``` - -3. In `parseLine()`, after building the turn, extract MCP tools from the message: - -```ts -// In parseLine(), after constructing turnBase but before return: -const mcpTools = extractMcpServers(message as Record); -return { id, ...turnBase, mcpTools }; -``` - -Note: `parseLine` currently only looks at `message.usage` — the `message.content` array is available on the same `message` object. Just pass it to `extractMcpServers`. - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(offline): extract MCP server names from tool call prefixes" -``` - -### Task 4.3: Add skill and MCP server insights to buildInsights - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing tests for skill and MCP insights** - -Add to `describe("insights", ...)` in `tests/offline.test.ts`: - -```ts -it("produces skill breakdown insights", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 8, - activeSkill: "career-ops", - }, - { - id: "2", - sessionId: "s1", - timestamp: 2, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 2, - }, - ]; - const insights = buildInsights(turns); - const skillInsights = insights.filter((i) => i.category === "skill"); - expect(skillInsights.length).toBeGreaterThanOrEqual(2); - expect(skillInsights[0].label).toBe("/career-ops"); - expect(skillInsights[0].detail).toContain("80.0%"); - // Turn without skill grouped as "(no skill)" - const noSkill = skillInsights.find((i) => i.label === "(no skill)"); - expect(noSkill).toBeDefined(); -}); - -it("produces MCP server breakdown insights", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 5, - mcpTools: ["playwright"], - }, - { - id: "2", - sessionId: "s1", - timestamp: 2, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 3, - mcpTools: ["playwright", "firefox"], - }, - { - id: "3", - sessionId: "s1", - timestamp: 3, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 2, - }, - ]; - const insights = buildInsights(turns); - const mcpInsights = insights.filter((i) => i.category === "mcp"); - // playwright: $5 + $3 = $8, firefox: $3 - expect(mcpInsights.length).toBeGreaterThanOrEqual(2); - expect(mcpInsights[0].label).toBe("playwright"); - expect(mcpInsights[1].label).toBe("firefox"); -}); - -it("omits skill/mcp insights when no data present", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 1, - }, - ]; - const insights = buildInsights(turns); - expect(insights.filter((i) => i.category === "skill")).toHaveLength(0); - expect(insights.filter((i) => i.category === "mcp")).toHaveLength(0); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — no skill or MCP insights produced. - -- [ ] **Step 3: Implement skill and MCP insights in buildInsights** - -In `src/core/offline.ts`, in `buildInsights()`, add skill and MCP grouping after the project insights block: - -```ts -// Skill insights -const bySkill = new Map(); -let hasAnySkill = false; -for (const t of turns) { - if (t.activeSkill) { - hasAnySkill = true; - const key = `/${t.activeSkill}`; - bySkill.set(key, (bySkill.get(key) ?? 0) + t.cost); - } else { - bySkill.set("(no skill)", (bySkill.get("(no skill)") ?? 0) + t.cost); - } -} -const skillInsights: InsightItem[] = hasAnySkill - ? [...bySkill.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([skill, cost]) => ({ - category: "skill", - label: skill, - cost, - detail: pct(cost), - })) - : []; - -// MCP server insights -const byMcp = new Map(); -for (const t of turns) { - if (t.mcpTools) { - for (const server of t.mcpTools) { - byMcp.set(server, (byMcp.get(server) ?? 0) + t.cost); - } - } -} -const mcpInsights: InsightItem[] = [...byMcp.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([server, cost]) => ({ - category: "mcp", - label: server, - cost, - detail: pct(cost), - })); -``` - -Update the return statement to include all categories: - -```ts -return [ - ...projectInsights, - ...skillInsights, - ...mcpInsights, - { - category: "cost", - label: "Parallel sessions", - cost: parallelCost, - detail: `${pct(parallelCost)} cost while >=4 active`, - }, - // ... other 4 cost insights unchanged ... -]; -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Run full check** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(insights): add skill and MCP server breakdowns" -``` - ---- - -## Final Verification - -- [ ] **Run full check one last time** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Review all changes** - -Run: `git log --oneline master..HEAD` - -Expected commits (in order): - -1. `fix(tui): add spacing between Usage Statistics title and period tabs` -2. `feat(types): add UsageConfig interface for provider toggle` -3. `feat(core): add provider enable/disable toggle via extensions/usage.json` -4. `feat(types): add category field to InsightItem and UsageInsight` -5. `feat(offline): extract project name from session CWD` -6. `feat(insights): add project breakdown by cost` -7. `feat(tui): render insights grouped by category` -8. `feat(offline): extract active skill from user messages` -9. `feat(offline): extract MCP server names from tool call prefixes` -10. `feat(insights): add skill and MCP server breakdowns` diff --git a/docs/superpowers/plans/2026-06-21-phase-1-spacing-fix.md b/docs/superpowers/plans/2026-06-21-phase-1-spacing-fix.md deleted file mode 100644 index b3da0ea..0000000 --- a/docs/superpowers/plans/2026-06-21-phase-1-spacing-fix.md +++ /dev/null @@ -1,93 +0,0 @@ -# Phase 1: Spacing Fix — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add an empty line between the "Usage Statistics" section title and the period tabs so the dashboard spacing is consistent with the "Current Usage" section. - -**Architecture:** One-line insertion in the dashboard render method, plus any test adjustments for shifted output. - -**Tech Stack:** TypeScript, Vitest, TUI rendering - -**Spec:** `docs/superpowers/specs/2026-06-21-dashboard-enhancements-design.md` → Feature 3 - -**Parent plan:** `docs/superpowers/plans/2026-06-21-dashboard-enhancements.md` → Phase 1 - ---- - -## File Map - -| File | Action | Responsibility | -| ------------------------- | ------ | ---------------------------------------------------- | -| `src/tui/dashboard.ts` | Modify | Add `lines.push("")` between section title and tabs | -| `tests/dashboard.test.ts` | Modify | Adjust any assertions affected by the new empty line | - ---- - -### Task 1.1: Add empty line between "Usage Statistics" title and period tabs - -**Files:** - -- Modify: `src/tui/dashboard.ts:364-377` -- Modify: `tests/dashboard.test.ts` (affected rendering assertions) - -- [ ] **Step 1: Update the rendering method** - -In `src/tui/dashboard.ts`, method `renderUsageStatistics()`, add an empty line between the section title and the tabs: - -```ts -// Find this block (lines 364-377): -private renderUsageStatistics(w: number, lines: string[]): void { - lines.push( - this.sectionTitle( - UI_STRINGS.dashboardBorderedSectionTitles.usageStatistics, - ), - ); - - lines.push( - ...this.renderTabs( - PERIODS.map((period) => PERIOD_LABELS[period]), - this.periodIndex, - w, - ), - ); - -// Replace with: -private renderUsageStatistics(w: number, lines: string[]): void { - lines.push( - this.sectionTitle( - UI_STRINGS.dashboardBorderedSectionTitles.usageStatistics, - ), - ); - lines.push(""); - - lines.push( - ...this.renderTabs( - PERIODS.map((period) => PERIOD_LABELS[period]), - this.periodIndex, - w, - ), - ); -``` - -- [ ] **Step 2: Run tests to check what breaks** - -Run: `pnpm test -- tests/dashboard.test.ts` - -Some render snapshot assertions may fail because they expect the old line ordering. Note which tests fail. - -- [ ] **Step 3: Fix any failing dashboard tests** - -If tests assert exact line positions or patterns that now shift by one line due to the empty line, update those assertions. The empty line appears between the "Usage Statistics" title and the `[All Time]` tab line in the render output. - -- [ ] **Step 4: Run full check** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Step 5: Commit** - -```bash -git add src/tui/dashboard.ts tests/dashboard.test.ts -git commit -m "fix(tui): add spacing between Usage Statistics title and period tabs" -``` diff --git a/docs/superpowers/plans/2026-06-21-phase-2-provider-toggle.md b/docs/superpowers/plans/2026-06-21-phase-2-provider-toggle.md deleted file mode 100644 index ada10b1..0000000 --- a/docs/superpowers/plans/2026-06-21-phase-2-provider-toggle.md +++ /dev/null @@ -1,259 +0,0 @@ -# Phase 2: Provider Enable/Disable Toggle — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Allow users to disable specific live providers via a `usage.json` config file so they produce no API calls, cache writes, or dashboard cards. - -**Architecture:** A `UsageConfig` type gates provider inclusion. `loadConfig()` reads `$PI_CODING_AGENT_DIR/extensions/usage.json` at bootstrap. Disabled providers are filtered from the registry before any fetches. All downstream logic (live runtime, cache watcher, dashboard) operates on the filtered set automatically. - -**Tech Stack:** TypeScript, Vitest, Node.js `readFile` - -**Spec:** `docs/superpowers/specs/2026-06-21-dashboard-enhancements-design.md` → Feature 1 - -**Parent plan:** `docs/superpowers/plans/2026-06-21-dashboard-enhancements.md` → Phase 2 - -**Prerequisite:** None (independent of Phase 1) - ---- - -## File Map - -| File | Action | Responsibility | -| -------------------------- | ------ | ----------------------------------------------------- | -| `src/shared/types.ts` | Modify | Add `UsageConfig` interface | -| `src/core/usage-core.ts` | Modify | Add `loadConfig()`, filter providers in `bootstrap()` | -| `tests/usage-core.test.ts` | Modify | Config loading + provider filtering tests | - ---- - -### Task 2.1: Add UsageConfig type - -**Files:** - -- Modify: `src/shared/types.ts` - -- [ ] **Step 1: Add the UsageConfig interface** - -Append after the `UsageDashboardState` interface (around line 136) in `src/shared/types.ts`: - -```ts -export interface UsageConfig { - providers?: Partial>; -} -``` - -- [ ] **Step 2: Run typecheck** - -Run: `pnpm typecheck` - -Expected: PASS (new type, no consumers yet). - -- [ ] **Step 3: Commit** - -```bash -git add src/shared/types.ts -git commit -m "feat(types): add UsageConfig interface for provider toggle" -``` - ---- - -### Task 2.2: Implement loadConfig and provider filtering - -**Files:** - -- Modify: `src/core/usage-core.ts` -- Modify: `tests/usage-core.test.ts` - -- [ ] **Step 1: Write failing tests for config loading** - -Add a new `describe("config loading", ...)` block at the end of `tests/usage-core.test.ts`. The file already imports `createUsageCore` and has a `createTestDeps(root, overrides?)` helper (lines 16-29). Add the following imports to the top of the file if not already present: - -```ts -import { join } from "node:path"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -``` - -Then add the describe block: - -```ts -describe("config loading", () => { - function mkTmp(): string { - return mkdtempSync(join(tmpdir(), "pi-usage-config-")); - } - - it("loads config and filters disabled providers", async () => { - const root = mkTmp(); - const extDir = join(root, "extensions"); - mkdirSync(extDir, { recursive: true }); - writeFileSync( - join(extDir, "usage.json"), - JSON.stringify({ providers: { minimax: { enabled: false } } }), - ); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - writeFileSync(join(sessions, "s.jsonl"), "", "utf8"); - - const core = createUsageCore({ - deps: createTestDeps(root), - onEmit: () => {}, - }); - await core.bootstrap(); - const state = core.getState(); - - expect( - state.providers.find((p) => p.providerId === "minimax"), - ).toBeUndefined(); - expect( - state.providers.find((p) => p.providerId === "openai-codex"), - ).toBeDefined(); - - rmSync(root, { recursive: true, force: true }); - }); - - it("treats missing config file as all providers enabled", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - writeFileSync(join(sessions, "s.jsonl"), "", "utf8"); - - const core = createUsageCore({ - deps: createTestDeps(root), - onEmit: () => {}, - }); - await core.bootstrap(); - const state = core.getState(); - - const ids = state.providers.map((p) => p.providerId); - expect(ids).toContain("openai-codex"); - expect(ids).toContain("minimax"); - - rmSync(root, { recursive: true, force: true }); - }); - - it("ignores malformed config JSON", async () => { - const root = mkTmp(); - const extDir = join(root, "extensions"); - mkdirSync(extDir, { recursive: true }); - writeFileSync(join(extDir, "usage.json"), "not json!!!"); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - writeFileSync(join(sessions, "s.jsonl"), "", "utf8"); - - const core = createUsageCore({ - deps: createTestDeps(root), - onEmit: () => {}, - }); - await core.bootstrap(); - const state = core.getState(); - - const ids = state.providers.map((p) => p.providerId); - expect(ids).toContain("minimax"); - - rmSync(root, { recursive: true, force: true }); - }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/usage-core.test.ts` - -Expected: FAIL — the config loading logic doesn't exist yet, so the disabled-provider test should fail. - -- [ ] **Step 3: Implement loadConfig and filtering in usage-core.ts** - -In `src/core/usage-core.ts`: - -**Add imports at the top:** - -```ts -import { join } from "node:path"; -import type { UsageConfig } from "../shared/types.ts"; -``` - -**Add `loadConfig` function before `createUsageCore`:** - -```ts -async function loadConfig(deps: UsageDeps): Promise { - try { - const configPath = join(deps.agentDir(), "extensions", "usage.json"); - const raw = await deps.readFile(configPath, "utf8"); - return JSON.parse(raw as string) as UsageConfig; - } catch { - return {}; - } -} -``` - -**Change `const` to `let` for provider registry variables (lines 52-58):** - -```ts -// Before: -const providers = createProviderRegistry(deps); -const liveProviderIds = new Set( - providers.filter((p) => p.strategy === "api").map((p) => p.id), -); -const liveProviderSnapshotFiles = new Set( - [...liveProviderIds].map((id) => `${id}.json`), -); - -// After: -let providers = createProviderRegistry(deps); -let liveProviderIds = new Set( - providers.filter((p) => p.strategy === "api").map((p) => p.id), -); -let liveProviderSnapshotFiles = new Set( - [...liveProviderIds].map((id) => `${id}.json`), -); -``` - -**Update `bootstrap()` to load config and filter (lines 202-206):** - -```ts -// Before: -async function bootstrap(): Promise { - await Promise.all([populateProviders(false), refreshOffline(false)]); - state.diagnostics = ["live runtime ready"]; - emit(USAGE_CORE_READY_EVENT); -} - -// After: -async function bootstrap(): Promise { - const config = await loadConfig(deps); - if (config.providers) { - providers = providers.filter((p) => { - const setting = config.providers?.[p.id]; - return setting?.enabled !== false; - }); - liveProviderIds = new Set( - providers.filter((p) => p.strategy === "api").map((p) => p.id), - ); - liveProviderSnapshotFiles = new Set( - [...liveProviderIds].map((id) => `${id}.json`), - ); - } - await Promise.all([populateProviders(false), refreshOffline(false)]); - state.diagnostics = ["live runtime ready"]; - emit(USAGE_CORE_READY_EVENT); -} -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/usage-core.test.ts` - -Expected: PASS — all existing tests plus the new config tests. - -- [ ] **Step 5: Run full check** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/core/usage-core.ts tests/usage-core.test.ts -git commit -m "feat(core): add provider enable/disable toggle via extensions/usage.json" -``` diff --git a/docs/superpowers/plans/2026-06-21-phase-3-project-insights.md b/docs/superpowers/plans/2026-06-21-phase-3-project-insights.md deleted file mode 100644 index 65aff0e..0000000 --- a/docs/superpowers/plans/2026-06-21-phase-3-project-insights.md +++ /dev/null @@ -1,695 +0,0 @@ -# Phase 3: Insight Infrastructure + Project Breakdown — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a `category` field to insights, extract project names from session CWD during the offline scan, compute a "Top projects" breakdown (capped at 5 with overflow summary), and render insights grouped by category in the dashboard. - -**Architecture:** Five tasks in sequence. Task 3.1 adds the `category` field to types and tags existing insights as `"cost"`. Task 3.2 enriches the JSONL scan to extract session CWD and set `project` on each turn. Task 3.3 adds project grouping to `buildInsights()`. Task 3.4 replaces the flat insights list in the dashboard with category-grouped rendering (tables for breakdown categories, bullet list for cost patterns). Task 3.5 caps project insights at 5, with a `+N more` overflow row aggregating the remaining projects. This also establishes the rendering infrastructure that Phase 4 builds on. - -**Tech Stack:** TypeScript, Vitest, TUI rendering - -**Spec:** `docs/superpowers/specs/2026-06-21-dashboard-enhancements-design.md` → Feature 2 (part 1) - -**Parent plan:** `docs/superpowers/plans/2026-06-21-dashboard-enhancements.md` → Phase 3 - -**Prerequisite:** None (independent of Phases 1-2) - ---- - -## File Map - -| File | Action | Responsibility | -| ------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `src/core/offline.ts` | Modify | Add `category` to `InsightItem`; add `project` to `UsageTurn`; add `projectFromCwd()` helper; extract CWD in scan loop; add project insights to `buildInsights()` | -| `src/shared/types.ts` | Modify | Add `category` to `UsageInsight` | -| `src/tui/dashboard.ts` | Modify | Add `renderInsightsByCategory()` method; update insights rendering block | -| `tests/offline.test.ts` | Modify | Project extraction tests; project insight tests | -| `tests/dashboard.test.ts` | Modify | Category-grouped rendering test | - ---- - -### Task 3.1: Add category field to InsightItem and UsageInsight - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `src/shared/types.ts` - -- [ ] **Step 1: Add category to InsightItem in offline.ts** - -In `src/core/offline.ts`, update the `InsightItem` interface (around line 287): - -```ts -export interface InsightItem { - category?: string; - label: string; - cost: number; - detail: string; -} -``` - -- [ ] **Step 2: Add category to UsageInsight in types.ts** - -In `src/shared/types.ts`, update the `UsageInsight` interface (around line 108): - -```ts -export interface UsageInsight { - category?: string; - label: string; - cost: number; - detail: string; -} -``` - -- [ ] **Step 3: Tag existing insights with category "cost"** - -In `src/core/offline.ts`, in `buildInsights()`, add `category: "cost"` to each of the five existing insight items in the return array (around line 344): - -```ts -return [ - { - category: "cost", - label: "Parallel sessions", - cost: parallelCost, - detail: `${pct(parallelCost)} cost while >=4 active`, - }, - { - category: "cost", - label: "Large context", - cost: largeContext, - detail: `${pct(largeContext)} over 150k context`, - }, - { - category: "cost", - label: "Large uncached", - cost: largeUncached, - detail: `${pct(largeUncached)} over 100k input`, - }, - { - category: "cost", - label: "Long sessions", - cost: longSessionCost, - detail: `${pct(longSessionCost)} from 8h+ sessions`, - }, - { - category: "cost", - label: "Top-5 concentration", - cost: top5, - detail: `${pct(top5)} in top 5 sessions`, - }, -]; -``` - -- [ ] **Step 4: Run tests** - -Run: `pnpm check` - -Expected: PASS — `category` is optional, existing tests don't assert on it. - -- [ ] **Step 5: Commit** - -```bash -git add src/core/offline.ts src/shared/types.ts -git commit -m "feat(types): add category field to InsightItem and UsageInsight" -``` - ---- - -### Task 3.2: Extract session CWD and add project field to UsageTurn - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing test for project extraction** - -Add to the `describe("offline scanner", ...)` block in `tests/offline.test.ts`: - -```ts -it("extracts project name from session header cwd", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions", "proj"); - mkdirSync(sessions, { recursive: true }); - const sessionHeader = JSON.stringify({ - type: "session", - version: 3, - id: "test-session", - timestamp: "2026-05-30T10:00:00Z", - cwd: "/Users/dev/career-ops", - }); - const message = JSON.stringify({ - type: "message", - id: "m1", - timestamp: "2026-05-30T11:00:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "MiniMax-M2.7", - usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, cost: 0.5 }, - }, - }); - writeFileSync( - join(sessions, "s.jsonl"), - `${sessionHeader}\n${message}\n`, - "utf8", - ); - const result = await scanOfflineUsage({ - ...createDefaultDeps(), - agentDir: () => root, - now: () => Date.parse("2026-05-30T12:00:00Z"), - }); - expect(result.turns).toHaveLength(1); - expect(result.turns[0].project).toBe("career-ops"); - rmSync(root, { recursive: true, force: true }); -}); - -it("falls back to undefined project when no session header", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - const message = JSON.stringify({ - type: "message", - id: "m1", - timestamp: "2026-05-30T11:00:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "m", - usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, cost: 0.1 }, - }, - }); - writeFileSync(join(sessions, "s.jsonl"), `${message}\n`, "utf8"); - const result = await scanOfflineUsage({ - ...createDefaultDeps(), - agentDir: () => root, - }); - expect(result.turns[0].project).toBeUndefined(); - rmSync(root, { recursive: true, force: true }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — `project` property doesn't exist on `UsageTurn` yet. - -- [ ] **Step 3: Implement CWD extraction** - -In `src/core/offline.ts`: - -**1. Add `project` to the `UsageTurn` interface:** - -```ts -export interface UsageTurn { - id: string; - sessionId: string; - timestamp: number; - provider: string; - model: string; - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - tokens: number; - cost: number; - project?: string; -} -``` - -**2. Add a helper to extract project from CWD (place near other helpers, before `scanOfflineUsage`):** - -```ts -function projectFromCwd(cwd: unknown): string | undefined { - if (typeof cwd !== "string" || !cwd) return undefined; - const segments = cwd.replace(/\/+$/, "").split("/"); - return segments[segments.length - 1] || undefined; -} -``` - -**3. In `scanOfflineUsage()`, inside the per-file loop, track session CWD.** - -The scan loop reads lines from each JSONL file. Add session-level state before the line iteration, and check for session headers: - -```ts -// Inside the per-file loop, before iterating lines: -let sessionProject: string | undefined; - -// When processing each line, before the existing parseLine call: -try { - const parsed = JSON.parse(line) as Record; - if (parsed?.type === "session" && parsed.cwd) { - sessionProject = projectFromCwd(parsed.cwd); - continue; - } -} catch { - // fall through to existing parseLine logic -} -``` - -**4. After `parseLine()` returns a turn, set `turn.project`:** - -```ts -const turn = parseLine(line, sessionId); -if (!turn) continue; -turn.project = sessionProject; -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(offline): extract project name from session CWD" -``` - ---- - -### Task 3.3: Add project insights to buildInsights - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing test for project insights** - -Add to the `describe("insights", ...)` block in `tests/offline.test.ts`: - -```ts -it("produces project breakdown insights", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 9, - project: "career-ops", - }, - { - id: "2", - sessionId: "s2", - timestamp: 2, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 1, - project: "dotfiles", - }, - ]; - const insights = buildInsights(turns); - const projectInsights = insights.filter((i) => i.category === "project"); - expect(projectInsights.length).toBeGreaterThanOrEqual(2); - expect(projectInsights[0].label).toBe("career-ops"); - expect(projectInsights[0].detail).toContain("90.0%"); - expect(projectInsights[1].label).toBe("dotfiles"); -}); - -it("omits project insights when no projects are set", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 1, - }, - ]; - const insights = buildInsights(turns); - const projectInsights = insights.filter((i) => i.category === "project"); - expect(projectInsights).toHaveLength(0); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — no insights with `category: "project"` produced yet. - -- [ ] **Step 3: Implement project insights in buildInsights** - -In `src/core/offline.ts`, in the `buildInsights()` function, add project grouping after the existing `top5` calculation but before the return statement: - -```ts -const byProject = new Map(); -for (const t of turns) { - if (t.project) { - byProject.set(t.project, (byProject.get(t.project) ?? 0) + t.cost); - } -} -const projectInsights: InsightItem[] = [...byProject.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([project, cost]) => ({ - category: "project", - label: project, - cost, - detail: pct(cost), - })); -``` - -Then update the return to prepend project insights: - -```ts -return [ - ...projectInsights, - { - category: "cost", - label: "Parallel sessions", - cost: parallelCost, - detail: `${pct(parallelCost)} cost while >=4 active`, - }, - { - category: "cost", - label: "Large context", - cost: largeContext, - detail: `${pct(largeContext)} over 150k context`, - }, - { - category: "cost", - label: "Large uncached", - cost: largeUncached, - detail: `${pct(largeUncached)} over 100k input`, - }, - { - category: "cost", - label: "Long sessions", - cost: longSessionCost, - detail: `${pct(longSessionCost)} from 8h+ sessions`, - }, - { - category: "cost", - label: "Top-5 concentration", - cost: top5, - detail: `${pct(top5)} in top 5 sessions`, - }, -]; -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. The existing `"produces five insight rows"` test should still pass because those turns have no `project` field, so `projectInsights` will be empty and the array is still length 5. - -- [ ] **Step 5: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(insights): add project breakdown by cost" -``` - ---- - -### Task 3.4: Category-grouped insights rendering in dashboard - -**Files:** - -- Modify: `src/tui/dashboard.ts` -- Modify: `tests/dashboard.test.ts` - -- [ ] **Step 1: Write failing test for category-grouped rendering** - -Add a new test in the `describe("dashboard rendering", ...)` block in `tests/dashboard.test.ts`: - -```ts -it("renders insights grouped by category", () => { - const state = mkState(); - state.insights = [ - { category: "project", label: "career-ops", cost: 9, detail: "90.0%" }, - { category: "project", label: "dotfiles", cost: 1, detail: "10.0%" }, - { - category: "cost", - label: "Large context", - cost: 5, - detail: "50.0% over 150k context", - }, - ]; - const c = new UsageDashboardComponent(state, () => undefined, { - theme: noTheme, - }); - c.handleInput("v"); - const out = c.render(100).join("\n"); - expect(out).toContain("Projects"); - expect(out).toContain("career-ops"); - expect(out).toContain("90.0%"); - expect(out).toContain("Cost patterns"); - expect(out).toContain("Large context"); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pnpm test -- tests/dashboard.test.ts` - -Expected: FAIL — "Projects" category header not rendered. - -- [ ] **Step 3: Implement category-grouped rendering** - -In `src/tui/dashboard.ts`, find the insights rendering block inside `renderUsageStatistics()` (around lines 383-398). - -**Replace the flat list rendering:** - -```ts -// Find this block: -if (this.showInsights) { - lines.push( - this.sectionTitle(UI_STRINGS.dashboardBorderedSectionTitles.insights), - ); - if (this.state.insights.length === 0) { - lines.push(this.theme.dim("No insights yet.")); - } else { - for (const item of this.state.insights) { - lines.push( - this.theme.dim( - `- ${item.label}: ${formatCurrency(item.cost)} (${item.detail})`, - ), - ); - } - } - return; -} - -// Replace with: -if (this.showInsights) { - lines.push( - this.sectionTitle(UI_STRINGS.dashboardBorderedSectionTitles.insights), - ); - if (this.state.insights.length === 0) { - lines.push(this.theme.dim("No insights yet.")); - } else { - lines.push(...this.renderInsightsByCategory(w)); - } - return; -} -``` - -**Add the `renderInsightsByCategory` method to `UsageDashboardComponent`:** - -```ts -private renderInsightsByCategory(w: number): string[] { - const lines: string[] = []; - const categoryOrder = ["project", "skill", "mcp", "cost"]; - const categoryLabels: Record = { - project: "Projects", - skill: "Skills", - mcp: "MCP servers", - cost: "Cost patterns", - }; - - const grouped = new Map(); - for (const item of this.state.insights) { - const cat = item.category ?? "cost"; - const list = grouped.get(cat) ?? []; - list.push(item); - grouped.set(cat, list); - } - - for (const cat of categoryOrder) { - const items = grouped.get(cat); - if (!items || items.length === 0) continue; - lines.push(""); - const header = categoryLabels[cat] ?? cat; - if (cat === "cost") { - lines.push(this.theme.dim(header)); - for (const item of items) { - lines.push( - this.theme.dim( - ` - ${item.label}: ${formatCurrency(item.cost)} (${item.detail})`, - ), - ); - } - } else { - const pctHeader = "% of usage"; - const maxLabelLen = Math.max( - ...items.map((i) => i.label.length), - header.length, - ); - const headerLine = ` ${padVisible(this.theme.dim(header), maxLabelLen + 2, "left")} ${this.theme.dim(pctHeader)}`; - lines.push(headerLine); - for (const item of items) { - const label = padVisible( - this.theme.dim(item.label), - maxLabelLen + 2, - "left", - ); - lines.push(` ${label} ${this.theme.dim(item.detail)}`); - } - } - } - - return lines; -} -``` - -Note: `padVisible` and `formatCurrency` are already imported/available in the file. - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/dashboard.test.ts` - -Expected: PASS. The existing test `"uses enter/space for expand, v for insights"` uses `mkState()` which has `insights: [{ label: "x", cost: 1, detail: "y" }]` — no category, so it defaults to `"cost"` and renders under "Cost patterns". The test checks `toContain("Insights")` which is the section title, still present. - -- [ ] **Step 5: Run full check** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/tui/dashboard.ts tests/dashboard.test.ts -git commit -m "feat(tui): render insights grouped by category" -``` - ---- - -### Task 3.5: Cap project insights at 5 with overflow summary - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing test for project cap with overflow** - -Add to the `describe("insights", ...)` block in `tests/offline.test.ts`, after the existing project insight tests: - -```ts -it("caps project insights at 5 with overflow summary", () => { - const turns = Array.from({ length: 7 }, (_, i) => ({ - id: String(i), - sessionId: `s${i}`, - timestamp: i, - provider: "p", - model: "m", - input: 1, - output: 1, - cacheRead: 0, - cacheWrite: 0, - tokens: 2, - cost: 7 - i, - project: `proj-${String.fromCharCode(97 + i)}`, - })); - const insights = buildInsights(turns); - const projectInsights = insights.filter((i) => i.category === "project"); - expect(projectInsights).toHaveLength(6); - expect(projectInsights[0].label).toBe("proj-a"); - expect(projectInsights[4].label).toBe("proj-e"); - expect(projectInsights[5].label).toBe("+2 more"); - expect(projectInsights[5].cost).toBe(3); - expect(projectInsights[5].detail).toContain("10.7%"); -}); -``` - -Math check: 7 turns with costs [7, 6, 5, 4, 3, 2, 1], totalCost = 28. Top 5 costs sum to 25. Remaining 2 projects cost 2 + 1 = 3. `pct(3)` = `((100 * 3) / 28).toFixed(1)` = `"10.7%"`. - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — currently all 7 projects are returned, but test expects only 6 items (5 + overflow). - -- [ ] **Step 3: Implement project cap in buildInsights** - -In `src/core/offline.ts`, replace the current `projectInsights` construction (lines 370-377): - -```ts -// Replace this: -const projectInsights: InsightItem[] = [...byProject.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([project, cost]) => ({ - category: "project", - label: project, - cost, - detail: pct(cost), - })); - -// With this: -const maxProjects = 5; -const allProjectEntries = [...byProject.entries()].sort( - (a, b) => b[1] - a[1], -); -const projectInsights: InsightItem[] = allProjectEntries - .slice(0, maxProjects) - .map(([project, cost]) => ({ - category: "project", - label: project, - cost, - detail: pct(cost), - })); -if (allProjectEntries.length > maxProjects) { - const remainingCost = allProjectEntries - .slice(maxProjects) - .reduce((sum, [, c]) => sum + c, 0); - projectInsights.push({ - category: "project", - label: `+${allProjectEntries.length - maxProjects} more`, - cost: remainingCost, - detail: pct(remainingCost), - }); -} -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. The existing `"produces project breakdown insights"` test (2 projects, under the cap) still passes unchanged. The new test verifies the cap and overflow row. - -- [ ] **Step 5: Run full check** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(insights): cap project insights at 5 with overflow summary" -``` diff --git a/docs/superpowers/plans/2026-06-21-phase-4-skill-mcp-insights.md b/docs/superpowers/plans/2026-06-21-phase-4-skill-mcp-insights.md deleted file mode 100644 index b31fed3..0000000 --- a/docs/superpowers/plans/2026-06-21-phase-4-skill-mcp-insights.md +++ /dev/null @@ -1,679 +0,0 @@ -# Phase 4: Skill and MCP Server Breakdowns — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Extract skill invocations and MCP tool calls from session JSONL data, then compute "Top skills" and "Top MCP servers" insight categories that render alongside the project breakdown from Phase 3. - -**Architecture:** Three tasks. Task 4.1 extracts skill names from user messages containing `` tags and tracks the active skill per session. Task 4.2 extracts MCP server names from assistant tool calls by taking the first underscore-separated segment (excluding known built-in tools). Task 4.3 adds skill and MCP grouping to `buildInsights()`. The category-grouped rendering from Phase 3 picks up the new categories automatically. - -**Tech Stack:** TypeScript, Vitest, regex extraction - -**Spec:** `docs/superpowers/specs/2026-06-21-dashboard-enhancements-design.md` → Feature 2 (part 2) - -**Parent plan:** `docs/superpowers/plans/2026-06-21-dashboard-enhancements.md` → Phase 4 - -**Prerequisite:** Phase 3 must be completed first (this phase depends on the `category` field on `InsightItem`, the `project` field on `UsageTurn`, and the category-grouped rendering in the dashboard). - ---- - -## File Map - -| File | Action | Responsibility | -| ----------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `src/core/offline.ts` | Modify | Add `activeSkill` and `mcpTools` to `UsageTurn`; add `extractSkillName()`, `extractMcpServers()`, `BUILTIN_TOOLS`; extract in scan loop; add skill/MCP insights to `buildInsights()` | -| `tests/offline.test.ts` | Modify | Skill extraction, MCP extraction, skill insights, MCP insights tests | - ---- - -### Task 4.1: Extract skill invocations from user messages - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing test for skill extraction** - -Add to `describe("offline scanner", ...)` in `tests/offline.test.ts`: - -```ts -it("tags turns with the active skill from user messages", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - const skillMessage = JSON.stringify({ - type: "message", - id: "u1", - timestamp: "2026-05-30T10:00:00Z", - message: { - role: "user", - content: [ - { - type: "text", - text: '\nSkill content\n\nDo the thing', - }, - ], - }, - }); - const assistantTurn = JSON.stringify({ - type: "message", - id: "a1", - timestamp: "2026-05-30T10:01:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "m", - usage: { input: 10, output: 10, cacheRead: 0, cacheWrite: 0, cost: 1.0 }, - }, - }); - const secondSkill = JSON.stringify({ - type: "message", - id: "u2", - timestamp: "2026-05-30T10:02:00Z", - message: { - role: "user", - content: [ - { - type: "text", - text: '\ncontent\n', - }, - ], - }, - }); - const assistantTurn2 = JSON.stringify({ - type: "message", - id: "a2", - timestamp: "2026-05-30T10:03:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "m", - usage: { input: 10, output: 10, cacheRead: 0, cacheWrite: 0, cost: 2.0 }, - }, - }); - writeFileSync( - join(sessions, "s.jsonl"), - [skillMessage, assistantTurn, secondSkill, assistantTurn2].join("\n") + - "\n", - "utf8", - ); - const result = await scanOfflineUsage({ - ...createDefaultDeps(), - agentDir: () => root, - now: () => Date.parse("2026-05-30T12:00:00Z"), - }); - expect(result.turns).toHaveLength(2); - expect(result.turns[0].activeSkill).toBe("career-ops"); - expect(result.turns[1].activeSkill).toBe("writing-plans"); - rmSync(root, { recursive: true, force: true }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — `activeSkill` doesn't exist on `UsageTurn` yet. - -- [ ] **Step 3: Implement skill extraction** - -In `src/core/offline.ts`: - -**1. Add `activeSkill` to the `UsageTurn` interface:** - -```ts -export interface UsageTurn { - id: string; - sessionId: string; - timestamp: number; - provider: string; - model: string; - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - tokens: number; - cost: number; - project?: string; - activeSkill?: string; -} -``` - -**2. Add a skill extraction helper (place near other helpers, before `scanOfflineUsage`):** - -```ts -const SKILL_NAME_RE = /; - if (row?.type !== "message") return undefined; - const message = row.message as Record | undefined; - if (message?.role !== "user") return undefined; - const content = message.content; - if (!Array.isArray(content)) return undefined; - for (const block of content) { - if ( - typeof block === "object" && - block !== null && - (block as Record).type === "text" - ) { - const text = (block as Record).text; - if (typeof text === "string") { - const match = SKILL_NAME_RE.exec(text); - if (match) return match[1]; - } - } - } - } catch { - // ignore parse errors - } - return undefined; -} -``` - -**3. In the per-file scan loop (inside `scanOfflineUsage`), track `activeSkill` as session-level state alongside `sessionProject`:** - -```ts -let activeSkill: string | undefined; -``` - -Before calling `parseLine()`, check for skill invocations: - -```ts -const skillName = extractSkillName(line); -if (skillName !== undefined) { - activeSkill = skillName; -} -``` - -After `parseLine()` returns a turn, set the active skill: - -```ts -turn.activeSkill = activeSkill; -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(offline): extract active skill from user messages" -``` - ---- - -### Task 4.2: Extract MCP tool calls from assistant messages - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing test for MCP tool extraction** - -Add to `describe("offline scanner", ...)` in `tests/offline.test.ts`: - -```ts -it("extracts MCP server names from tool call prefixes", async () => { - const root = mkTmp(); - const sessions = join(root, "sessions"); - mkdirSync(sessions, { recursive: true }); - const message = JSON.stringify({ - type: "message", - id: "a1", - timestamp: "2026-05-30T10:00:00Z", - message: { - role: "assistant", - provider: "minimax", - model: "m", - content: [ - { - type: "toolCall", - id: "c1", - name: "playwright_browser_click", - arguments: {}, - }, - { type: "toolCall", id: "c2", name: "read", arguments: {} }, - { type: "toolCall", id: "c3", name: "tavily", arguments: {} }, - ], - usage: { input: 10, output: 10, cacheRead: 0, cacheWrite: 0, cost: 1.0 }, - }, - }); - writeFileSync(join(sessions, "s.jsonl"), `${message}\n`, "utf8"); - const result = await scanOfflineUsage({ - ...createDefaultDeps(), - agentDir: () => root, - now: () => Date.parse("2026-05-30T12:00:00Z"), - }); - expect(result.turns).toHaveLength(1); - // "read" is built-in so excluded; "playwright" from prefix; "tavily" is single-word non-built-in - expect(result.turns[0].mcpTools).toEqual( - expect.arrayContaining(["playwright", "tavily"]), - ); - expect(result.turns[0].mcpTools).not.toContain("read"); - rmSync(root, { recursive: true, force: true }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — `mcpTools` doesn't exist on `UsageTurn` yet. - -- [ ] **Step 3: Implement MCP tool extraction** - -In `src/core/offline.ts`: - -**1. Add `mcpTools` to `UsageTurn`:** - -```ts -export interface UsageTurn { - id: string; - sessionId: string; - timestamp: number; - provider: string; - model: string; - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - tokens: number; - cost: number; - project?: string; - activeSkill?: string; - mcpTools?: string[]; -} -``` - -**2. Add the built-in tools set and extraction helper (place near other helpers):** - -```ts -const BUILTIN_TOOLS = new Set([ - "bash", - "read", - "write", - "edit", - "grep", - "ls", - "find", -]); - -function extractMcpServers( - message: Record, -): string[] | undefined { - const content = message.content; - if (!Array.isArray(content)) return undefined; - const servers = new Set(); - for (const block of content) { - if ( - typeof block === "object" && - block !== null && - (block as Record).type === "toolCall" - ) { - const name = (block as Record).name; - if (typeof name !== "string") continue; - if (BUILTIN_TOOLS.has(name)) continue; - const firstSegment = name.split("_")[0]; - if (firstSegment) servers.add(firstSegment); - } - } - return servers.size > 0 ? [...servers] : undefined; -} -``` - -**3. In `parseLine()`, extract MCP tools from the message.** - -After building the turn object but before returning it, call `extractMcpServers`: - -```ts -// In parseLine(), after constructing the turn fields but before return: -const mcpTools = extractMcpServers(message as Record); -// Include mcpTools in the returned object -return { - id, - sessionId, - timestamp, - provider, - model, - input, - output, - cacheRead, - cacheWrite, - tokens, - cost, - mcpTools, -}; -``` - -The exact integration depends on how `parseLine` currently constructs and returns the turn. The key change: pass the `message` object to `extractMcpServers()` and include the result in the returned `UsageTurn`. - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(offline): extract MCP server names from tool call prefixes" -``` - ---- - -### Task 4.3: Add skill and MCP server insights to buildInsights - -**Files:** - -- Modify: `src/core/offline.ts` -- Modify: `tests/offline.test.ts` - -- [ ] **Step 1: Write failing tests for skill and MCP insights** - -Add to `describe("insights", ...)` in `tests/offline.test.ts`: - -```ts -it("produces skill breakdown insights", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 8, - activeSkill: "career-ops", - }, - { - id: "2", - sessionId: "s1", - timestamp: 2, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 2, - }, - ]; - const insights = buildInsights(turns); - const skillInsights = insights.filter((i) => i.category === "skill"); - expect(skillInsights.length).toBeGreaterThanOrEqual(2); - expect(skillInsights[0].label).toBe("/career-ops"); - expect(skillInsights[0].detail).toContain("80.0%"); - const noSkill = skillInsights.find((i) => i.label === "(no skill)"); - expect(noSkill).toBeDefined(); -}); - -it("produces MCP server breakdown insights", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 5, - mcpTools: ["playwright"], - }, - { - id: "2", - sessionId: "s1", - timestamp: 2, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 3, - mcpTools: ["playwright", "firefox"], - }, - { - id: "3", - sessionId: "s1", - timestamp: 3, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 2, - }, - ]; - const insights = buildInsights(turns); - const mcpInsights = insights.filter((i) => i.category === "mcp"); - // playwright: $5 + $3 = $8, firefox: $3 - expect(mcpInsights.length).toBeGreaterThanOrEqual(2); - expect(mcpInsights[0].label).toBe("playwright"); - expect(mcpInsights[1].label).toBe("firefox"); -}); - -it("caps skill insights at 5 with overflow summary", () => { - const turns = Array.from({ length: 7 }, (_, i) => ({ - id: String(i), - sessionId: `s${i}`, - timestamp: i, - provider: "p", - model: "m", - input: 1, - output: 1, - cacheRead: 0, - cacheWrite: 0, - tokens: 2, - cost: 7 - i, - activeSkill: `skill-${String.fromCharCode(97 + i)}`, - })); - const insights = buildInsights(turns); - const skillInsights = insights.filter((i) => i.category === "skill"); - expect(skillInsights).toHaveLength(6); - expect(skillInsights[0].label).toBe("/skill-a"); - expect(skillInsights[4].label).toBe("/skill-e"); - expect(skillInsights[5].label).toBe("+2 more"); - expect(skillInsights[5].cost).toBe(3); -}); - -it("caps MCP insights at 5 with overflow summary", () => { - const turns = Array.from({ length: 7 }, (_, i) => ({ - id: String(i), - sessionId: `s${i}`, - timestamp: i, - provider: "p", - model: "m", - input: 1, - output: 1, - cacheRead: 0, - cacheWrite: 0, - tokens: 2, - cost: 7 - i, - mcpTools: [`server-${String.fromCharCode(97 + i)}`], - })); - const insights = buildInsights(turns); - const mcpInsights = insights.filter((i) => i.category === "mcp"); - expect(mcpInsights).toHaveLength(6); - expect(mcpInsights[0].label).toBe("server-a"); - expect(mcpInsights[4].label).toBe("server-e"); - expect(mcpInsights[5].label).toBe("+2 more"); - expect(mcpInsights[5].cost).toBe(3); -}); - -it("omits skill/mcp insights when no data present", () => { - const turns = [ - { - id: "1", - sessionId: "s1", - timestamp: 1, - provider: "p", - model: "m", - input: 10, - output: 10, - cacheRead: 0, - cacheWrite: 0, - tokens: 20, - cost: 1, - }, - ]; - const insights = buildInsights(turns); - expect(insights.filter((i) => i.category === "skill")).toHaveLength(0); - expect(insights.filter((i) => i.category === "mcp")).toHaveLength(0); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: FAIL — no skill or MCP insights produced. - -- [ ] **Step 3: Implement skill and MCP insights in buildInsights** - -In `src/core/offline.ts`, in `buildInsights()`, add skill and MCP grouping after the `projectInsights` block (added in Phase 3, Task 3.3) and before the return: - -```ts -// Skill insights -const bySkill = new Map(); -let hasAnySkill = false; -for (const t of turns) { - if (t.activeSkill) { - hasAnySkill = true; - const key = `/${t.activeSkill}`; - bySkill.set(key, (bySkill.get(key) ?? 0) + t.cost); - } else { - bySkill.set("(no skill)", (bySkill.get("(no skill)") ?? 0) + t.cost); - } -} -const allSkillEntries = hasAnySkill - ? [...bySkill.entries()].sort((a, b) => b[1] - a[1]) - : []; -const skillInsights: InsightItem[] = allSkillEntries - .slice(0, maxProjects) - .map(([skill, cost]) => ({ - category: "skill", - label: skill, - cost, - detail: pct(cost), - })); -if (allSkillEntries.length > maxProjects) { - const remainingCost = allSkillEntries - .slice(maxProjects) - .reduce((sum, [, c]) => sum + c, 0); - skillInsights.push({ - category: "skill", - label: `+${allSkillEntries.length - maxProjects} more`, - cost: remainingCost, - detail: pct(remainingCost), - }); -} - -// MCP server insights -const byMcp = new Map(); -for (const t of turns) { - if (t.mcpTools) { - for (const server of t.mcpTools) { - byMcp.set(server, (byMcp.get(server) ?? 0) + t.cost); - } - } -} -const allMcpEntries = [...byMcp.entries()].sort((a, b) => b[1] - a[1]); -const mcpInsights: InsightItem[] = allMcpEntries - .slice(0, maxProjects) - .map(([server, cost]) => ({ - category: "mcp", - label: server, - cost, - detail: pct(cost), - })); -if (allMcpEntries.length > maxProjects) { - const remainingCost = allMcpEntries - .slice(maxProjects) - .reduce((sum, [, c]) => sum + c, 0); - mcpInsights.push({ - category: "mcp", - label: `+${allMcpEntries.length - maxProjects} more`, - cost: remainingCost, - detail: pct(remainingCost), - }); -} -``` - -Update the return to include all categories in order: - -```ts -return [ - ...projectInsights, - ...skillInsights, - ...mcpInsights, - { - category: "cost", - label: "Parallel sessions", - cost: parallelCost, - detail: `${pct(parallelCost)} cost while >=4 active`, - }, - { - category: "cost", - label: "Large context", - cost: largeContext, - detail: `${pct(largeContext)} over 150k context`, - }, - { - category: "cost", - label: "Large uncached", - cost: largeUncached, - detail: `${pct(largeUncached)} over 100k input`, - }, - { - category: "cost", - label: "Long sessions", - cost: longSessionCost, - detail: `${pct(longSessionCost)} from 8h+ sessions`, - }, - { - category: "cost", - label: "Top-5 concentration", - cost: top5, - detail: `${pct(top5)} in top 5 sessions`, - }, -]; -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm test -- tests/offline.test.ts` - -Expected: PASS. - -- [ ] **Step 5: Run full check** - -Run: `pnpm check` - -Expected: all lint, typecheck, and tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/core/offline.ts tests/offline.test.ts -git commit -m "feat(insights): add skill and MCP server breakdowns" -``` diff --git a/docs/superpowers/plans/2026-06-22-next-release-docs-plan.md b/docs/superpowers/plans/2026-06-22-next-release-docs-plan.md deleted file mode 100644 index d006fa9..0000000 --- a/docs/superpowers/plans/2026-06-22-next-release-docs-plan.md +++ /dev/null @@ -1,430 +0,0 @@ -# Next Release Docs Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Refresh the next release’s documentation surface so `README.md`, `CHANGELOG.md`, and license metadata match the current shipped behavior. - -**Architecture:** Keep the implementation docs-only and stage it in three focused slices: first rewrite `README.md` as a usage guide, then add a release-ready `CHANGELOG.md` entry based on commits after `v0.4.0`, then verify MIT license surfacing and package contents. Avoid code changes, historical changelog rewrites, or version bumps unless explicitly requested during execution. - -**Tech Stack:** Markdown, JSON metadata, git, pnpm - -**Spec:** `docs/superpowers/specs/2026-06-22-next-release-docs-design.md` - ---- - -## File Map - -| File | Action | Responsibility | -| --- | --- | --- | -| `README.md` | Modify | Rewrite as a usage-first guide, update Node badge, add `usage.json` documentation, and remove development-focused framing | -| `CHANGELOG.md` | Modify | Add the next release notes entry summarizing shipped changes since `v0.4.0` | -| `package.json` | Verify | Confirm `license` and packaged `files` metadata stay aligned with the repo root artifacts | -| `LICENSE` | Verify | Confirm the existing MIT text remains present at the repo root | - ---- - -## Task 1: Rewrite README.md as a usage-first guide - -**Files:** -- Modify: `README.md` -- Verify against: `src/shared/types.ts`, `src/index.ts`, `src/core/usage-core.ts`, `package.json` - -- [ ] **Step 1: Replace `README.md` with the full usage-first draft** - -Overwrite `README.md` with this exact content: - -```md -# @pi-vault/pi-usage - -[![npm version](https://img.shields.io/npm/v/%40pi-vault%2Fpi-usage)](https://www.npmjs.com/package/@pi-vault/pi-usage) -[![Quality](https://github.com/pi-vault/pi-usage/actions/workflows/quality.yml/badge.svg?branch=master)](https://github.com/pi-vault/pi-usage/actions/workflows/quality.yml) -[![Node >= 24.15.0](https://img.shields.io/badge/node-%3E%3D24.15.0-339933?logo=node.js&logoColor=white)](https://nodejs.org/) -[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) - -Track Pi usage across your sessions in one dashboard. `@pi-vault/pi-usage` combines offline history with live provider snapshots so you can review costs, tokens, session activity, current quotas, and usage insights without leaving Pi. - -![Pi usage dashboard showing the aggregated usage table for the "All Time" period, current usage quota bars for OpenAI/Codex, and the keyboard navigation hints at the bottom](docs/assets/dashboard-ui.png) - -## Install - -```bash -pi install npm:@pi-vault/pi-usage -``` - -Then reload Pi: - -```bash -/reload -``` - -## Commands - -- `/usage` opens the dashboard using cached live data when available. Use it for quick inspection. -- `/usage:refresh` forces a live refresh, rescans local history, and then opens the dashboard. - -## What the dashboard shows - -### Usage statistics - -The top section aggregates local Pi session history for the selected period. - -- switch between `Today`, `This Week`, `Last Week`, and `All Time` -- expand provider rows to inspect model-level usage -- compare sessions, messages, cost, total tokens, input, output, cache reads, and cache writes -- keep a running total row for everything currently shown - -### Current usage - -The lower section shows live quota and balance information for providers you have already configured. - -- switch between `OpenAI/Codex`, `MiniMax`, `StepFun`, `OpenCode Go`, `Command Code`, and `OpenRouter` -- view rolling-window quota bars like `5h` and weekly usage -- see balance-style fields where the provider exposes them -- get inline status for live, cached, stale, local, or unavailable data - -### Insights - -Press `v` to toggle insights for the selected period. - -- review the most expensive projects in your local session history -- see active skill and MCP server breakdowns when that data is present -- keep long sections readable through grouped insight categories and capped lists with overflow summaries - -## How to use it - -### Keyboard shortcuts - -- `[Tab/Shift-Tab]` switch provider tabs -- `[Left/Right]` switch time period -- `[Up/Down]` move through rows -- `[Enter/Space]` expand or collapse provider rows -- `[v]` toggle insights -- `[q/Esc]` close the dashboard - -## Configuration - -### `usage.json` - -Create `$PI_CODING_AGENT_DIR/extensions/usage.json` to disable specific live providers. - -Default behavior: -- if the file is missing, all providers stay enabled -- if the file is `{}`, all providers stay enabled -- if a provider is omitted, that provider stays enabled -- if the JSON is malformed, `@pi-vault/pi-usage` ignores it and falls back to the default behavior - -Default example: - -```json -{} -``` - -Explicit all-providers-enabled example: - -```json -{ - "providers": { - "openai-codex": { "enabled": true }, - "minimax": { "enabled": true }, - "stepfun": { "enabled": true }, - "opencode-go": { "enabled": true }, - "command-code": { "enabled": true }, - "openrouter": { "enabled": true } - } -} -``` - -Disable MiniMax only: - -```json -{ - "providers": { - "minimax": { "enabled": false } - } -} -``` - -### Provider setup - -Offline history works without extra setup. Live provider cards appear only for providers you have both configured and left enabled. - -#### OpenAI/Codex - -Pi usage can reuse existing Pi or Codex auth. Optional overrides: - -- `OPENAI_CODEX_OAUTH_TOKEN` -- `OPENAI_CODEX_ACCESS_TOKEN` -- `CODEX_OAUTH_TOKEN` -- `CODEX_ACCESS_TOKEN` -- `OPENAI_CODEX_ACCOUNT_ID` -- `CHATGPT_ACCOUNT_ID` - -#### MiniMax - -Set one of: - -- `MINIMAX_CODING_API_KEY` -- `MINIMAX_API_KEY` - -Optional override: - -- `MINIMAX_API_HOST` - -#### StepFun - -Set one of: - -- `STEPFUN_TOKEN` -- `STEPFUN_USERNAME` and `STEPFUN_PASSWORD` - -#### OpenCode Go - -Set: - -- `OPENCODE_GO_COOKIE_HEADER` -- `OPENCODE_GO_WORKSPACE_ID` - -`OPENCODE_GO_WORKSPACE_ID` accepts either the raw `wrk_...` id or the full workspace URL. - -#### Command Code - -Set: - -- `COMMAND_CODE_COOKIE_HEADER` - -#### OpenRouter - -Set: - -- `OPENROUTER_API_KEY` - -Optional overrides: - -- `OPENROUTER_API_URL` -- `OPENROUTER_X_TITLE` -- `OPENROUTER_HTTP_REFERER` - -## Changelog - -See [`CHANGELOG.md`](CHANGELOG.md) for release-by-release notes. - -## License - -MIT — see [`LICENSE`](LICENSE). -``` - -- [ ] **Step 2: Verify README claims against the codebase** - -Run: - -```bash -rg -n '"node":|"license":|"files"' package.json -sed -n '1,20p' src/shared/types.ts -rg -n 'registerCommand\("usage|registerCommand\("usage:refresh' src/index.ts -rg -n 'join\(deps.agentDir\(\), "extensions", "usage.json"\)|return \{\}' src/core/usage-core.ts -rg -n 'treats missing config file as all providers enabled|ignores malformed config JSON' tests/usage-core.test.ts -``` - -Expected: -- `package.json` shows Node `>=24.15.0` and license `MIT` -- `src/shared/types.ts` lists `offline` plus the configurable live providers: `openai-codex`, `minimax`, `stepfun`, `opencode-go`, `command-code`, and `openrouter` -- `src/index.ts` still registers `/usage` and `/usage:refresh` -- `src/core/usage-core.ts` loads `$PI_CODING_AGENT_DIR/extensions/usage.json` and falls back to `{}` on errors -- `tests/usage-core.test.ts` explicitly covers both the missing-file and malformed-JSON behaviors - -- [ ] **Step 3: Read the rendered README sections once before moving on** - -Run: - -```bash -rg -n '^## |^### |usage.json|CHANGELOG|LICENSE|Node >=' README.md -``` - -Expected headings present: -- `## Install` -- `## Commands` -- `## What the dashboard shows` -- `## How to use it` -- `## Configuration` -- `## Changelog` -- `## License` - -Also confirm there is no `## Development` section anymore. - -- [ ] **Step 4: Commit the README refresh** - -```bash -git add README.md -git commit -m "docs: refresh README for current usage flow" -``` - ---- - -## Task 2: Add the next release entry to CHANGELOG.md - -**Files:** -- Modify: `CHANGELOG.md` -- Verify against: `git log v0.4.0..HEAD` - -- [ ] **Step 1: Insert an `Unreleased` section above `## v0.4.0`** - -Add this exact block after the intro paragraph in `CHANGELOG.md`: - -```md -## Unreleased - -- added `usage.json` provider toggles so you can disable live providers you do not want queried -- added dashboard insights for project, active skill, and MCP server breakdowns -- grouped insight rows by category and cap each insight section with an overflow summary to keep the dashboard readable -- improved offline session parsing to extract project names, active skills, MCP server names, and builtin tool usage more accurately -- fixed dashboard readability around usage statistics spacing and grouped insight rendering -- updated runtime requirements and tooling to Node `>=24.15.0`, refreshed dependencies, and aligned CI with Node 24 and pnpm 11.8.0 -``` - -Do not rewrite older version sections in this task. - -- [ ] **Step 2: Verify every bullet against the commit history** - -Run: - -```bash -git log --oneline v0.4.0..HEAD -``` - -Expected supporting commits include: -- `feat(core): add provider enable/disable toggle via extensions/usage.json` -- `feat(insights): add project breakdown by cost` -- `feat(insights): add skill and MCP server breakdowns` -- `feat(tui): render insights grouped by category` -- `feat(insights): cap project insights at 5 with overflow summary` -- `fix(offline): expand BUILTIN_TOOLS from real session data, rename cap constant` -- `chore: update node engine to 24.15.0, refresh package dependencies, and adjust project metadata` -- `chore: upgrade pnpm to 11.8.0 and Node.js to 24 in CI workflows` - -If a bullet cannot be traced to the log, fix the wording now rather than carrying a vague release note forward. - -- [ ] **Step 3: Decide whether to keep `Unreleased` or rename it during the actual release cut** - -During this plan’s implementation, leave the heading as `## Unreleased` unless the user explicitly provides the final version number. If the release number is known during execution, rename the heading to `## vX.Y.Z` only after the wording is finalized. - -- [ ] **Step 4: Read the top of the changelog after editing** - -Run: - -```bash -sed -n '1,30p' CHANGELOG.md -``` - -Expected: -- the file starts with `# Changelog` -- the intro paragraph stays intact -- `## Unreleased` appears before `## v0.4.0` -- the new bullets read cleanly and do not duplicate older entries verbatim - -- [ ] **Step 5: Commit the changelog update** - -```bash -git add CHANGELOG.md -git commit -m "docs: add unreleased release notes" -``` - ---- - -## Task 3: Verify MIT license surfacing and packaged release files - -**Files:** -- Verify: `LICENSE` -- Verify: `package.json` -- Verify: `README.md` -- Verify: `CHANGELOG.md` - -- [ ] **Step 1: Confirm MIT license metadata and root files are aligned** - -Run: - -```bash -test -f LICENSE && echo "LICENSE present" -rg -n '"license": "MIT"' package.json -rg -n '\[!\[License: MIT\]|\]\(LICENSE\)|\[`LICENSE`\]\(LICENSE\)' README.md -rg -n '"files"|LICENSE|CHANGELOG.md|README.md' package.json -``` - -Expected: -- `LICENSE present` -- `package.json` contains `"license": "MIT"` -- `README.md` includes both the MIT badge and the final license link -- `package.json` includes `LICENSE`, `CHANGELOG.md`, and `README.md` in the packaged file list - -- [ ] **Step 2: If any license surface is inconsistent, fix it immediately** - -Use these exact targets if a repair is needed: - -```json -{ - "license": "MIT", - "files": [ - "src", - "docs/assets", - "LICENSE", - "CHANGELOG.md", - "README.md" - ] -} -``` - -For README fixes, keep the license badge and final section in this form: - -```md -[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) -``` - -```md -## License - -MIT — see [`LICENSE`](LICENSE). -``` - -If all checks already pass, make no file edits in this step. - -- [ ] **Step 3: Run package and repo verification** - -Run: - -```bash -pnpm pack --dry-run -pnpm check -git diff --check -``` - -Expected: -- `pnpm pack --dry-run` succeeds and includes `LICENSE`, `CHANGELOG.md`, `README.md`, and `docs/assets/dashboard-ui.png` -- `pnpm check` passes -- `git diff --check` prints no whitespace errors - -- [ ] **Step 4: Create the final documentation commit if this task changed any files** - -If Step 2 required edits: - -```bash -git add package.json README.md LICENSE CHANGELOG.md -git commit -m "docs: align license metadata and package contents" -``` - -If Step 2 made no edits, skip this commit. - ---- - -## Self-Review Checklist - -- Spec coverage: - - README usage-only rewrite: covered by Task 1 - - `usage.json` defaults and explicit examples: covered by Task 1 - - next release notes based on post-`v0.4.0` commits: covered by Task 2 - - MIT license verification and package surfacing: covered by Task 3 -- Placeholder scan: - - no `TBD`, `TODO`, or “similar to above” placeholders remain - - every edit step includes exact markdown or command targets -- Type and naming consistency: - - provider IDs in the README match `ProviderId` names in `src/shared/types.ts` - - the changelog references shipped features only - - the plan leaves the version heading as `Unreleased` until the release number is explicitly chosen diff --git a/docs/superpowers/specs/2026-06-21-dashboard-enhancements-design.md b/docs/superpowers/specs/2026-06-21-dashboard-enhancements-design.md deleted file mode 100644 index 27900fd..0000000 --- a/docs/superpowers/specs/2026-06-21-dashboard-enhancements-design.md +++ /dev/null @@ -1,239 +0,0 @@ -# Dashboard Enhancements Design - -Three independent improvements to the pi-usage dashboard: provider enable/disable toggle, richer insights from session data, and a spacing fix. - -## Feature 1: Provider Enable/Disable Toggle - -### Problem - -Live provider cards appear for every provider that has credentials configured. There is no way to suppress a provider you have env vars set for but don't want pi-usage to query. - -### Solution - -A per-provider toggle stored in a config file. Disabled providers are filtered out at startup -- no API calls, no cache writes, no dashboard cards. - -### Config file - -**Path:** `$PI_CODING_AGENT_DIR/extensions/usage.json` - -```json -{ - "providers": { - "minimax": { "enabled": false }, - "stepfun": { "enabled": false } - } -} -``` - -**Rules:** - -- File is optional. Missing file means all providers enabled. -- Unmentioned providers default to enabled. -- `{ "enabled": false }` disables the provider entirely. -- Config is read once at `UsageCore` initialization. Changes require Pi restart or `/reload`. - -### Type - -```ts -// src/shared/types.ts -export interface UsageConfig { - providers?: Partial>; -} -``` - -### Code changes - -**`src/core/usage-core.ts`:** - -- Add `loadConfig(deps: UsageDeps): Promise` that reads `join(deps.agentDir(), "extensions", "usage.json")`, parses JSON, returns `UsageConfig`. Returns `{}` on missing file or parse error. -- In `UsageCore` constructor (or `init()`), after `createProviderRegistry(deps)`, filter the provider list: remove any provider where `config.providers?.[id]?.enabled === false`. -- `isLiveProvider()`, `startLiveRuntime()`, and all downstream logic operate on the filtered registry, so disabled providers vanish automatically. - -**`src/tui/dashboard.ts`:** No changes. Dashboard renders what `state.providers` contains, which already excludes filtered-out providers. - ---- - -## Feature 2: Richer Insights - -### Problem - -The current insights (toggled with `v`) show five cost-pattern categories: parallel sessions, large context, large uncached, long sessions, and top-5 concentration. These are useful but miss the most actionable information: which projects, skills, and MCP servers are driving usage. - -### Solution - -Enrich the offline scan to extract additional data from session JSONL files, then compute new insight categories for projects, skills, and MCP servers. - -### Data extraction - -Enhance the JSONL scan in `src/core/offline.ts` to extract three new data points per session/turn: - -**1. Session CWD (project attribution):** - -- Source: `type: "session"` header entry (first line of each JSONL file), `cwd` field. -- Derive project name from last path segment (e.g. `/Users/lanh/Developer/career-ops` -> `career-ops`). -- Tag every turn in that session with the project name. - -**2. Skill invocations:** - -- Source: `type: "message"` entries with `role: "user"`, text content containing ``. -- Extract skill name with regex: ` `playwright`, `firefox_devtools_take_snapshot` -> `firefox`, `minimax_coding_plan_web_search` -> `minimax`). This is a heuristic -- multi-word server names like `firefox-devtools` will appear as their first segment (`firefox`). Acceptable for v1; can be refined with a known-prefix mapping later. -- Built-in tools are excluded from MCP attribution. Known built-in tools (from the Pi SDK's `core/tools/`): `bash`, `read`, `write`, `edit`, `grep`, `ls`, `find`. -- Single-word tool names that aren't built-in (e.g. `tavily`, `ddgs`, `exa`, `firecrawl`) are treated as direct MCP server calls and attributed to that server name. - -### Enriched turn type - -```ts -// src/core/offline.ts -export interface UsageTurn { - // existing fields - id: string; - sessionId: string; - timestamp: number; - provider: string; - model: string; - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - tokens: number; - cost: number; - // new fields - project?: string; - activeSkill?: string; - mcpTools?: string[]; -} -``` - -### New insight categories - -Added to the output of `buildInsights()`. Each entry uses the existing `InsightItem` type with an added `category` field: - -```ts -// src/core/offline.ts -export interface InsightItem { - category?: string; // "project" | "skill" | "mcp" | "cost" - label: string; - cost: number; - detail: string; -} -``` - -**Top projects:** Group turns by `project`, sum cost, sort by cost descending, cap at 5. If more than 5 projects exist, append one summary row (`+N more`) aggregating the remaining projects' cost and percentage. Label: project name. Detail: percentage of total cost. - -**Top skills:** Group turns by `activeSkill` (where set), sum cost, sort by cost descending, cap at 5. If more than 5 skills exist, append one summary row (`+N more`) aggregating the remaining skills' cost and percentage. Turns with no active skill are grouped as "(no skill)". Label: skill name (with `/` prefix for display). Detail: percentage of total cost. - -**Top MCP servers:** For each turn, extract unique MCP server prefixes from `mcpTools`. Attribute that turn's full cost to each server used. Sort by cost descending, cap at 5. If more than 5 servers exist, append one summary row (`+N more`) aggregating the remaining servers' cost and percentage. Label: server name. Detail: percentage of total cost. - -Existing five cost-pattern insights remain, tagged with `category: "cost"`. - -### Dashboard rendering - -The insights view (toggled with `v`) groups insights by category: - -``` -Insights - - Projects % of usage - career-ops 91.2% - pi-packages 6.1% - dotfiles 2.7% - - Skills % of usage - /executing-plans 45.6% - /writing-plans 22.3% - (no skill) 32.1% - - MCP servers % of usage - playwright 50.0% - firefox-devtools 12.3% - - Cost patterns - - Parallel sessions: $0.50 (10.8%) - - Large context: $1.20 (26.0%) - - Top-5 concentration: $3.80 (82.4%) -``` - -Rendering changes in `src/tui/dashboard.ts`: - -- Group `state.insights` by `category`. -- For `project`, `skill`, `mcp` categories: render as a two-column table (label + percentage), with a category header. -- For `cost` category (or items without a category): render as the existing `- label: $cost (detail)` format. - ---- - -## Feature 3: Spacing Fix - -### Problem - -The "Usage Statistics" section title runs directly into the period tabs with no visual separation. The "Current Usage" section has a border separator between its title and tabs, creating an inconsistency. - -### Solution - -Add one empty line between the "Usage Statistics" section title and the period tabs. - -### Code change - -**`src/tui/dashboard.ts`, `renderUsageStatistics()` method:** - -```ts -// Before -lines.push( - this.sectionTitle(UI_STRINGS.dashboardBorderedSectionTitles.usageStatistics), -); -lines.push( - ...this.renderTabs( - PERIODS.map((period) => PERIOD_LABELS[period]), - this.periodIndex, - w, - ), -); - -// After -lines.push( - this.sectionTitle(UI_STRINGS.dashboardBorderedSectionTitles.usageStatistics), -); -lines.push(""); -lines.push( - ...this.renderTabs( - PERIODS.map((period) => PERIOD_LABELS[period]), - this.periodIndex, - w, - ), -); -``` - ---- - -## Testing - -### Feature 1 (provider toggle) - -- Unit test: `loadConfig()` returns empty object for missing file, parses valid config, ignores malformed JSON. -- Unit test: `UsageCore` with config disabling a provider excludes it from the registry. -- Integration: disabled provider has no snapshot in `state.providers`. - -### Feature 2 (richer insights) - -- Unit test: `parseLine()` / scan loop extracts `project`, `activeSkill`, `mcpTools` from session entries. -- Unit test: skill tagging propagates across turns within a session. -- Unit test: MCP server inference from tool name prefixes (underscore splitting, known built-ins excluded). -- Unit test: `buildInsights()` produces correct project/skill/mcp categories with percentages. -- Snapshot test: insights rendering groups by category. - -### Feature 3 (spacing) - -- Existing dashboard render tests should be updated to expect the extra empty line. - -## Verification - -- `pnpm check` (lint + typecheck + tests) passes. -- Manual verification: open `/usage` dashboard, toggle insights with `v`, confirm new categories render. -- Manual verification: disable a provider in config, reload Pi, confirm provider card is absent and no API calls are made. diff --git a/docs/superpowers/specs/2026-06-21-project-insights-cap-design.md b/docs/superpowers/specs/2026-06-21-project-insights-cap-design.md deleted file mode 100644 index 065816e..0000000 --- a/docs/superpowers/specs/2026-06-21-project-insights-cap-design.md +++ /dev/null @@ -1,46 +0,0 @@ -# Project Insights Cap - -Limit project breakdown insights to the top 5 projects by cost, with a summary overflow row when more exist. - -## Problem - -The project insights list grows unbounded. Users with many session directories see a long list that overwhelms the insights view and pushes cost-pattern insights off screen. - -## Solution - -In `buildInsights()`, after sorting projects by cost descending, take only the first 5. If more than 5 projects exist, append a summary item that aggregates the remaining projects' cost and percentage. - -### Data layer (`src/core/offline.ts`) - -After sorting `projectInsights` by cost descending: - -1. If `projectInsights.length <= 5`, return as-is (no change). -2. If `projectInsights.length > 5`: - - Keep the first 5 items. - - Sum the cost of remaining items. - - Append one summary item: `{ category: "project", label: "+N more", cost: remainingCost, detail: pct(remainingCost) }` where N is the count of remaining projects. - -### Dashboard rendering - -No changes. The renderer already handles any `InsightItem` with `category: "project"`. The overflow row renders as a normal project row in the table. - -### Rendered example - -``` - Projects % of usage - career-ops 42.3% - dotfiles 18.1% - pi-vault 12.0% - homelab 8.5% - scripts 6.2% - +12 more 12.9% -``` - -## Testing - -- Existing test with 2 projects: unaffected (under cap). -- New test with 7 projects: verify only 6 items returned (5 individual + 1 overflow), overflow label is "+2 more", overflow cost is sum of bottom 2 projects' costs, overflow detail contains the correct percentage. - -## Verification - -`pnpm check` passes. diff --git a/docs/superpowers/specs/2026-06-22-next-release-docs-design.md b/docs/superpowers/specs/2026-06-22-next-release-docs-design.md deleted file mode 100644 index d0645c3..0000000 --- a/docs/superpowers/specs/2026-06-22-next-release-docs-design.md +++ /dev/null @@ -1,156 +0,0 @@ -# Next Release Docs Design - -## Summary - -Prepare the next release by refreshing the project’s release-facing documentation around the current shipped codebase. - -This work covers three surfaces: -- `README.md`: rewrite it as a usage-first guide -- `CHANGELOG.md`: add or refresh the next release entry based on code changes since `v0.4.0` -- `LICENSE`: verify existing MIT licensing stays correctly surfaced - -## Goals - -- Make `README.md` reflect current user-facing usage rather than internal implementation details -- Ensure `CHANGELOG.md` clearly explains what changed for the next release -- Keep licensing aligned across `LICENSE`, `package.json`, and README references -- Keep the scope narrow enough to support a single follow-up implementation plan - -## Non-goals - -- Retroactively rewriting all past changelog entries -- Making feature or behavior changes just to support documentation updates -- Changing the project license -- Performing unrelated repository cleanup - -## Current context - -Current repo state already includes: -- `README.md` -- `CHANGELOG.md` -- `LICENSE` -- package version `0.4.0` - -Commits since `v0.4.0` include user-visible changes around provider toggles, richer dashboard insights, offline extraction improvements, and runtime/tooling updates. - -## Deliverable 1: README.md - -### Intent - -Rewrite `README.md` as a practical usage guide for people installing and operating `@pi-vault/pi-usage`. - -### Required structure - -1. **What it is** - - brief description of the dashboard and its value -2. **Install** - - installation command - - Pi reload step -3. **Commands** - - `/usage` - - `/usage:refresh` - - when to use each -4. **What the dashboard shows** - - offline usage aggregation - - live provider status, quota, and balance surfaces - - currently shipped insights, including project, skill, and MCP breakdowns where applicable -5. **How to use it** - - period switching - - provider tab switching - - row expansion and navigation - - keyboard shortcuts -6. **Configuration** - - provider environment variables for live usage cards - - `usage.json` configuration for provider enable/disable -7. **Changelog pointer** - - short pointer to `CHANGELOG.md` -8. **License** - - short pointer to MIT licensing - -### README style rules - -- Prefer usage-first language over architecture-first language -- Do not describe internal module names, refactors, or test/tooling details unless directly relevant to usage -- Keep examples copy-pasteable -- Only document behavior that exists in the current codebase - -### usage.json section requirements - -Document the config file at: -- `$PI_CODING_AGENT_DIR/extensions/usage.json` - -The README must make the default behavior explicit: -- missing file means all providers remain enabled -- omitted providers remain enabled -- malformed JSON is ignored and falls back to default behavior - -The README should include both: -- the effective default example: `{}` -- an explicit full example that lists the current configurable live providers with `enabled: true`: `openai-codex`, `minimax`, `stepfun`, `opencode-go`, `command-code`, and `openrouter` - -It should also include at least one focused example showing how to disable a provider such as `minimax`. - -## Deliverable 2: CHANGELOG.md - -### Intent - -Update the next release entry so it reflects code changes after `v0.4.0`, written as release notes rather than a raw commit list. - -If the final version has not yet been chosen when editing begins, use an `Unreleased` section first and convert it to the final version heading once the release number is decided. - -### Writing policy - -- Lead with user-visible changes -- Include internal/runtime changes only when they materially affect compatibility, support expectations, or release risk -- Keep older release entries mostly intact -- Allow light consistency edits to nearby text if needed, but do not perform a full historical rewrite - -### Planned content areas - -#### User-visible additions -- provider enable/disable config via `usage.json` -- richer dashboard insights, including: - - project breakdowns - - active skill breakdowns - - MCP server breakdowns - - grouped and capped insight presentation where shipped - -#### Behavior fixes and improvements -- improved offline extraction from session data -- more accurate project-name extraction -- MCP server-name extraction from tool prefixes -- builtin tool handling improvements where they affect insight accuracy -- TUI readability fixes such as spacing or grouped rendering improvements - -#### Notable internal/runtime changes -- Node engine raised to `>=24.15.0` -- dependency refreshes -- CI/runtime alignment updates - -These internal items should appear after user-facing notes and remain brief. - -### Versioning guidance - -The final version bump should be chosen after drafting the release notes. Based on currently known changes, a minor bump appears more likely than a patch because the release includes new shipped capability. - -## Deliverable 3: LICENSE - -### Intent - -Treat licensing as a verification task, not a legal/content change. - -### Requirements - -- keep the existing MIT license text -- verify `LICENSE` remains at repository root -- verify `package.json` still declares `"license": "MIT"` -- verify README badge/linking remains aligned with the root license file -- if inconsistencies exist, fix docs or metadata rather than changing the license itself - -## Success criteria - -This design is complete when the follow-up implementation produces: -- a usage-first `README.md` aligned with current shipped behavior -- a next release changelog entry that accurately summarizes changes since `v0.4.0` -- verified MIT license surfacing across repository files -- enough release-note clarity to choose the next version intentionally