From 5d4b3d4e92aa5261fbb411e83a9eb8013c3d8dae Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Wed, 12 Aug 2026 15:08:27 -0400 Subject: [PATCH 1/7] deps: update dependencies --- package.json | 16 +- pnpm-lock.yaml | 439 ++++++++++++++++++++++++++++++++++---------- pnpm-workspace.yaml | 10 +- 3 files changed, 350 insertions(+), 115 deletions(-) diff --git a/package.json b/package.json index 46b819a..164c304 100644 --- a/package.json +++ b/package.json @@ -54,17 +54,17 @@ "@mozilla/readability": "^0.6.0", "linkedom": "^0.18.13", "turndown": "^7.2.4", - "unpdf": "^1.6.2" + "unpdf": "^1.8.0" }, "devDependencies": { - "@biomejs/biome": "^2.5.5", - "@earendil-works/pi-ai": "^0.80.10", - "@earendil-works/pi-coding-agent": "^0.80.10", - "@earendil-works/pi-tui": "^0.80.10", - "@types/node": "^26.1.1", + "@biomejs/biome": "^2.5.8", + "@earendil-works/pi-ai": "^0.84.1", + "@earendil-works/pi-coding-agent": "^0.84.1", + "@earendil-works/pi-tui": "^0.84.1", + "@types/node": "^26.2.0", "@types/turndown": "^5.0.6", - "typebox": "^1.3.6", - "typescript": "^6.0.3", + "typebox": "^1.3.13", + "typescript": "^7.0.2", "vitest": "^4.1.10" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a71b24..4fff621 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,36 +21,36 @@ importers: specifier: ^7.2.4 version: 7.2.4 unpdf: - specifier: ^1.6.2 - version: 1.6.2 + specifier: ^1.8.0 + version: 1.8.0 devDependencies: '@biomejs/biome': - specifier: ^2.5.5 - version: 2.5.5 + specifier: ^2.5.8 + version: 2.5.8 '@earendil-works/pi-ai': - specifier: ^0.80.10 - version: 0.80.10(ws@8.21.0)(zod@4.4.3) + specifier: ^0.84.1 + version: 0.84.1(ws@8.21.0)(zod@4.4.3) '@earendil-works/pi-coding-agent': - specifier: ^0.80.10 - version: 0.80.10(ws@8.21.0)(zod@4.4.3) + specifier: ^0.84.1 + version: 0.84.1(ws@8.21.0)(zod@4.4.3) '@earendil-works/pi-tui': - specifier: ^0.80.10 - version: 0.80.10 + specifier: ^0.84.1 + version: 0.84.1 '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.2.0 + version: 26.2.0 '@types/turndown': specifier: ^5.0.6 version: 5.0.6 typebox: - specifier: ^1.3.6 - version: 1.3.6 + specifier: ^1.3.13 + version: 1.3.13 typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.0.2 + version: 7.0.2 vitest: specifier: ^4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(vite@8.1.0(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.2.0)(vite@8.1.0(@types/node@26.2.0)(jiti@2.7.0)(yaml@2.9.0)) packages: @@ -168,79 +168,91 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.5.5': - resolution: {integrity: sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==} + '@biomejs/biome@2.5.8': + resolution: {integrity: sha512-aeAeeJB9fSDc7Gq+2GqpQxA0qBj6gj1k2R6L1cYqGePKP/baIq1WX8y6B+D+nRsO5ViQL22K/8IwbqERW0q1nw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.5': - resolution: {integrity: sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==} + '@biomejs/cli-darwin-arm64@2.5.8': + resolution: {integrity: sha512-mk1QON9PHllvvLN5gU3f4rMxeh4syK5p9OvKyWH6/W8ueh04uaC8TUXXByhGufWf/y5mQc03ZLM45zU+cmqMjA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.5': - resolution: {integrity: sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==} + '@biomejs/cli-darwin-x64@2.5.8': + resolution: {integrity: sha512-bsGwFMBNyHPyiLSsQcZJxdoRrg1V4JL+d7wEsvUBczlP9U9lwM+7mzQHxI4o1mhBsTmdOBbAb6fHU3Z3snN45w==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.5': - resolution: {integrity: sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==} + '@biomejs/cli-linux-arm64-musl@2.5.8': + resolution: {integrity: sha512-VcJNbstduTHx83NGAdhp78/JOcP45BZHXL7yNsfI1uGzdUgegAz2s+mSoT7wK6PBNzLoqG0zDOXaz/RQYVtSiw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.5.5': - resolution: {integrity: sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==} + '@biomejs/cli-linux-arm64@2.5.8': + resolution: {integrity: sha512-XmFiA0WPYFC+uiUDC8WRFzAIH9bo7vwQLav38Uoq4ETC+T/+uBi0TsYGJECkugY3r8USl3jc+Ae2/irAF6F2lQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.5': - resolution: {integrity: sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==} + '@biomejs/cli-linux-x64-musl@2.5.8': + resolution: {integrity: sha512-kKmiyokeISRGq2FLwvr+TzsgBusfxaZ0FZNLcOYOpCK/78tRrEjeEBLvq3xLZMpqbANgJdRPI7vZX8ZL37u9/w==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.5.5': - resolution: {integrity: sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==} + '@biomejs/cli-linux-x64@2.5.8': + resolution: {integrity: sha512-S5wcm9OBDvLHodD4PUaN488hCpco9QD/9ZxuYJiw4euWtr/oQvLR72z2ixItH8Wd5BCm6FZaeb+YNvOoM1xHtQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.5': - resolution: {integrity: sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==} + '@biomejs/cli-win32-arm64@2.5.8': + resolution: {integrity: sha512-nILH0mzm3Hi3iEdd7o7GpB8kBR/mSQwfQG/tyBqyNrY2GFtcgwfV9nV8xLmbtUpMNY/Oi0Ml1XgfR4flOdq+AA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.5': - resolution: {integrity: sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==} + '@biomejs/cli-win32-x64@2.5.8': + resolution: {integrity: sha512-I2czzXTY61f3nFJxXoMDq80t7MivxDEnCjE+8sDKoFfcKMaoQdkqhIFQ3KyY0XLzeSpUBYeNAXgD+iOV/BU0VA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] - '@earendil-works/pi-agent-core@0.80.10': - resolution: {integrity: sha512-nwnOR3SuLYGRFfyQm8ri4Nj5VGVAvAM9GuqQd3u7BUQj0d6hmD2F8w7OHAAjThE3CuySIdM+v8E22QJG6/RfCg==} + '@earendil-works/pi-agent-core@0.84.1': + resolution: {integrity: sha512-evyzXYWCLQGmcaBYHlmSku02r8qoN4SGI60GZABo6iV+H+nqX+P9ud8fEZ4GmRq9mUSREvvfX+w9dA9ThF9C6w==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.80.10': - resolution: {integrity: sha512-Moe/H8c87yacDGK9dPbWphZNjVsrb3nTrIHycOQJAkFEnY9PYxOOd74+ny44kATfPU9Dm7aTHefar3pZF+UKUA==} + '@earendil-works/pi-ai@0.84.1': + resolution: {integrity: sha512-wMsAdJMxuNri08vLqTyYVI201DQQezGhPSTkzYsHdw5dYX3rCNwEmSvpaAwhi7ELKI/2tE/CEgSWg/6iRxSgdQ==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-coding-agent@0.80.10': - resolution: {integrity: sha512-aL4apbupCHiVLSXASXvRzH4Q2vmtfrDa+0s909CJuVu/GgGylbDzr7oyF1mPmip5E+VxYYxKWmph4hV04wUcQg==} + '@earendil-works/pi-client@0.84.1': + resolution: {integrity: sha512-/V5hGHE4Zq+jG0GtwIB9PyBUOGd6gBLZ7lkQYFKchKnxYHeH3rmWC5xw4kpnZKKBuBuFTdLVbU9vEjlAGMMb2A==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-coding-agent@0.84.1': + resolution: {integrity: sha512-ncAqFrG+iybuPGOhMiZoEHkEzTpJgz3guYD32pD+M7ucc0WeHmauP6wa7qwP8V/KWvsZDVNa5XGsdZ7fkC7w7A==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-tui@0.80.10': - resolution: {integrity: sha512-c2JO29PbhKPEQ6fgHQKAl0WhwuFqzWfzspMmP+8B5tpDuP+0mvarRbKKg8gq4b+pQx/QX+6aVS4ko7deoyjQjg==} + '@earendil-works/pi-protocol@0.84.1': + resolution: {integrity: sha512-Ox1pciyeSPGEEUcxvR0/dJcrY7C6hrEGA8y71rOsvSIUlXN1Cbp/be/eoL71OGDBk5O97TeQPfWN6Ju/2Ehjww==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-telemetry@0.84.1': + resolution: {integrity: sha512-180/xGJtsq7IoR3p9EKWjRd0e9M4DkxInhlo9xyD7prDC7Qrhqq+nhvwrW0lFjPfXcEI2FSHmGCSyvSJE9GsaQ==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-tui@0.84.1': + resolution: {integrity: sha512-udeXFbgEhJ6JiB0uguwNVNkDy2FENfmtQwPcY+/iJ8GWeq18wkal1tKqa5YyeH0IqtX1vG0cGh8zfSYzyzVuLA==} engines: {node: '>=22.19.0'} '@emnapi/core@1.11.1': @@ -553,12 +565,135 @@ packages: '@types/node@26.1.1': resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} '@types/turndown@5.0.6': resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + '@vitest/expect@4.1.10': resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} @@ -773,6 +908,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + grok-mermaid@0.2.2: + resolution: {integrity: sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==} + engines: {node: '>=18'} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -1067,15 +1206,15 @@ packages: resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==} engines: {node: '>=18', npm: '>=9'} - typebox@1.1.38: - resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} + typebox@1.3.13: + resolution: {integrity: sha512-rzv/3uBDgWGb5b4+Rqb/WoDFCc+5N0iRC/7kgQyk/mJJeumQjHFFAZ+X+Zs+CNxnNnJOkhIMrQc2irTzfEbKjw==} - typebox@1.3.6: - resolution: {integrity: sha512-Sc8RA0NCMEFmApHNU9ZMzqcpQj46She44J8ffpLM/bdhLNUZKq7DJumcLcsFx1gRmDfQPgCgOmFFJ7rcnfWNyA==} + typebox@1.3.7: + resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==} - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} hasBin: true uhyphen@0.2.0: @@ -1084,14 +1223,15 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - undici@8.5.0: - resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} engines: {node: '>=22.19.0'} - unpdf@1.6.2: - resolution: {integrity: sha512-zQ80ySoPuPHOsvIoRp/nJyQt8TOUoTh1+WBCGcBvlddQNgKDLRwm0AY3x8Q35I7+kIiRSgqMx+Ma2pl9McIp7A==} + unpdf@1.8.0: + resolution: {integrity: sha512-jQlkckbe5nKxRHQdbvo9IKHlU6Cjq00UlxxB8lrCIxvS2o5IbAL1wM+4a1Yc3+BIohg9p5AsoaftwO+G4Aq/qQ==} + engines: {node: '>=22'} peerDependencies: - '@napi-rs/canvas': ^0.1.69 + '@napi-rs/canvas': ^0.1.69 || ^1.0.0 peerDependenciesMeta: '@napi-rs/canvas': optional: true @@ -1451,46 +1591,48 @@ snapshots: '@babel/runtime@7.29.7': {} - '@biomejs/biome@2.5.5': + '@biomejs/biome@2.5.8': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.5 - '@biomejs/cli-darwin-x64': 2.5.5 - '@biomejs/cli-linux-arm64': 2.5.5 - '@biomejs/cli-linux-arm64-musl': 2.5.5 - '@biomejs/cli-linux-x64': 2.5.5 - '@biomejs/cli-linux-x64-musl': 2.5.5 - '@biomejs/cli-win32-arm64': 2.5.5 - '@biomejs/cli-win32-x64': 2.5.5 - - '@biomejs/cli-darwin-arm64@2.5.5': + '@biomejs/cli-darwin-arm64': 2.5.8 + '@biomejs/cli-darwin-x64': 2.5.8 + '@biomejs/cli-linux-arm64': 2.5.8 + '@biomejs/cli-linux-arm64-musl': 2.5.8 + '@biomejs/cli-linux-x64': 2.5.8 + '@biomejs/cli-linux-x64-musl': 2.5.8 + '@biomejs/cli-win32-arm64': 2.5.8 + '@biomejs/cli-win32-x64': 2.5.8 + + '@biomejs/cli-darwin-arm64@2.5.8': optional: true - '@biomejs/cli-darwin-x64@2.5.5': + '@biomejs/cli-darwin-x64@2.5.8': optional: true - '@biomejs/cli-linux-arm64-musl@2.5.5': + '@biomejs/cli-linux-arm64-musl@2.5.8': optional: true - '@biomejs/cli-linux-arm64@2.5.5': + '@biomejs/cli-linux-arm64@2.5.8': optional: true - '@biomejs/cli-linux-x64-musl@2.5.5': + '@biomejs/cli-linux-x64-musl@2.5.8': optional: true - '@biomejs/cli-linux-x64@2.5.5': + '@biomejs/cli-linux-x64@2.5.8': optional: true - '@biomejs/cli-win32-arm64@2.5.5': + '@biomejs/cli-win32-arm64@2.5.8': optional: true - '@biomejs/cli-win32-x64@2.5.5': + '@biomejs/cli-win32-x64@2.5.8': optional: true - '@earendil-works/pi-agent-core@0.80.10(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.84.1(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.80.10(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.84.1(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-telemetry': 0.84.1 + diff: 8.0.4 ignore: 7.0.5 - typebox: 1.1.38 + typebox: 1.3.7 yaml: 2.9.0 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -1500,10 +1642,11 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.80.10(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-ai@0.84.1(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 + '@earendil-works/pi-telemetry': 0.84.1 '@google/genai': 1.52.0 '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) '@opentelemetry/api': 1.9.0 @@ -1512,7 +1655,7 @@ snapshots: https-proxy-agent: 7.0.6 openai: 6.26.0(ws@8.21.0)(zod@4.4.3) partial-json: 0.1.7 - typebox: 1.1.38 + typebox: 1.3.7 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - bufferutil @@ -1521,16 +1664,23 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.80.10(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-client@0.84.1': + dependencies: + '@earendil-works/pi-protocol': 0.84.1 + + '@earendil-works/pi-coding-agent@0.84.1(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.80.10(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-ai': 0.80.10(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-tui': 0.80.10 + '@earendil-works/pi-agent-core': 0.84.1(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.84.1(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-client': 0.84.1 + '@earendil-works/pi-protocol': 0.84.1 + '@earendil-works/pi-tui': 0.84.1 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cross-spawn: 7.0.6 diff: 8.0.4 glob: 13.0.6 + grok-mermaid: 0.2.2 highlight.js: 10.7.3 hosted-git-info: 9.0.3 ignore: 7.0.5 @@ -1538,8 +1688,8 @@ snapshots: minimatch: 10.2.5 proper-lockfile: 4.1.2 semver: 7.8.0 - typebox: 1.1.38 - undici: 8.5.0 + typebox: 1.3.7 + undici: 8.9.0 yaml: 2.9.0 optionalDependencies: '@mariozechner/clipboard': 0.3.9 @@ -1551,7 +1701,13 @@ snapshots: - ws - zod - '@earendil-works/pi-tui@0.80.10': + '@earendil-works/pi-protocol@0.84.1': + dependencies: + typebox: 1.3.7 + + '@earendil-works/pi-telemetry@0.84.1': {} + + '@earendil-works/pi-tui@0.84.1': dependencies: get-east-asian-width: 1.6.0 marked: 18.0.5 @@ -1807,10 +1963,74 @@ snapshots: dependencies: undici-types: 8.3.0 + '@types/node@26.2.0': + dependencies: + undici-types: 8.3.0 + '@types/retry@0.12.0': {} '@types/turndown@5.0.6': {} + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 @@ -1820,13 +2040,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.0(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.1.0(@types/node@26.2.0)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.0(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.0(@types/node@26.2.0)(jiti@2.7.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -2017,6 +2237,8 @@ snapshots: graceful-fs@4.2.11: {} + grok-mermaid@0.2.2: {} + highlight.js@10.7.3: {} hosted-git-info@9.0.3: @@ -2278,21 +2500,42 @@ snapshots: dependencies: '@mixmark-io/domino': 2.2.0 - typebox@1.1.38: {} + typebox@1.3.13: {} - typebox@1.3.6: {} + typebox@1.3.7: {} - typescript@6.0.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 uhyphen@0.2.0: {} undici-types@8.3.0: {} - undici@8.5.0: {} + undici@8.9.0: {} - unpdf@1.6.2: {} + unpdf@1.8.0: {} - vite@8.1.0(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0): + vite@8.1.0(@types/node@26.2.0)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -2300,15 +2543,15 @@ snapshots: rolldown: 1.1.4 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.1.1 + '@types/node': 26.2.0 fsevents: 2.3.3 jiti: 2.7.0 yaml: 2.9.0 - vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(vite@8.1.0(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.2.0)(vite@8.1.0(@types/node@26.2.0)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.0(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.1.0(@types/node@26.2.0)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -2325,11 +2568,11 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.0(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.0(@types/node@26.2.0)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 - '@types/node': 26.1.1 + '@types/node': 26.2.0 transitivePeerDependencies: - msw diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 95bdb6c..5dcd505 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,12 +2,4 @@ allowBuilds: "@google/genai": true protobufjs: true minimumReleaseAgeExclude: - - '@biomejs/biome@2.5.5' - - '@biomejs/cli-darwin-arm64@2.5.5' - - '@biomejs/cli-darwin-x64@2.5.5' - - '@biomejs/cli-linux-arm64-musl@2.5.5' - - '@biomejs/cli-linux-arm64@2.5.5' - - '@biomejs/cli-linux-x64-musl@2.5.5' - - '@biomejs/cli-linux-x64@2.5.5' - - '@biomejs/cli-win32-arm64@2.5.5' - - '@biomejs/cli-win32-x64@2.5.5' + - typebox@1.3.13 From 50750eb7a2803aee21a8ca5253dfd4f6a290e166 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Wed, 12 Aug 2026 15:08:30 -0400 Subject: [PATCH 2/7] chore: upgrade biome schema version to 2.5.8 --- biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biome.json b/biome.json index dc4ef09..c3a860d 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.5/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.8/schema.json", "vcs": { "enabled": true, "clientKind": "git", From 6ffad6d9c486c5528be4e8521fc7fc383acffd24 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Wed, 12 Aug 2026 15:44:51 -0400 Subject: [PATCH 3/7] docs: add implementation plan for TinyFish Search and Fetch provider --- .../plans/2026-08-12-tinyfish-provider.md | 457 ++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-12-tinyfish-provider.md diff --git a/docs/superpowers/plans/2026-08-12-tinyfish-provider.md b/docs/superpowers/plans/2026-08-12-tinyfish-provider.md new file mode 100644 index 0000000..e8a587c --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-tinyfish-provider.md @@ -0,0 +1,457 @@ +# TinyFish Search + Fetch Provider 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 TinyFish as a tier-2 provider for the existing `web_search` and `web_fetch` tools. + +**Architecture:** Add one dedicated `TinyFishProvider` using Node's native `fetch` and TinyFish's canonical Search and Fetch REST endpoints. Register it through pi-tools' existing `ProviderMeta`/`ProviderRegistry` seams so fallback, fusion, budgets, dashboard discovery, credentials, and automatic selection require no new infrastructure. This is not a Pi `ExtensionAPI.registerProvider()` model-provider integration; Pi continues to load the unchanged pi-tools extension and its existing `web_search`/`web_fetch` tools. + +**Tech Stack:** TypeScript, native `fetch`, Vitest, Biome, existing Pi Tools provider/configuration APIs. + +--- + +## File map + +- Create `src/providers/tinyfish.ts`: TinyFish Search/Fetch HTTP client and `ProviderMeta`. +- Create `tests/providers/tinyfish.test.ts`: mocked TinyFish request/response contract tests. +- Modify `src/providers/all.ts`: include TinyFish in the provider registry barrel. +- Modify `src/config.ts`: add the default provider entry and `TINYFISH_API_KEY` fallback mapping. +- Modify `tests/providers/all.test.ts`: update provider count/name expectations. +- Modify `tests/config.test.ts`: update built-in budget and environment mapping expectations. +- Modify `tests/config-manager.test.ts`: verify the resolved TinyFish key registers both capabilities through the real metadata path. +- Modify `README.md`: document TinyFish setup, capabilities, and configuration. +- Modify `CHANGELOG.md`: add the Unreleased provider entry. + +## Decisions locked by the design + +- V1 supports TinyFish Search and Fetch only; Agent and Browser APIs are out of scope. +- Provider name is `tinyfish`; display label is `TinyFish`. +- Tier is `2`, so configured TinyFish participates in automatic selection after tier-1 providers. +- API key is required and resolves through `TINYFISH_API_KEY`. +- Built-in budget is `{ mode: "unlimited" }`; TinyFish documents Search and Fetch as non-credit APIs, while upstream rate-limit errors remain ordinary provider failures. +- Search uses the documented first page and slices results locally to `maxResults`; no pagination changes are introduced. +- Existing single-URL `FetchProvider.fetch()` is retained even though TinyFish supports batch Fetch. +- No SDK, dependency, endpoint override, retry layer, new tool, generic adapter change, or Pi core change. + +### Task 1: Add failing TinyFish provider contract tests + +**Files:** + +- Create: `tests/providers/tinyfish.test.ts` + +- [ ] **Step 1: Add metadata and Search request tests** + +Use the existing `stubFetch` helper and assert the exact public contract: + +```ts +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + TinyFishProvider, + providerMeta, +} from "../../src/providers/tinyfish.ts"; +import { stubFetch } from "../helpers.ts"; + +describe("TinyFishProvider", () => { + let fetchStub: ReturnType; + + beforeEach(() => { + fetchStub = stubFetch(); + }); + + afterEach(() => { + fetchStub.restore(); + }); + + it("has the expected metadata", () => { + const provider = new TinyFishProvider("key"); + expect(provider.name).toBe("tinyfish"); + expect(provider.label).toBe("TinyFish"); + expect(providerMeta).toMatchObject({ + name: "tinyfish", + tier: 2, + requiresKey: true, + }); + expect(providerMeta.create("key").search).toBeDefined(); + expect(providerMeta.create("key").fetch).toBeDefined(); + }); + + it("sends the API key and maps Search filters", async () => { + fetchStub.addResponse("api.search.tinyfish.ai", { + body: { results: [] }, + }); + + await new TinyFishProvider("tiny-key").search("pi tools", 5, undefined, { + includeDomains: ["github.com", "docs.example.com"], + excludeDomains: ["spam.example.com"], + startDate: "2026-01-01", + endDate: "2026-08-12", + }); + + const [input, init] = (globalThis.fetch as any).mock.calls[0]; + const url = new URL(input as string); + expect(url.origin + url.pathname).toBe("https://api.search.tinyfish.ai/"); + expect(url.searchParams.get("query")).toBe("pi tools"); + expect(url.searchParams.get("include_domains")).toBe( + "github.com,docs.example.com", + ); + expect(url.searchParams.get("exclude_domains")).toBe("spam.example.com"); + expect(url.searchParams.get("after_date")).toBe("2026-01-01"); + expect(url.searchParams.get("before_date")).toBe("2026-08-12"); + expect(init.headers["X-API-Key"]).toBe("tiny-key"); + expect(init.headers.Accept).toBe("application/json"); + }); +}); +``` + +- [ ] **Step 2: Add Search normalization and failure tests** + +Add tests for the documented `{ results: [{ title, snippet, url }] }` response: + +- return normalized `SearchResult` values; +- discard non-HTTP URLs and cap output to `maxResults`; +- throw an error containing `TinyFish search error` for a 429 response. + +Use a fixture with two valid results and one `javascript:` result, call `search("test", 1)`, and assert exactly one valid result is returned. + +- [ ] **Step 3: Add Fetch request, response, and partial-error tests** + +Add tests asserting: + +```ts +fetchStub.addResponse("api.fetch.tinyfish.ai", { + body: { + results: [ + { + url: "https://example.com", + title: "Example", + text: "# Example\n\nFetched markdown", + format: "markdown", + }, + ], + errors: [], + }, +}); + +const result = await new TinyFishProvider("tiny-key").fetch( + "https://example.com", +); +expect(result).toEqual({ + text: "# Example\n\nFetched markdown", + title: "Example", + contentType: "text/markdown", +}); + +const [input, init] = (globalThis.fetch as any).mock.calls[0]; +expect(input).toBe("https://api.fetch.tinyfish.ai"); +expect(init.method).toBe("POST"); +expect(JSON.parse(init.body)).toEqual({ + urls: ["https://example.com"], + format: "markdown", +}); +expect(init.headers["X-API-Key"]).toBe("tiny-key"); +``` + +Also test that a response with no matching result and an `errors` entry throws an error containing that URL/error, and that a non-2xx response throws `TinyFish fetch error`. + +- [ ] **Step 4: Add abort-signal forwarding test** + +Call `search` with an `AbortController.signal`, inspect the mocked fetch call, and assert `init.signal` is the same signal object. + +- [ ] **Step 5: Run the focused tests and confirm they fail** + +Run: + +```bash +pnpm exec vitest run tests/providers/tinyfish.test.ts +``` + +Expected: collection/import failure because `src/providers/tinyfish.ts` does not yet exist. + +### Task 2: Implement the native TinyFish provider + +**Files:** + +- Create: `src/providers/tinyfish.ts` +- Test: `tests/providers/tinyfish.test.ts` + +- [ ] **Step 1: Implement response guards and URL normalization** + +Add local helpers without changing shared provider types: + +```ts +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function validHttpUrl(value: unknown): value is string { + if (typeof value !== "string") return false; + try { + const url = new URL(value); + return url.protocol === "http:" || url.protocol === "https:"; + } catch { + return false; + } +} +``` + +Use these helpers when converting unknown JSON into local provider results. Do not use `any` in production code. + +- [ ] **Step 2: Implement Search with native fetch** + +Build the request against `https://api.search.tinyfish.ai` using `URL`/`URLSearchParams`: + +```ts +const url = new URL("https://api.search.tinyfish.ai"); +url.searchParams.set("query", query); +if (filters?.includeDomains?.length) { + url.searchParams.set("include_domains", filters.includeDomains.join(",")); +} +if (filters?.excludeDomains?.length) { + url.searchParams.set("exclude_domains", filters.excludeDomains.join(",")); +} +if (filters?.startDate) url.searchParams.set("after_date", filters.startDate); +if (filters?.endDate) url.searchParams.set("before_date", filters.endDate); + +const response = await fetch(url, { + headers: { Accept: "application/json", "X-API-Key": this.apiKey }, + signal, +}); +if (!response.ok) { + throw new Error( + `TinyFish search error: ${response.status} ${response.statusText}`, + ); +} +``` + +Parse `results` only when it is an array. Map string title/snippet values to empty strings when absent, discard records whose URL is not HTTP(S), and return `.slice(0, maxResults)`. + +- [ ] **Step 3: Implement Fetch with native fetch** + +Send the documented Markdown request: + +```ts +const response = await fetch("https://api.fetch.tinyfish.ai", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-API-Key": this.apiKey, + }, + body: JSON.stringify({ urls: [url], format: "markdown" }), + signal, +}); +if (!response.ok) { + throw new Error( + `TinyFish fetch error: ${response.status} ${response.statusText}`, + ); +} +``` + +For the existing single-URL interface, use the first usable result. TinyFish documents `results[].url` as the original requested URL and `final_url` as the post-redirect URL, so no redirect matching or URL rewriting is needed in the local `FetchResult`. Return its string `text`, optional string `title`, and `contentType: "text/markdown"`. + +If there is no usable result, inspect `errors` for `errors[].url === url` and throw `TinyFish fetch error: `; otherwise throw `TinyFish fetch error: no result for `. Preserve the caller's signal and do not retry. + +- [ ] **Step 4: Add provider metadata** + +Export: + +```ts +export const providerMeta: ProviderMeta = { + name: "tinyfish", + tier: 2, + requiresKey: true, + create: (key) => { + const provider = new TinyFishProvider(key!); + return { search: provider, fetch: provider }; + }, +}; +``` + +- [ ] **Step 5: Run focused tests and typecheck** + +Run: + +```bash +pnpm exec vitest run tests/providers/tinyfish.test.ts +pnpm exec tsc --noEmit +``` + +Expected: both commands pass. + +### Task 3: Register TinyFish and add configuration defaults + +**Files:** + +- Modify: `src/providers/all.ts` +- Modify: `src/config.ts` +- Modify: `tests/providers/all.test.ts` +- Modify: `tests/config.test.ts` +- Modify: `tests/config-manager.test.ts` + +- [ ] **Step 1: Register the provider barrel entry** + +Add: + +```ts +import { providerMeta as tinyfish } from "./tinyfish.ts"; +``` + +and append `tinyfish` to `allProviders`. + +- [ ] **Step 2: Add credential fallback mapping** + +Add to `FALLBACK_ENV_MAP`: + +```ts +tinyfish: "TINYFISH_API_KEY", +``` + +This preserves the existing `resolveProviderKey()` fallback path. `ConfigManager` registers the provider from `DEFAULT_CONFIG.providers.tinyfish.apiKey` and resolves that configured environment-variable name through `resolveApiKey()`; the fallback map is kept consistent for callers that use the provider-name lookup path. + +- [ ] **Step 3: Add built-in provider defaults** + +Add to `DEFAULT_CONFIG.providers`: + +```ts +tinyfish: { + enabled: true, + budget: { mode: "unlimited" }, + apiKey: "TINYFISH_API_KEY", +}, +``` + +- [ ] **Step 4: Update provider barrel tests** + +Update `tests/providers/all.test.ts` to: + +- change the expected provider count from `22` to `23`; +- add `tinyfish` to the expected provider names; +- keep the custom-cost provider list unchanged because TinyFish uses the default unit cost behavior. + +- [ ] **Step 5: Update config tests** + +Update `tests/config.test.ts` to: + +- expect `tinyfish: { mode: "unlimited" }` in the built-in budget map; +- add `tinyfish` to the expected fallback provider list; +- assert `FALLBACK_ENV_MAP.tinyfish === "TINYFISH_API_KEY"`. + +- [ ] **Step 6: Cover ConfigManager registration with the real TinyFish metadata** + +Extend `tests/config-manager.test.ts` with a focused test that: + +- loads a config containing an enabled `tinyfish` entry whose `apiKey` is `TINYFISH_API_KEY`; +- stubs `resolveApiKey("TINYFISH_API_KEY")` to return a test key; +- constructs `ConfigManager` with the real TinyFish `providerMeta` and a fresh `ProviderRegistry`; +- asserts `registerProvider` receives the TinyFish name, tier-2 policy, and instances containing both `search` and `fetch`. + +This verifies the runtime registration path used during Pi session startup without calling TinyFish. + +- [ ] **Step 7: Run registration/config tests** + +Run: + +```bash +pnpm exec vitest run tests/providers/all.test.ts tests/config.test.ts tests/config-manager.test.ts tests/providers/tinyfish.test.ts +``` + +Expected: all tests pass. + +### Task 4: Document TinyFish setup and release notes + +**Files:** + +- Modify: `README.md` +- Modify: `CHANGELOG.md` + +- [ ] **Step 1: Add the provider capability row** + +Add to the README provider table: + +```markdown +| TinyFish | Search, fetch | `TINYFISH_API_KEY` | +``` + +- [ ] **Step 2: Add TinyFish to the configuration example** + +Add to the full configuration reference: + +```json +"tinyfish": { + "enabled": true, + "apiKey": "TINYFISH_API_KEY" +} +``` + +Add a short setup note near the provider documentation explaining that TinyFish uses the Search and Fetch REST APIs and requires an API key. + +- [ ] **Step 3: Add the Unreleased changelog entry** + +Under `## [Unreleased]`, add: + +```markdown +### Added + +- Added TinyFish Search and Fetch provider support through `TINYFISH_API_KEY`. +``` + +If an `### Added` subsection already exists, append to it instead of duplicating it. + +- [ ] **Step 4: Check documentation whitespace** + +Run: + +```bash +git diff --check +``` + +Expected: no whitespace errors. Markdown is intentionally excluded from this repository's Biome configuration; TypeScript remains covered by `pnpm check`. + +### Task 5: Run full verification and self-review + +**Files:** + +- Verify all files changed by Tasks 1–4. + +- [ ] **Step 1: Run the complete project check** + +Run: + +```bash +pnpm check +``` + +Expected: Biome lint, TypeScript, and all Vitest tests pass. + +- [ ] **Step 2: Verify the package contents without publishing** + +Run: + +```bash +pnpm pack:dry-run +``` + +Expected: the package includes `src/providers/tinyfish.ts`, tests are excluded according to the existing package configuration, and no new dependency is listed. + +- [ ] **Step 3: Review the final diff** + +Run: + +```bash +git diff --check +git status --short +git diff -- src/providers/tinyfish.ts src/providers/all.ts src/config.ts tests/providers/tinyfish.test.ts tests/providers/all.test.ts tests/config.test.ts tests/config-manager.test.ts README.md CHANGELOG.md +``` + +Confirm that only the planned provider, tests, configuration, and documentation files changed; there are no lockfile or dependency changes. + +- [ ] **Step 4: Complete the implementation handoff** + +Report the passing commands and note that live TinyFish API verification was not required. Do not claim live connectivity or upstream availability based solely on mocked tests. + +## Self-review result + +- Spec coverage: Search, Fetch, authentication, tier, budget, config, fallback, ConfigManager registration, normalization, errors, cancellation, tests, docs, and verification are each assigned to a task. +- Placeholder scan: no unfinished markers or unspecified implementation steps remain. +- Type consistency: `TinyFishProvider` implements the existing `SearchProvider` and `FetchProvider` methods; `providerMeta.create()` returns the capabilities expected by `ProviderMeta`. +- Scope check: one provider integration only; TinyFish Agent/Browser work is explicitly excluded. From 6abf6dc8e5f5c171d7445ebdd683091b613cd64e Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Wed, 12 Aug 2026 18:16:00 -0400 Subject: [PATCH 4/7] feat(providers): add TinyFish Search and Fetch provider --- src/config.ts | 6 + src/providers/all.ts | 2 + src/providers/tinyfish.ts | 122 ++++++++++++++++++ tests/config-manager.test.ts | 27 ++++ tests/config.test.ts | 2 + tests/providers/all.test.ts | 5 +- tests/providers/tinyfish.test.ts | 204 +++++++++++++++++++++++++++++++ 7 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 src/providers/tinyfish.ts create mode 100644 tests/providers/tinyfish.test.ts diff --git a/src/config.ts b/src/config.ts index 556e85f..5d15313 100644 --- a/src/config.ts +++ b/src/config.ts @@ -138,6 +138,7 @@ export const FALLBACK_ENV_MAP: Record = { marginalia: "MARGINALIA_API_KEY", context7: "CONTEXT7_API_KEY", parallel: "PARALLEL_API_KEY", + tinyfish: "TINYFISH_API_KEY", ollama: "OLLAMA_API_KEY", "openai-web-search": "OPENAI_API_KEY", gemini: "GEMINI_API_KEY", @@ -267,6 +268,11 @@ const DEFAULT_CONFIG: PiToolsConfig = { budget: { mode: "hard", limit: 1000, period: "month", unit: "credit" }, apiKey: "TAVILY_API_KEY", }, + tinyfish: { + enabled: true, + budget: { mode: "unlimited" }, + apiKey: "TINYFISH_API_KEY", + }, websearchapi: { enabled: false, budget: { mode: "hard", limit: 2000, period: "month", unit: "credit" }, diff --git a/src/providers/all.ts b/src/providers/all.ts index 7a1f5f5..dcc56ae 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -13,6 +13,7 @@ import { providerMeta as searxng } from "./searxng.ts"; import { providerMeta as serper } from "./serper.ts"; import { providerMeta as sofya } from "./sofya.ts"; import { providerMeta as tavily } from "./tavily.ts"; +import { providerMeta as tinyfish } from "./tinyfish.ts"; export const allProviders: ProviderMeta[] = [ ...httpProviders, @@ -29,4 +30,5 @@ export const allProviders: ProviderMeta[] = [ serper, sofya, tavily, + tinyfish, ]; diff --git a/src/providers/tinyfish.ts b/src/providers/tinyfish.ts new file mode 100644 index 0000000..931dfdb --- /dev/null +++ b/src/providers/tinyfish.ts @@ -0,0 +1,122 @@ +import type { + FetchProvider, + ProviderMeta, + SearchFilters, + SearchProvider, + SearchResult, +} from "./types.ts"; + +const SEARCH_ENDPOINT = "https://api.search.tinyfish.ai/"; +const FETCH_ENDPOINT = "https://api.fetch.tinyfish.ai"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function validHttpUrl(value: unknown): value is string { + if (typeof value !== "string") return false; + try { + const url = new URL(value); + return url.protocol === "http:" || url.protocol === "https:"; + } catch { + return false; + } +} + +export class TinyFishProvider implements SearchProvider, FetchProvider { + readonly name = "tinyfish"; + readonly label = "TinyFish"; + private readonly apiKey: string; + + constructor(apiKey: string) { + this.apiKey = apiKey; + } + + async search( + query: string, + maxResults: number, + signal?: AbortSignal, + filters?: SearchFilters, + ): Promise { + const url = new URL(SEARCH_ENDPOINT); + url.searchParams.set("query", query); + if (filters?.includeDomains?.length) { + url.searchParams.set("include_domains", filters.includeDomains.join(",")); + } + if (filters?.excludeDomains?.length) { + url.searchParams.set("exclude_domains", filters.excludeDomains.join(",")); + } + if (filters?.startDate) url.searchParams.set("after_date", filters.startDate); + if (filters?.endDate) url.searchParams.set("before_date", filters.endDate); + + const response = await fetch(url, { + headers: { Accept: "application/json", "X-API-Key": this.apiKey }, + signal, + }); + if (!response.ok) { + throw new Error(`TinyFish search error: ${response.status} ${response.statusText}`); + } + + const data: unknown = await response.json(); + if (!isRecord(data) || !Array.isArray(data.results)) return []; + const results: SearchResult[] = []; + for (const entry of data.results) { + if (!isRecord(entry) || !validHttpUrl(entry.url)) continue; + results.push({ + title: typeof entry.title === "string" ? entry.title : "", + snippet: typeof entry.snippet === "string" ? entry.snippet : "", + url: entry.url, + }); + if (results.length >= maxResults) break; + } + return results; + } + + async fetch(url: string, signal?: AbortSignal) { + const response = await fetch(FETCH_ENDPOINT, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-API-Key": this.apiKey, + }, + body: JSON.stringify({ urls: [url], format: "markdown" }), + signal, + }); + if (!response.ok) { + throw new Error(`TinyFish fetch error: ${response.status} ${response.statusText}`); + } + + const data: unknown = await response.json(); + if (!isRecord(data)) throw new Error(`TinyFish fetch error: no result for ${url}`); + + const results = Array.isArray(data.results) ? data.results : []; + const match = results.find((entry) => isRecord(entry) && entry.url === url); + if (match && typeof match.text === "string") { + return { + text: match.text, + title: typeof match.title === "string" ? match.title : undefined, + contentType: "text/markdown", + }; + } + + const errors = Array.isArray(data.errors) ? data.errors : []; + const errorMatch = errors.find( + (entry) => isRecord(entry) && entry.url === url && typeof entry.error === "string", + ); + if (errorMatch) { + throw new Error(`TinyFish fetch error: ${errorMatch.error}`); + } + throw new Error(`TinyFish fetch error: no result for ${url}`); + } +} + +export const providerMeta: ProviderMeta = { + name: "tinyfish", + tier: 2, + requiresKey: true, + create: (key) => { + const provider = new TinyFishProvider(key!); + return { search: provider, fetch: provider }; + }, +}; diff --git a/tests/config-manager.test.ts b/tests/config-manager.test.ts index 848e500..e738923 100644 --- a/tests/config-manager.test.ts +++ b/tests/config-manager.test.ts @@ -15,6 +15,7 @@ vi.mock("../src/config.ts", async (importOriginal) => { }); import { loadMergedConfig, resolveApiKey } from "../src/config.ts"; +import { providerMeta as tinyfishMeta } from "../src/providers/tinyfish.ts"; const managed: ProviderBudget = { mode: "managed" }; const hard: ProviderBudget = { mode: "hard", limit: 5, period: "month", unit: "usd" }; @@ -195,4 +196,30 @@ describe("ConfigManager", () => { ]); expect(registry.getSearchProviderNames()).toEqual(["enabled"]); }); + + it("registers tinyfish with the real metadata, tier-2 policy, and both capabilities", () => { + vi.mocked(loadMergedConfig).mockReturnValue( + makeConfig({ + tinyfish: entry({ mode: "unlimited" }, { apiKey: "TINYFISH_API_KEY" }), + }), + ); + vi.mocked(resolveApiKey).mockImplementation((key) => + key === "TINYFISH_API_KEY" ? "resolved-tiny-key" : key, + ); + + const registry = memory(); + const register = vi.spyOn(registry, "registerProvider"); + + new ConfigManager("/cwd", registry, [tinyfishMeta]); + + expect(register).toHaveBeenCalledOnce(); + const [instances, options] = register.mock.calls[0]; + expect(options.name).toBe("tinyfish"); + expect(options.tier).toBe(2); + expect(options.budget).toEqual({ mode: "unlimited" }); + expect(instances.search).toBeDefined(); + expect(instances.fetch).toBeDefined(); + expect(registry.getSearchProviderNames()).toEqual(["tinyfish"]); + expect(registry.getBudgetStatus("tinyfish")).toEqual({ mode: "unlimited" }); + }); }); diff --git a/tests/config.test.ts b/tests/config.test.ts index 07d0179..c6a718d 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -69,6 +69,7 @@ describe("loadConfig", () => { duckduckgo: { mode: "unlimited" }, ollama: { mode: "unlimited" }, searxng: { mode: "unlimited" }, + tinyfish: { mode: "unlimited" }, jina: { mode: "managed" }, marginalia: { mode: "managed" }, "openai-codex": { mode: "managed" }, @@ -861,6 +862,7 @@ describe("FALLBACK_ENV_MAP", () => { "marginalia", "context7", "parallel", + "tinyfish", ]; for (const name of expected) { expect(FALLBACK_ENV_MAP[name]).toBeDefined(); diff --git a/tests/providers/all.test.ts b/tests/providers/all.test.ts index 503fce5..bdd8d78 100644 --- a/tests/providers/all.test.ts +++ b/tests/providers/all.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest"; import { allProviders } from "../../src/providers/all.ts"; describe("allProviders barrel", () => { - it("exports exactly 22 providers", () => { - expect(allProviders).toHaveLength(22); + it("exports exactly 23 providers", () => { + expect(allProviders).toHaveLength(23); }); it("every entry has required ProviderMeta fields", () => { @@ -86,6 +86,7 @@ describe("allProviders barrel", () => { "serper", "sofya", "tavily", + "tinyfish", "websearchapi", "youcom", ]); diff --git a/tests/providers/tinyfish.test.ts b/tests/providers/tinyfish.test.ts new file mode 100644 index 0000000..c02f898 --- /dev/null +++ b/tests/providers/tinyfish.test.ts @@ -0,0 +1,204 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { providerMeta, TinyFishProvider } from "../../src/providers/tinyfish.ts"; +import { stubFetch } from "../helpers.ts"; + +describe("TinyFishProvider", () => { + let fetchStub: ReturnType; + + beforeEach(() => { + fetchStub = stubFetch(); + }); + + afterEach(() => { + fetchStub.restore(); + }); + + it("has the expected metadata", () => { + const provider = new TinyFishProvider("key"); + expect(provider.name).toBe("tinyfish"); + expect(provider.label).toBe("TinyFish"); + expect(providerMeta).toMatchObject({ + name: "tinyfish", + tier: 2, + requiresKey: true, + }); + expect(providerMeta.create("key")).toMatchObject({ + search: expect.anything(), + fetch: expect.anything(), + }); + }); + + it("sends the API key and maps Search filters", async () => { + fetchStub.addResponse("api.search.tinyfish.ai", { + body: { results: [] }, + }); + + await new TinyFishProvider("tiny-key").search("pi tools", 5, undefined, { + includeDomains: ["github.com", "docs.example.com"], + excludeDomains: ["spam.example.com"], + startDate: "2026-01-01", + endDate: "2026-08-12", + }); + + const [input, init] = (globalThis.fetch as any).mock.calls[0]; + const url = new URL(input as string); + expect(url.origin + url.pathname).toBe("https://api.search.tinyfish.ai/"); + expect(url.searchParams.get("query")).toBe("pi tools"); + expect(url.searchParams.get("include_domains")).toBe("github.com,docs.example.com"); + expect(url.searchParams.get("exclude_domains")).toBe("spam.example.com"); + expect(url.searchParams.get("after_date")).toBe("2026-01-01"); + expect(url.searchParams.get("before_date")).toBe("2026-08-12"); + expect(init.headers["X-API-Key"]).toBe("tiny-key"); + expect(init.headers.Accept).toBe("application/json"); + }); + + it("normalizes Search results and drops non-HTTP URLs", async () => { + fetchStub.addResponse("api.search.tinyfish.ai", { + body: { + results: [ + { + title: "First", + snippet: "snippet one", + url: "https://example.com/one", + }, + { + title: "Second", + snippet: "snippet two", + url: "javascript:alert(1)", + }, + ], + }, + }); + + const results = await new TinyFishProvider("tiny-key").search("test", 5); + expect(results).toEqual([ + { + title: "First", + snippet: "snippet one", + url: "https://example.com/one", + }, + ]); + }); + + it("caps Search results to maxResults", async () => { + fetchStub.addResponse("api.search.tinyfish.ai", { + body: { + results: [ + { title: "A", snippet: "a", url: "https://a.example" }, + { title: "B", snippet: "b", url: "https://b.example" }, + { title: "C", snippet: "c", url: "https://c.example" }, + ], + }, + }); + + const results = await new TinyFishProvider("tiny-key").search("test", 1); + expect(results).toHaveLength(1); + expect(results[0].url).toBe("https://a.example"); + }); + + it("uses empty strings for missing title/snippet in Search results", async () => { + fetchStub.addResponse("api.search.tinyfish.ai", { + body: { + results: [{ url: "https://example.com/x" }], + }, + }); + + const results = await new TinyFishProvider("tiny-key").search("test", 5); + expect(results).toEqual([{ title: "", snippet: "", url: "https://example.com/x" }]); + }); + + it("throws on Search error response", async () => { + fetchStub.addResponse("api.search.tinyfish.ai", { + status: 429, + body: { error: "rate limited" }, + }); + + await expect(new TinyFishProvider("tiny-key").search("test", 5)).rejects.toThrow( + /TinyFish search error/, + ); + }); + + it("sends Fetch POST request with markdown format", async () => { + fetchStub.addResponse("api.fetch.tinyfish.ai", { + body: { + results: [ + { + url: "https://example.com", + title: "Example", + text: "# Example\n\nFetched markdown", + format: "markdown", + }, + ], + errors: [], + }, + }); + + const controller = new AbortController(); + const result = await new TinyFishProvider("tiny-key").fetch( + "https://example.com", + controller.signal, + ); + expect(result).toEqual({ + text: "# Example\n\nFetched markdown", + title: "Example", + contentType: "text/markdown", + }); + + const [input, init] = (globalThis.fetch as any).mock.calls[0]; + expect(input).toBe("https://api.fetch.tinyfish.ai"); + expect(init.method).toBe("POST"); + expect(JSON.parse(init.body)).toEqual({ + urls: ["https://example.com"], + format: "markdown", + }); + expect(init.headers["X-API-Key"]).toBe("tiny-key"); + expect(init.headers["Content-Type"]).toBe("application/json"); + expect(init.signal).toBe(controller.signal); + }); + + it("throws Fetch error when result is missing but errors entry matches URL", async () => { + fetchStub.addResponse("api.fetch.tinyfish.ai", { + body: { + results: [], + errors: [{ url: "https://example.com", error: "timeout" }], + }, + }); + + await expect(new TinyFishProvider("tiny-key").fetch("https://example.com")).rejects.toThrow( + /TinyFish fetch error.*timeout/, + ); + }); + + it("throws Fetch error when no result and no matching errors entry", async () => { + fetchStub.addResponse("api.fetch.tinyfish.ai", { + body: { results: [], errors: [] }, + }); + + await expect(new TinyFishProvider("tiny-key").fetch("https://example.com")).rejects.toThrow( + /TinyFish fetch error: no result for https:\/\/example\.com/, + ); + }); + + it("throws on Fetch non-2xx response", async () => { + fetchStub.addResponse("api.fetch.tinyfish.ai", { + status: 500, + body: { error: "boom" }, + }); + + await expect(new TinyFishProvider("tiny-key").fetch("https://example.com")).rejects.toThrow( + /TinyFish fetch error/, + ); + }); + + it("forwards AbortSignal to fetch", async () => { + fetchStub.addResponse("api.search.tinyfish.ai", { + body: { results: [] }, + }); + + const controller = new AbortController(); + await new TinyFishProvider("tiny-key").search("test", 5, controller.signal); + + const [, init] = (globalThis.fetch as any).mock.calls[0]; + expect(init.signal).toBe(controller.signal); + }); +}); From 142a8ca1bda04d3cafd26a348599ded2667e12a9 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Wed, 12 Aug 2026 18:16:18 -0400 Subject: [PATCH 5/7] feat: add support for TinyFish search and fetch provider --- CHANGELOG.md | 4 ++++ README.md | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc429c5..36bf98f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added TinyFish Search and Fetch provider support through `TINYFISH_API_KEY`. + ## [0.5.0] - 2026-07-21 ### Added diff --git a/README.md b/README.md index 7a28a8e..2715fc8 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ Provider configuration refreshes automatically every 30 seconds. The dashboard's | Serper | Search | `SERPER_API_KEY` | | Sofya | Search, fetch | `SOFYA_API_KEY` | | Tavily | Search, fetch | `TAVILY_API_KEY` | +| TinyFish | Search, fetch | `TINYFISH_API_KEY` | | WebSearchAPI | Search | `WEBSEARCHAPI_API_KEY` | | You.com | Search | `YOUCOM_API_KEY` | @@ -276,6 +277,10 @@ The complete shape of the merged configuration. Every field shown here is honore "enabled": false, "apiKey": "TAVILY_API_KEY" }, + "tinyfish": { + "enabled": true, + "apiKey": "TINYFISH_API_KEY" + }, "websearchapi": { "enabled": false, "apiKey": "WEBSEARCHAPI_API_KEY" From 230f2d9b9e963e038c523ec3262cd0210c3c1207 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Wed, 12 Aug 2026 18:21:16 -0400 Subject: [PATCH 6/7] feat: return empty search results for non-positive maxResults in TinyFish provider --- src/providers/tinyfish.ts | 2 ++ tests/providers/tinyfish.test.ts | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/providers/tinyfish.ts b/src/providers/tinyfish.ts index 931dfdb..7349d9e 100644 --- a/src/providers/tinyfish.ts +++ b/src/providers/tinyfish.ts @@ -38,6 +38,8 @@ export class TinyFishProvider implements SearchProvider, FetchProvider { signal?: AbortSignal, filters?: SearchFilters, ): Promise { + if (maxResults <= 0) return []; + const url = new URL(SEARCH_ENDPOINT); url.searchParams.set("query", query); if (filters?.includeDomains?.length) { diff --git a/tests/providers/tinyfish.test.ts b/tests/providers/tinyfish.test.ts index c02f898..684e837 100644 --- a/tests/providers/tinyfish.test.ts +++ b/tests/providers/tinyfish.test.ts @@ -96,6 +96,15 @@ describe("TinyFishProvider", () => { expect(results[0].url).toBe("https://a.example"); }); + it("returns no Search results for non-positive maxResults", async () => { + fetchStub.addResponse("api.search.tinyfish.ai", { + body: { results: [{ title: "A", snippet: "a", url: "https://a.example" }] }, + }); + + await expect(new TinyFishProvider("tiny-key").search("test", 0)).resolves.toEqual([]); + await expect(new TinyFishProvider("tiny-key").search("test", -1)).resolves.toEqual([]); + }); + it("uses empty strings for missing title/snippet in Search results", async () => { fetchStub.addResponse("api.search.tinyfish.ai", { body: { From 73222e7bf38b25d9cffb60d1bcadfd3a5a5c5b49 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Wed, 12 Aug 2026 18:56:31 -0400 Subject: [PATCH 7/7] refactor: simplify TinyFishProvider constructor using parameter property shorthand --- src/providers/tinyfish.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/providers/tinyfish.ts b/src/providers/tinyfish.ts index 7349d9e..7a62496 100644 --- a/src/providers/tinyfish.ts +++ b/src/providers/tinyfish.ts @@ -26,11 +26,8 @@ function validHttpUrl(value: unknown): value is string { export class TinyFishProvider implements SearchProvider, FetchProvider { readonly name = "tinyfish"; readonly label = "TinyFish"; - private readonly apiKey: string; - constructor(apiKey: string) { - this.apiKey = apiKey; - } + constructor(private readonly apiKey: string) {} async search( query: string,