From af1df1f675bb99e6f8e2daf9f2d654dc9414aad8 Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 15:35:59 +0300 Subject: [PATCH 01/12] chore(deps): upgrade incur to 0.4.8, drop @remix-run/fs, require node >=22 incur 0.4.x requires Node >=22; also removes the unused @remix-run/fs dependency. --- cli/bun.lock | 213 ++--------------------------------------------- cli/package.json | 5 +- 2 files changed, 8 insertions(+), 210 deletions(-) diff --git a/cli/bun.lock b/cli/bun.lock index d6065d6..d61edd2 100644 --- a/cli/bun.lock +++ b/cli/bun.lock @@ -8,9 +8,8 @@ "@clack/prompts": "^1.0.0", "@filoz/synapse-core": "^0.7.0", "@filoz/synapse-sdk": "^1.0.1", - "@remix-run/fs": "^0.4.1", "conf": "^15.0.2", - "incur": "^0.3.1", + "incur": "^0.4.8", "terminal-link": "^5.0.0", "viem": "^2.47.1", }, @@ -24,14 +23,6 @@ "packages": { "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], - "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@14.2.1", "", { "dependencies": { "js-yaml": "^4.1.0" }, "peerDependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg=="], - - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], - "@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], @@ -50,6 +41,8 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="], + "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], + "@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="], "@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="], @@ -58,11 +51,7 @@ "@filoz/synapse-sdk": ["@filoz/synapse-sdk@1.0.1", "", { "dependencies": { "@filoz/synapse-core": "^0.7.0", "multiformats": "^14.0.0" }, "peerDependencies": { "viem": "2.x" } }, "sha512-zUAFPVPit9CNcOfjzv4oH+zqj0FkyVHJkXbeWpuJo4ZQxlCTieILpgEn7zR5LFnGKBpukzM0iIzW1mOEqGZ6jA=="], - "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], - - "@humanwhocodes/momoa": ["@humanwhocodes/momoa@2.0.4", "", {}, "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], + "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0-alpha.2", "", { "dependencies": { "zod": "^4.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmLgdHzlYM8L7Aw/+VE0kxjT25WKamtUSLNhdOgrJq5CrESvqVSoAfWSJJeNPUXNTluQ+dYDGFbKVitdsJtbPA=="], "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], @@ -70,17 +59,7 @@ "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@readme/better-ajv-errors": ["@readme/better-ajv-errors@2.4.0", "", { "dependencies": { "@babel/code-frame": "^7.22.5", "@babel/runtime": "^7.22.5", "@humanwhocodes/momoa": "^2.0.3", "jsonpointer": "^5.0.0", "leven": "^3.1.0", "picocolors": "^1.1.1" }, "peerDependencies": { "ajv": "4.11.8 - 8" } }, "sha512-9WODaOAKSl/mU+MYNZ2aHCrkoRSvmQ+1YkLj589OEqqjOAhbn8j7Z+ilYoiTu/he6X63/clsxxAB4qny9/dDzg=="], - - "@readme/openapi-parser": ["@readme/openapi-parser@6.0.0", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^14.1.1", "@readme/better-ajv-errors": "^2.3.2", "@readme/openapi-schemas": "^3.1.0", "@types/json-schema": "^7.0.15", "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0" }, "peerDependencies": { "openapi-types": ">=7" } }, "sha512-PaTnrKlKgEJZzjJ77AAhGe28NiyLBdiKMx95rJ9xlLZ8QLqYitMpPBQAKhsuEGOWQQbsIMfBZEPavbXghACQHA=="], - - "@readme/openapi-schemas": ["@readme/openapi-schemas@3.1.0", "", {}, "sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw=="], - - "@remix-run/fs": ["@remix-run/fs@0.4.2", "", { "dependencies": { "@remix-run/lazy-file": "^5.0.2", "@remix-run/mime": "^0.4.0" } }, "sha512-z3W2L+iUwgZ7i0S379SYQ8veOe2Weqs+JajmyTCqSVzbmMUniH3qQ6SAYr3FjbrKtLLWHN3SpK4XtFv57VzbLA=="], - - "@remix-run/lazy-file": ["@remix-run/lazy-file@5.0.2", "", { "dependencies": { "@remix-run/mime": "^0.4.0" } }, "sha512-52Bo5dTV+EDwrUMS3mjeR+Sly85aHeN3fnNTeaflqzlCMWJwr2pX+y6/3mTDtRdxmTWF1MGQAoeayzfPb4zZJg=="], - - "@remix-run/mime": ["@remix-run/mime@0.4.0", "", {}, "sha512-O6TcTL6CtuX82Q8BHqAere5O+0hYcrzSgY9whsDOBuqbW753Rczprs2jYw3qCDSo0kLxykW4ys3qgZcdgZ+chw=="], + "@scalar/openapi-types": ["@scalar/openapi-types@0.8.0", "", {}, "sha512-WmaxVSfvY5K/TwcG2B2TU1WOe1As1uc2s7myswtP6dBlcjU3hM08SApxv/jmyGaCE8t4gO5BBhmHY4pDUfmr2g=="], "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], @@ -92,140 +71,50 @@ "@toon-format/toon": ["@toon-format/toon@2.1.0", "", {}, "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], "bigint-mod-arith": ["bigint-mod-arith@3.3.1", "", {}, "sha512-pX/cYW3dCa87Jrzv6DAr8ivbbJRzEX5yGhdt8IutnX/PCIXfpx+mabWNK/M8qqh+zQ0J3thftUBHW0ByuUlG0w=="], - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="], - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "delay": ["delay@7.0.0", "", { "dependencies": { "random-int": "^3.1.0", "unlimited-timeout": "^0.1.0" } }, "sha512-C3vaGs818qzZjCvVJ98GQUMVyWeg7dr5w2Nwwb2t5K8G98jOyyVO2ti2bKYk5yoYElqH3F2yA53ykuEnwD6MCg=="], - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "dnum": ["dnum@2.17.0", "", { "dependencies": { "from-exponential": "^1.1.1" } }, "sha512-Abo8RU2ZoABVO2R051XlJEgDIXAlA8/ZjOT2F1uAWvm6Vb8TphmN4k7qgu5nWKSv/JUGLVty6QPEeLTvaxNRYQ=="], "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "from-exponential": ["from-exponential@1.1.1", "", {}, "sha512-VBE7f5OVnYwdgB3LHa+Qo29h8qVpxhVO9Trlc+AWm+/XNAgks1tAwMFHb33mjeiof77GglsJzeYF7OqXrROP/A=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "hono": ["hono@4.12.7", "", {}, "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], - "incur": ["incur@0.3.1", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", "@readme/openapi-parser": "^6.0.0", "@toon-format/toon": "^2.1.0", "tokenx": "^1.3.0", "yaml": "^2.8.2", "zod": "^4.3.6" }, "bin": { "incur": "dist/bin.js", "incur.src": "src/bin.ts" } }, "sha512-Qbx+LI55wzHSTpSS8XcyzUDiHS2DinM8rSonw/Ri3t3KuPimwbbGUfoyPrIjxciBQxjAaL2Nkv0RKJIVp0b5iA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "incur": ["incur@0.4.8", "", { "dependencies": { "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/server": "^2.0.0-alpha.2", "@scalar/openapi-types": "^0.8.0", "@toon-format/toon": "^2.1.0", "tokenx": "^1.3.0", "yaml": "^2.8.2", "zod": "^4.3.6" }, "bin": { "incur": "dist/bin.js", "incur.src": "src/bin.ts" } }, "sha512-SjW2QNtY7Bcvqjj0KvOJ3qiuFATlC3mEpYQyUzWdLb9MAzakkQ1KQg5WZ/yy2tDGBmHLN/zUJNimIWEqkRfqdw=="], "is-network-error": ["is-network-error@1.3.1", "", {}, "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw=="], - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "iso-base": ["iso-base@4.4.0", "", { "dependencies": { "bigint-mod-arith": "^3.3.1" } }, "sha512-W4BJbRDBi66wcBFJ23ZlGnFOZ874CIut4y9PlsScMSYu7ckCX+MWNAaEoE0efTSYDoVEn1qy0FDVSjE8q0ieHw=="], "iso-kv": ["iso-kv@3.2.0", "", { "dependencies": { "conf": "^15.1.0", "idb-keyval": "^6.2.1", "iso-base": "^4.4.0", "kysely": "^0.29.2" } }, "sha512-rMVXO7zDEecXOJEiDnbDqjZ7gSe7VqDn2FTkUFWeqkcYMmgMdmB7pDgUaZxiEyiL1+gO7RnbqEI2Ce3xIW4UBQ=="], @@ -234,50 +123,16 @@ "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], - "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "kysely": ["kysely@0.29.2", "", {}, "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg=="], - "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "multiformats": ["multiformats@14.0.0", "", {}, "sha512-iWK1RrAS58p2NDfeZFuSUSv3ZPewTIhsGbh/5NgeGGJwJmRljLxGtjRR3nkn+loG3zl+IrfR/W1590QnrSK+Gg=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "ox": ["ox@0.14.29", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-M5j87Ec4V99MQdRct/g09eWXW60g6zhHTUs1lr4deUtrPDnezBdCJTgKd7pxqTpSZBFveV0ALi9jMMuT1qKyNg=="], "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], @@ -292,56 +147,14 @@ "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], - "random-int": ["random-int@3.1.0", "", {}, "sha512-h8CRz8cpvzj0hC/iH/1Gapgcl2TQ6xtnCpyOI5WvWfXf/yrDx2DOU+tD9rX23j36IF11xg1KqB9W11Z18JPMdw=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], @@ -356,14 +169,10 @@ "terminal-link": ["terminal-link@5.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^4.1.0" } }, "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA=="], - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "tokenx": ["tokenx@1.3.0", "", {}, "sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ=="], "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], @@ -372,18 +181,10 @@ "unlimited-timeout": ["unlimited-timeout@0.1.0", "", {}, "sha512-D4g+mxFeQGQHzCfnvij+R35ukJ0658Zzudw7j16p4tBBbNasKkKM4SocYxqhwT5xA7a9JYWDzKkEFyMlRi5sng=="], - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "viem": ["viem@2.47.1", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.0", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-frlK109+X5z2vlZeIGKa6Rxev6CcIpumV/VVhaIPc/QFotiB6t/CgUwkMlYfr4F2YNBZZ2l6jguWz2sY1XrQHw=="], "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], @@ -392,8 +193,6 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - "p-queue/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "viem/ox": ["ox@0.14.0", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-WLOB7IKnmI3Ol6RAqY7CJdZKl8QaI44LN91OGF1061YIeN6bL5IsFcdp7+oQShRyamE/8fW/CBRWhJAOzI35Dw=="], diff --git a/cli/package.json b/cli/package.json index 154ef6f..56a6616 100644 --- a/cli/package.json +++ b/cli/package.json @@ -46,15 +46,14 @@ }, "homepage": "https://github.com/FIL-Builders/foc-cli#readme", "engines": { - "node": ">=18" + "node": ">=22" }, "dependencies": { "@clack/prompts": "^1.0.0", "@filoz/synapse-core": "^0.7.0", "@filoz/synapse-sdk": "^1.0.1", - "@remix-run/fs": "^0.4.1", "conf": "^15.0.2", - "incur": "^0.3.1", + "incur": "^0.4.8", "terminal-link": "^5.0.0", "viem": "^2.47.1" }, From 245c66bcbf635c2813a3d35b6751886af44a30c1 Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 15:35:59 +0300 Subject: [PATCH 02/12] fix(cli): read version from package.json The hardcoded version had drifted (0.0.4 vs 0.1.1), so --version reported the wrong number. --- cli/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/index.ts b/cli/src/index.ts index dfebc32..fc40a6c 100755 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import { Cli } from 'incur' +import packageJson from '../package.json' with { type: 'json' } import { dataset } from './commands/dataset/index.ts' import { docsCommand } from './commands/docs.ts' import { multiUploadCommand } from './commands/multi-upload.ts' @@ -9,7 +10,7 @@ import { uploadCommand } from './commands/upload.ts' import { wallet } from './commands/wallet/index.ts' const cli = Cli.create('foc-cli', { - version: '0.0.4', + version: packageJson.version, description: 'CLI for Filecoin Onchain Cloud — decentralized storage on Filecoin with PDP verification and USDFC payments.', sync: { From 5b6dc36ec344ce74e1221e9389d877b3b4b048f1 Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 15:36:00 +0300 Subject: [PATCH 03/12] chore: remove dead link helpers from utils datasetLink, pieceLink, and dealbotLink were unused (flagged by knip); commands use the *ScannerUrl helpers. --- cli/src/utils.ts | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/cli/src/utils.ts b/cli/src/utils.ts index dc42258..38f5d61 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -9,21 +9,6 @@ export function hashLink(hash: string, chain: Chain) { return terminalLink(hash, `${chain.blockExplorers?.default?.url}/tx/${hash}`) } -export function datasetLink(dataSetId: string | bigint, chain: Chain) { - const id = dataSetId.toString() - return terminalLink( - `#${id}`, - `https://pdp.vxb.ai/${networkSlug(chain)}/dataset/${id}` - ) -} - -export function pieceLink(pieceCid: string, chain: Chain) { - return terminalLink( - pieceCid, - `https://pdp.vxb.ai/${networkSlug(chain)}/piece/${pieceCid}` - ) -} - export function datasetScannerUrl(dataSetId: string | bigint, chain: Chain) { return `https://pdp.vxb.ai/${networkSlug(chain)}/dataset/${dataSetId.toString()}` } @@ -42,11 +27,6 @@ export function dealbotDashboardUrl(chain: Chain) { : 'https://staging.dealbot.filoz.org' } -export function dealbotLink(chain: Chain) { - const url = dealbotDashboardUrl(chain) - return terminalLink('Dealbot Dashboard', url) -} - export function formatBytes(bytes: bigint): string { const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'] let value = Number(bytes) @@ -65,7 +45,7 @@ export function formatBytes(bytes: bigint): string { * so c.agent is undefined when invoked via --mcp. This helper checks both * the run context AND process-level signals (--mcp flag, non-TTY stdout). */ -export const mcpMode = process.argv.includes('--mcp') +const mcpMode = process.argv.includes('--mcp') export function isAgent(c: { agent?: boolean }): boolean { return c.agent === true || mcpMode || !process.stdout.isTTY From 8826bbeae532d085f9ada6792f7078fc3b9b30ec Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 15:36:18 +0300 Subject: [PATCH 04/12] test: extend command mocks and unexport unused fixtures Add mocks for provider-selection input + fetch health checks, piece hasMore, costs needsFwssMaxApproval, and createDataSet statusUrl (asserting statusUrl is forwarded). Drop the export keyword from fixtures only used in-file (flagged by knip). --- cli/tests/command-mocks.ts | 61 +++++++++++++++++++++++++++--- cli/tests/synapse-commands.test.ts | 5 ++- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/cli/tests/command-mocks.ts b/cli/tests/command-mocks.ts index ca76df0..718ffc5 100644 --- a/cli/tests/command-mocks.ts +++ b/cli/tests/command-mocks.ts @@ -6,7 +6,7 @@ export function cid(value: string) { } } -export const fakeChain = { +const fakeChain = { id: 314159, blockExplorers: { default: { @@ -21,7 +21,7 @@ export const fakeWalletClient = { }, } -export const fakePublicClient = { +const fakePublicClient = { name: 'public-client', } @@ -55,6 +55,7 @@ export const synapseStorage = { }, depositNeeded: 222n, ready: true, + needsFwssMaxApproval: false, }, })), upload: mock(async () => ({ @@ -68,7 +69,7 @@ export const synapseStorage = { } export const synapseConstructorArgs: any[] = [] -export class Synapse { +class Synapse { client: { waitForTransactionReceipt: typeof synapseWaitForTransactionReceipt } payments: typeof synapsePayments storage: typeof synapseStorage @@ -92,7 +93,7 @@ export const privateKeyClient = mock(() => ({ export const publicClient = mock(() => fakePublicClient) -export const getChain = mock((chainId: number) => ({ +const getChain = mock((chainId: number) => ({ ...fakeChain, id: chainId, })) @@ -127,7 +128,43 @@ export const fakeProvider = { export const getPDPProvider = mock(async () => fakeProvider) export const getApprovedPDPProviders = mock(async () => [fakeProvider]) -export const fakeDataSet = { +// Provider universe for pre-flight health selection. Three endorsed, reachable +// providers so `--copies` up to 3 resolves without a shortfall. +export const fakeProviderSelectionInput = { + providers: [ + { + id: 77n, + name: 'Provider 77', + pdp: { serviceURL: 'https://provider.example' }, + }, + { + id: 79n, + name: 'Provider 79', + pdp: { serviceURL: 'https://provider79.example' }, + }, + { + id: 80n, + name: 'Provider 80', + pdp: { serviceURL: 'https://provider80.example' }, + }, + ], + endorsedIds: [77n, 79n, 80n], + clientDataSets: [], +} + +export const fetchProviderSelectionInput = mock( + async () => fakeProviderSelectionInput +) + +// Provider health checks GET {serviceURL}/pdp/ping via global fetch; mock it so +// every provider answers 200 (reachable) by default. +export const fetchMock = mock( + async (_url: string | URL): Promise => + new Response(null, { status: 200 }) +) +globalThis.fetch = fetchMock as unknown as typeof fetch + +const fakeDataSet = { dataSetId: 42n, clientDataSetId: 100n, provider: fakeProvider, @@ -144,7 +181,7 @@ export const fakeDataSet = { export const getPdpDataSets = mock(async () => [fakeDataSet]) export const getPdpDataSet = mock(async () => fakeDataSet) -export const fakePiece = { +const fakePiece = { id: 7n, cid: cid('baga-piece'), url: 'https://provider.example/piece/baga-piece', @@ -155,10 +192,12 @@ export const fakePiece = { export const getPiecesWithMetadata = mock(async () => ({ pieces: [fakePiece], + hasMore: false, })) export const createDataSet = mock(async () => ({ txHash: '0xcreate', + statusUrl: 'https://provider.example/status', })) export const waitForCreateDataSet = mock(async () => ({ @@ -244,6 +283,7 @@ mock.module('@filoz/synapse-core/warm-storage', () => ({ getPdpDataSet, getPdpDataSets, terminateServiceSync, + fetchProviderSelectionInput, })) mock.module('@filoz/synapse-core/pdp-verifier', () => ({ @@ -320,6 +360,7 @@ export function resetCommandMocks() { }, depositNeeded: 222n, ready: true, + needsFwssMaxApproval: false, }, })) synapseStorage.upload.mockImplementation(async () => ({ @@ -332,14 +373,22 @@ export function resetCommandMocks() { getPDPProvider.mockImplementation(async () => fakeProvider) getApprovedPDPProviders.mockImplementation(async () => [fakeProvider]) + fetchProviderSelectionInput.mockImplementation( + async () => fakeProviderSelectionInput + ) + fetchMock.mockImplementation( + async () => new Response(null, { status: 200 }) + ) getPdpDataSets.mockImplementation(async () => [fakeDataSet]) getPdpDataSet.mockImplementation(async () => fakeDataSet) getPiecesWithMetadata.mockImplementation(async () => ({ pieces: [fakePiece], + hasMore: false, })) createDataSet.mockImplementation(async () => ({ txHash: '0xcreate', + statusUrl: 'https://provider.example/status', })) waitForCreateDataSet.mockImplementation(async () => ({ dataSetId: 42n, diff --git a/cli/tests/synapse-commands.test.ts b/cli/tests/synapse-commands.test.ts index 998ef50..d0ab7d6 100644 --- a/cli/tests/synapse-commands.test.ts +++ b/cli/tests/synapse-commands.test.ts @@ -568,7 +568,10 @@ describe('dataset commands', () => { serviceURL: 'https://provider.example', cdn: true, }) - expect(waitForCreateDataSet).toHaveBeenCalledWith({ txHash: '0xcreate' }) + expect(waitForCreateDataSet).toHaveBeenCalledWith({ + txHash: '0xcreate', + statusUrl: 'https://provider.example/status', + }) expect(result).toMatchObject({ dataSetId: '42', scannerUrl: 'https://pdp.vxb.ai/calibration/dataset/42', From 5bdb45e7841f4de4145a2e75a1cbe551be797401 Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 15:36:32 +0300 Subject: [PATCH 05/12] fix(output): render the real error code and message on failure OutputContext.fail passed a nested { error } object, but incur's c.error reads code/message off the top level, so every failure printed 'code: null, message: null'. Pass them at the top level; incur rebuilds the envelope. (incur's error path has no slot for processLog, so the step trail is surfaced on success only.) --- cli/src/output.ts | 19 ++++++++++++------- cli/tests/output.test.ts | 27 ++++++++++++++++----------- cli/tests/synapse-commands.test.ts | 15 +++++++++++++-- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/cli/src/output.ts b/cli/src/output.ts index 5cbd4ba..52cf80d 100644 --- a/cli/src/output.ts +++ b/cli/src/output.ts @@ -108,12 +108,17 @@ export class OutputContext { p.log.error(message) } - const error: any = { code, message } - if (opts?.retryable) error.retryable = true - - const result: any = { error, processLog: this.log } - if (opts?.cta) result.cta = opts.cta - - return this.c.error(result) + // incur's run-context `error()` reads `code`/`message`/`retryable`/`cta` + // off the TOP LEVEL of its argument and rebuilds the `{ ok:false, error }` + // envelope itself. Passing a nested `{ error }` made incur read `undefined` + // and render every failure as `code: null, message: null`, hiding the real + // cause. (incur's error envelope has no slot for processLog, so the step + // trail is only surfaced on success.) + return this.c.error({ + code, + message, + ...(opts?.retryable ? { retryable: true } : {}), + ...(opts?.cta ? { cta: opts.cta } : {}), + }) } } diff --git a/cli/tests/output.test.ts b/cli/tests/output.test.ts index 7cb2e4b..bc74e47 100644 --- a/cli/tests/output.test.ts +++ b/cli/tests/output.test.ts @@ -37,11 +37,21 @@ describe('deepSerialize', () => { }) describe('OutputContext', () => { + // Mirror incur's run-context: error() reads code/message/retryable/cta off + // the top level and rebuilds the { error } envelope (it does NOT echo the + // argument verbatim, and has no slot for processLog). function mockContext(agent: boolean) { return { agent, ok: (data: any, opts?: any) => (opts ? { ...data, ...opts } : data), - error: (err: any) => err, + error: (opts: any) => ({ + error: { + code: opts.code, + message: opts.message, + ...(opts.retryable !== undefined ? { retryable: opts.retryable } : {}), + }, + ...(opts.cta ? { cta: opts.cta } : {}), + }), } } @@ -77,7 +87,7 @@ describe('OutputContext', () => { expect(result.cta.commands[0].command).toBe('wallet balance') }) - test('fail returns error with processLog trail', () => { + test('fail returns an error envelope incur can render (code, message, cta)', () => { const c = mockContext(true) const out = new OutputContext(c) out.step('Connecting') @@ -87,16 +97,11 @@ describe('OutputContext', () => { commands: [{ command: 'wallet fund', description: 'Get tokens' }], }, }) + // code/message must survive to the top-level error object so incur + // renders them instead of `code: null, message: null`. expect(result.error.code).toBe('TX_FAILED') - expect(result.processLog[0]).toEqual({ - step: 'Connecting', - status: 'done', - }) - expect(result.processLog[1]).toEqual({ - step: 'Submitting', - status: 'failed', - error: 'insufficient funds', - }) + expect(result.error.message).toBe('insufficient funds') + expect(result.cta.commands[0].command).toBe('wallet fund') }) test('fail with retryable flag', () => { diff --git a/cli/tests/synapse-commands.test.ts b/cli/tests/synapse-commands.test.ts index d0ab7d6..bbb2881 100644 --- a/cli/tests/synapse-commands.test.ts +++ b/cli/tests/synapse-commands.test.ts @@ -87,8 +87,19 @@ function commandContext({ ok(data: any) { return data }, - error(data: any) { - return data + // Mirror incur's run-context error(): reads code/message/retryable/cta off + // the top level and rebuilds the { error } envelope (not a verbatim echo). + error(opts: any) { + return { + error: { + code: opts.code, + message: opts.message, + ...(opts.retryable !== undefined + ? { retryable: opts.retryable } + : {}), + }, + ...(opts.cta ? { cta: opts.cta } : {}), + } }, } } From 3b601f761a9c8e4b97d7af4b68eec47eccb24259 Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 15:36:46 +0300 Subject: [PATCH 06/12] fix(wallet): drop duplicate monthly rate and report approval need summary emitted monthlyAccountRate and monthlyStorageRate with identical values; keep a single account rate. costs now surfaces needsFwssMaxApproval so callers can tell 'needs deposit' from 'needs approval only'. --- cli/src/commands/wallet/costs.ts | 9 ++++++++- cli/src/commands/wallet/summary.ts | 4 ---- cli/tests/synapse-commands.test.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cli/src/commands/wallet/costs.ts b/cli/src/commands/wallet/costs.ts index 411f7c4..5bb7a4e 100644 --- a/cli/src/commands/wallet/costs.ts +++ b/cli/src/commands/wallet/costs.ts @@ -20,6 +20,7 @@ export const costsCommand = { newPerMonthRate: z.string(), depositNeeded: z.string(), alreadyCovered: z.boolean(), + needsFwssMaxApproval: z.boolean(), }), examples: [ { @@ -50,8 +51,14 @@ export const costsCommand = { }) const depositNeeded = formatBalance({ value: prep.costs.depositNeeded }) const alreadyCovered = prep.costs.ready + const needsFwssMaxApproval = prep.costs.needsFwssMaxApproval - return out.done({ newPerMonthRate, depositNeeded, alreadyCovered }) + return out.done({ + newPerMonthRate, + depositNeeded, + alreadyCovered, + needsFwssMaxApproval, + }) } catch (error) { if (c.options.debug) console.error(error) return out.fail('COSTS_FAILED', (error as Error).message) diff --git a/cli/src/commands/wallet/summary.ts b/cli/src/commands/wallet/summary.ts index dc3b020..06c5954 100644 --- a/cli/src/commands/wallet/summary.ts +++ b/cli/src/commands/wallet/summary.ts @@ -21,7 +21,6 @@ export const summaryCommand = { totalLockup: z.string(), rateBasedLockup: z.string(), monthlyAccountRate: z.string(), - monthlyStorageRate: z.string(), funds: z.string(), }), async run(c: any) { @@ -46,9 +45,6 @@ export const summaryCommand = { monthlyAccountRate: formatBalance({ value: summary.lockupRatePerMonth, }), - monthlyStorageRate: formatBalance({ - value: summary.lockupRatePerMonth, - }), funds: formatBalance({ value: summary.funds }), } diff --git a/cli/tests/synapse-commands.test.ts b/cli/tests/synapse-commands.test.ts index bbb2881..e8ed674 100644 --- a/cli/tests/synapse-commands.test.ts +++ b/cli/tests/synapse-commands.test.ts @@ -487,6 +487,7 @@ describe('wallet commands', () => { newPerMonthRate: 'formatted:111', depositNeeded: 'formatted:222', alreadyCovered: true, + needsFwssMaxApproval: false, processLog: [{ step: 'Getting costs', status: 'done' }], }) }) @@ -522,7 +523,6 @@ describe('wallet commands', () => { totalLockup: 'formatted:2', rateBasedLockup: 'formatted:3', monthlyAccountRate: 'formatted:4', - monthlyStorageRate: 'formatted:4', funds: 'formatted:5', }) }) From 1a8ad54ff9294c0dff77e6bd2c4f681a538dd92b Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 15:36:58 +0300 Subject: [PATCH 07/12] feat(dataset,piece): paginate piece listings with a next-page CTA Add --offset/--limit to dataset details and piece list; return hasMore/nextOffset and a CTA with the next-page command when more pieces remain (previously capped silently at 100). --- cli/src/commands/dataset/details.ts | 40 ++++++++++++++- cli/src/commands/piece/list.ts | 32 +++++++++++- cli/tests/synapse-commands.test.ts | 75 +++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/cli/src/commands/dataset/details.ts b/cli/src/commands/dataset/details.ts index 8e0b181..1ecc27f 100644 --- a/cli/src/commands/dataset/details.ts +++ b/cli/src/commands/dataset/details.ts @@ -13,6 +13,14 @@ export const detailsCommand = { .number() .default(314159) .describe('Chain ID. 314159 = Calibration, 314 = Mainnet'), + offset: z.coerce + .number() + .default(0) + .describe('Piece offset to start from (for pagination)'), + limit: z.coerce + .number() + .default(100) + .describe('Max pieces per page (defaults to 100)'), debug: z.boolean().optional().describe('Enable debug mode'), }), alias: { chain: 'c', dataSetId: 'd' }, @@ -38,6 +46,8 @@ export const detailsCommand = { metadata: z.record(z.string(), z.string()), }) ), + hasMore: z.boolean(), + nextOffset: z.number().optional(), }), async run(c: any) { const out = new OutputContext(c) @@ -56,10 +66,15 @@ export const detailsCommand = { ) } + const offset = c.options.offset ?? 0 + const limit = c.options.limit ?? 100 + out.step('Fetching pieces and metadata') - const { pieces } = await getPiecesWithMetadata(client, { + const { pieces, hasMore } = await getPiecesWithMetadata(client, { dataSet: ds, address: client.account.address, + offset: BigInt(offset), + limit: BigInt(limit), }) const dataset = { @@ -86,11 +101,32 @@ export const detailsCommand = { } }) + const nextOffset = offset + piecesList.length + const nextPage = hasMore + ? [ + { + command: 'dataset details', + options: { + dataSetId: c.options.dataSetId, + offset: nextOffset, + limit, + }, + description: `Show the next page of pieces (offset ${nextOffset})`, + }, + ] + : [] + return out.done( - { dataset, pieces: piecesList }, + { + dataset, + pieces: piecesList, + hasMore, + ...(hasMore ? { nextOffset } : {}), + }, { cta: { commands: [ + ...nextPage, { command: 'piece remove', description: 'Remove a piece from this dataset', diff --git a/cli/src/commands/piece/list.ts b/cli/src/commands/piece/list.ts index 7d1b525..2311637 100644 --- a/cli/src/commands/piece/list.ts +++ b/cli/src/commands/piece/list.ts @@ -15,6 +15,14 @@ export const listCommand = { .number() .default(314159) .describe('Chain ID. 314159 = Calibration, 314 = Mainnet'), + offset: z.coerce + .number() + .default(0) + .describe('Piece offset to start from (for pagination)'), + limit: z.coerce + .number() + .default(100) + .describe('Max pieces per page (defaults to 100)'), debug: z.boolean().optional().describe('Enable debug mode'), }), alias: { chain: 'c' }, @@ -29,6 +37,8 @@ export const listCommand = { metadata: z.record(z.string(), z.string()), }) ), + hasMore: z.boolean(), + nextOffset: z.number().optional(), }), examples: [ { args: { dataSetId: 42 }, description: 'List pieces in dataset #42' }, @@ -45,10 +55,15 @@ export const listCommand = { if (!dataSet) return out.fail('NOT_FOUND', `Dataset ${c.args.dataSetId} not found`) + const offset = c.options.offset ?? 0 + const limit = c.options.limit ?? 100 + out.step('Fetching pieces') - const { pieces } = await getPiecesWithMetadata(client, { + const { pieces, hasMore } = await getPiecesWithMetadata(client, { dataSet, address: client.account.address, + offset: BigInt(offset), + limit: BigInt(limit), }) const piecesList = pieces.map((piece: any) => { @@ -61,15 +76,30 @@ export const listCommand = { } }) + const nextOffset = offset + piecesList.length + const nextPage = hasMore + ? [ + { + command: 'piece list', + args: { dataSetId: c.args.dataSetId }, + options: { offset: nextOffset, limit }, + description: `Show the next page of pieces (offset ${nextOffset})`, + }, + ] + : [] + return out.done( { dataSetId: c.args.dataSetId.toString(), datasetScannerUrl: datasetScannerUrl(c.args.dataSetId, chain), pieces: piecesList, + hasMore, + ...(hasMore ? { nextOffset } : {}), }, { cta: { commands: [ + ...nextPage, { command: 'piece remove', args: { dataSetId: c.args.dataSetId }, diff --git a/cli/tests/synapse-commands.test.ts b/cli/tests/synapse-commands.test.ts index e8ed674..3e2e807 100644 --- a/cli/tests/synapse-commands.test.ts +++ b/cli/tests/synapse-commands.test.ts @@ -684,6 +684,8 @@ describe('dataset commands', () => { expect(getPiecesWithMetadata).toHaveBeenCalledWith(fakeWalletClient, { dataSet: expect.anything(), address: fakeWalletClient.account.address, + offset: 0n, + limit: 100n, }) expect(result.dataset).toMatchObject({ dataSetId: '42', @@ -706,6 +708,7 @@ describe('dataset commands', () => { metadata: { name: 'file.txt' }, }, ]) + expect(result.hasMore).toBe(false) }) test('dataset terminate calls Synapse Core and maps the termination event', async () => { @@ -734,6 +737,7 @@ describe('dataset commands', () => { metadata: {}, }, ], + hasMore: false, })) const result = await datasetDetailsCommand.run( @@ -750,6 +754,38 @@ describe('dataset commands', () => { }, ]) }) + + test('dataset details paginates and emits a next-page CTA when more pieces remain', async () => { + getPiecesWithMetadata.mockImplementationOnce(async () => ({ + pieces: [ + { + id: 7n, + cid: cid('baga-page1'), + url: 'https://provider.example/piece/baga-page1', + metadata: { name: 'file.txt' }, + }, + ], + hasMore: true, + })) + + const result = await datasetDetailsCommand.run( + commandContext({ options: { dataSetId: 42, offset: 5, limit: 1 } }) + ) + + expect(getPiecesWithMetadata).toHaveBeenCalledWith(fakeWalletClient, { + dataSet: expect.anything(), + address: fakeWalletClient.account.address, + offset: 5n, + limit: 1n, + }) + expect(result.hasMore).toBe(true) + expect(result.nextOffset).toBe(6) + expect(result.cta.commands).toContainEqual({ + command: 'dataset details', + options: { dataSetId: 42, offset: 6, limit: 1 }, + description: 'Show the next page of pieces (offset 6)', + }) + }) }) describe('piece commands', () => { @@ -764,6 +800,8 @@ describe('piece commands', () => { expect(getPiecesWithMetadata).toHaveBeenCalledWith(fakeWalletClient, { dataSet: expect.anything(), address: fakeWalletClient.account.address, + offset: 0n, + limit: 100n, }) expect(result).toMatchObject({ dataSetId: '42', @@ -777,6 +815,43 @@ describe('piece commands', () => { }, ], }) + expect(result.hasMore).toBe(false) + }) + + test('piece list paginates and emits a next-page CTA when more pieces remain', async () => { + getPiecesWithMetadata.mockImplementationOnce(async () => ({ + pieces: [ + { + id: 7n, + cid: cid('baga-page1'), + url: 'https://provider.example/piece/baga-page1', + metadata: { name: 'file.txt' }, + }, + ], + hasMore: true, + })) + + const result = await pieceListCommand.run( + commandContext({ + args: { dataSetId: 42 }, + options: { offset: 5, limit: 1 }, + }) + ) + + expect(getPiecesWithMetadata).toHaveBeenCalledWith(fakeWalletClient, { + dataSet: expect.anything(), + address: fakeWalletClient.account.address, + offset: 5n, + limit: 1n, + }) + expect(result.hasMore).toBe(true) + expect(result.nextOffset).toBe(6) + expect(result.cta.commands).toContainEqual({ + command: 'piece list', + args: { dataSetId: 42 }, + options: { offset: 6, limit: 1 }, + description: 'Show the next page of pieces (offset 6)', + }) }) test('piece remove schedules deletion and waits for the transaction', async () => { From a60a4badc8565fe8c7d5b81046c60111e699ab22 Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 15:37:13 +0300 Subject: [PATCH 08/12] feat(upload): health-check providers before context selection The SDK's smartSelect requires an endorsed primary and pings with a 1s budget that excludes healthy-but-slow providers, so uploads failed even when providers were reachable. Pre-select reachable providers ourselves (endorsed first, falling back to a healthy approved provider; reduce copies if fewer are reachable) and pass explicit providerIds, bypassing smartSelect. --- cli/src/commands/multi-upload.ts | 19 ++++- cli/src/commands/upload.ts | 19 ++++- cli/src/provider-selection.ts | 124 +++++++++++++++++++++++++++++ cli/tests/synapse-commands.test.ts | 83 ++++++++++++++++++- 4 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 cli/src/provider-selection.ts diff --git a/cli/src/commands/multi-upload.ts b/cli/src/commands/multi-upload.ts index 394e622..3358490 100644 --- a/cli/src/commands/multi-upload.ts +++ b/cli/src/commands/multi-upload.ts @@ -6,6 +6,7 @@ import { z } from 'incur' import type { Hex } from 'viem' import { privateKeyClient } from '../client.ts' import { OutputContext } from '../output.ts' +import { selectHealthyProviders } from '../provider-selection.ts' import { datasetScannerUrl, hashLink, @@ -134,9 +135,25 @@ export const multiUploadCommand = { const synapse = new Synapse({ client, source: 'foc-cli' }) + out.step('Checking provider health') + const selection = await selectHealthyProviders( + client, + c.options.copies ?? 2 + ) + if (selection.usedUnendorsedPrimary) { + out.info( + `No endorsed provider reachable — using approved provider ${selection.primaryName} for the primary copy.` + ) + } + if (selection.reducedCopies) { + out.info( + `Storing ${selection.selectedCopies} of ${selection.requestedCopies} requested copies (${selection.reachableCount} of ${selection.approvedCount} providers reachable).` + ) + } + out.step('Creating storage contexts') const contexts = await synapse.storage.createContexts({ - copies: c.options.copies, + providerIds: selection.providerIds, withCDN: c.options.withCDN, }) diff --git a/cli/src/commands/upload.ts b/cli/src/commands/upload.ts index 8d50ac5..392d0d8 100644 --- a/cli/src/commands/upload.ts +++ b/cli/src/commands/upload.ts @@ -5,6 +5,7 @@ import { Synapse } from '@filoz/synapse-sdk' import { z } from 'incur' import { privateKeyClient } from '../client.ts' import { OutputContext } from '../output.ts' +import { selectHealthyProviders } from '../provider-selection.ts' import { datasetScannerUrl, hashLink, pieceScannerUrl } from '../utils.ts' export const uploadCommand = { @@ -90,9 +91,25 @@ export const uploadCommand = { const synapse = new Synapse({ client, source: 'foc-cli' }) + out.step('Checking provider health') + const selection = await selectHealthyProviders( + client, + c.options.copies ?? 2 + ) + if (selection.usedUnendorsedPrimary) { + out.info( + `No endorsed provider reachable — using approved provider ${selection.primaryName} for the primary copy.` + ) + } + if (selection.reducedCopies) { + out.info( + `Storing ${selection.selectedCopies} of ${selection.requestedCopies} requested copies (${selection.reachableCount} of ${selection.approvedCount} providers reachable).` + ) + } + out.step('Creating storage contexts') const contexts = await synapse.storage.createContexts({ - copies: c.options.copies, + providerIds: selection.providerIds, withCDN: c.options.withCDN, }) diff --git a/cli/src/provider-selection.ts b/cli/src/provider-selection.ts new file mode 100644 index 0000000..c764c4f --- /dev/null +++ b/cli/src/provider-selection.ts @@ -0,0 +1,124 @@ +import { fetchProviderSelectionInput } from '@filoz/synapse-core/warm-storage' + +// We health-check providers with the SAME GET {serviceURL}/pdp/ping the SDK's +// SP.ping uses, but with a more forgiving timeout. The SDK hard-codes a 1s +// budget, which produces false negatives for providers that are healthy but +// slow to answer (observed ~1.7s on calibration) — they'd be wrongly excluded +// even though they accept uploads fine. +const PING_TIMEOUT_MS = 5000 +const PING_ATTEMPTS = 2 +const PING_RETRY_DELAY_MS = 300 + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +async function pingOnce(serviceURL: string): Promise { + const url = new URL('pdp/ping', serviceURL).toString() + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), PING_TIMEOUT_MS) + try { + const response = await fetch(url, { signal: controller.signal }) + if (!response.ok) throw new Error(`HTTP ${response.status}`) + } finally { + clearTimeout(timer) + } +} + +async function pingWithRetries(serviceURL: string): Promise { + let lastError: unknown + for (let attempt = 0; attempt < PING_ATTEMPTS; attempt++) { + try { + await pingOnce(serviceURL) + return + } catch (error) { + lastError = error + if (attempt < PING_ATTEMPTS - 1) await delay(PING_RETRY_DELAY_MS) + } + } + throw lastError instanceof Error + ? lastError + : new Error('Provider unreachable') +} + +export interface HealthyProviderSelection { + /** Provider IDs to pass to `createContexts({ providerIds })`, primary first. */ + providerIds: bigint[] + /** Display name of the provider chosen for the primary copy (index 0). */ + primaryName: string + /** True when no endorsed provider was reachable and a non-endorsed approved + * provider had to take the primary slot. */ + usedUnendorsedPrimary: boolean + requestedCopies: number + selectedCopies: number + /** True when fewer providers were reachable than the requested copy count. */ + reducedCopies: boolean + reachableCount: number + approvedCount: number +} + +/** + * Pre-flight provider selection performed OUTSIDE the SDK's smartSelect. + * + * `synapse.storage.createContexts({ copies })` delegates to smartSelect, which + * requires the primary copy to come from the on-chain *endorsed* set and throws + * ("No endorsed provider available — all failed health check") when every + * endorsed provider fails its ping — even when other approved providers are + * healthy. We instead fetch the provider universe, ping each provider (with + * retries, since a single ping yields false negatives), and choose reachable + * providers ourselves: endorsed first (preserving the trust preference for the + * primary), then any reachable approved provider. The chosen ids are passed to + * `createContexts({ providerIds })`, which resolves them directly and skips + * smartSelect entirely. + * + * Note: the providerIds resolve path does NOT ping, so this health check is what + * guarantees we target live providers. + */ +export async function selectHealthyProviders( + client: any, + requestedCopies: number +): Promise { + const input = await fetchProviderSelectionInput(client, { + address: client.account.address, + }) + + const approvedCount = input.providers.length + const endorsed = new Set(input.endorsedIds.map((id: bigint) => id.toString())) + + const pings = await Promise.allSettled( + input.providers.map((p: any) => pingWithRetries(p.pdp.serviceURL)) + ) + const reachable = input.providers.filter( + (_: any, i: number) => pings[i].status === 'fulfilled' + ) + + if (reachable.length === 0) { + throw new Error( + `No reachable storage providers (${approvedCount} approved, all failed their health check). ` + + 'Providers may be temporarily down — retry shortly.' + ) + } + + // Endorsed first so the primary copy prefers the curated set; fall back to any + // reachable approved provider for the primary when none are endorsed. + const reachableEndorsed = reachable.filter((p: any) => + endorsed.has(p.id.toString()) + ) + const reachableOther = reachable.filter( + (p: any) => !endorsed.has(p.id.toString()) + ) + const ordered = [...reachableEndorsed, ...reachableOther] + + const selectedCopies = Math.min(requestedCopies, ordered.length) + const selected = ordered.slice(0, selectedCopies) + const primary = selected[0] + + return { + providerIds: selected.map((p: any) => p.id), + primaryName: primary.name || `Provider #${primary.id.toString()}`, + usedUnendorsedPrimary: reachableEndorsed.length === 0, + requestedCopies, + selectedCopies, + reducedCopies: selectedCopies < requestedCopies, + reachableCount: reachable.length, + approvedCount, + } +} diff --git a/cli/tests/synapse-commands.test.ts b/cli/tests/synapse-commands.test.ts index 3e2e807..0ae8c81 100644 --- a/cli/tests/synapse-commands.test.ts +++ b/cli/tests/synapse-commands.test.ts @@ -10,6 +10,8 @@ import { createDataSetAndAddPieces, fakeProvider, fakeWalletClient, + fetchMock, + fetchProviderSelectionInput, findPiece, formatBalance, getAccountSummary, @@ -67,6 +69,9 @@ const { listCommand: pieceListCommand } = await import( const { removeCommand: pieceRemoveCommand } = await import( '../src/commands/piece/remove.ts' ) +const { selectHealthyProviders } = await import( + '../src/provider-selection.ts' +) const tempDirs: string[] = [] @@ -177,8 +182,11 @@ describe('top-level upload commands', () => { expect(synapseConstructorArgs).toEqual([ { client: fakeWalletClient, source: 'foc-cli' }, ]) + expect(fetchProviderSelectionInput).toHaveBeenCalledWith(fakeWalletClient, { + address: fakeWalletClient.account.address, + }) expect(synapseStorage.createContexts).toHaveBeenCalledWith({ - copies: 2, + providerIds: [77n, 79n], withCDN: true, }) expect(synapseStorage.prepare).toHaveBeenCalledWith({ @@ -884,3 +892,76 @@ describe('piece commands', () => { expect(result.dataSetId).toBe('42') }) }) + +describe('provider health selection', () => { + test('selects reachable endorsed providers first, primary first', async () => { + const selection = await selectHealthyProviders(fakeWalletClient, 2) + + expect(fetchProviderSelectionInput).toHaveBeenCalledWith(fakeWalletClient, { + address: fakeWalletClient.account.address, + }) + expect(selection.providerIds).toEqual([77n, 79n]) + expect(selection.usedUnendorsedPrimary).toBe(false) + expect(selection.reducedCopies).toBe(false) + expect(selection.reachableCount).toBe(3) + }) + + test('falls back to a reachable non-endorsed provider for the primary when no endorsed is reachable', async () => { + fetchProviderSelectionInput.mockImplementation(async () => ({ + providers: [ + { + id: 77n, + name: 'Endorsed', + pdp: { serviceURL: 'https://endorsed.example' }, + }, + { + id: 81n, + name: 'Approved', + pdp: { serviceURL: 'https://approved.example' }, + }, + ], + endorsedIds: [77n], + clientDataSets: [], + })) + // The only endorsed provider is down; the approved one answers. + fetchMock.mockImplementation(async (url: string | URL) => + String(url).includes('endorsed.example') + ? new Response(null, { status: 503 }) + : new Response(null, { status: 200 }) + ) + + const selection = await selectHealthyProviders(fakeWalletClient, 1) + + expect(selection.providerIds).toEqual([81n]) + expect(selection.usedUnendorsedPrimary).toBe(true) + expect(selection.primaryName).toBe('Approved') + expect(selection.reducedCopies).toBe(false) + }) + + test('reduces copies when fewer providers are reachable than requested', async () => { + // Only provider 77 (https://provider.example) answers. + fetchMock.mockImplementation(async (url: string | URL) => + String(url).includes('://provider.example') + ? new Response(null, { status: 200 }) + : new Response(null, { status: 503 }) + ) + + const selection = await selectHealthyProviders(fakeWalletClient, 3) + + expect(selection.providerIds).toEqual([77n]) + expect(selection.selectedCopies).toBe(1) + expect(selection.requestedCopies).toBe(3) + expect(selection.reducedCopies).toBe(true) + expect(selection.reachableCount).toBe(1) + }) + + test('throws a clear error when no provider is reachable', async () => { + fetchMock.mockImplementation( + async () => new Response(null, { status: 503 }) + ) + + await expect(selectHealthyProviders(fakeWalletClient, 2)).rejects.toThrow( + /No reachable storage providers/ + ) + }) +}) From 23b0f9ff1304c4ca3cc51ad578a94c94ee99be82 Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 15:37:29 +0300 Subject: [PATCH 09/12] docs(skills): refresh foc-cli and foc-docs skills Document the pagination flags and needsFwssMaxApproval, replace the stale pricing minimum with the v1 model, sharpen triggering, and clarify the foc-cli vs foc-docs boundary. --- skills/foc-cli/SKILL.md | 18 ++++++++++-------- skills/foc-docs/SKILL.md | 6 ++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/skills/foc-cli/SKILL.md b/skills/foc-cli/SKILL.md index 8aa48df..da8fe91 100644 --- a/skills/foc-cli/SKILL.md +++ b/skills/foc-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: foc-cli -description: Use when working with Filecoin Onchain Cloud, the foc-cli CLI, Synapse SDK, storing files on Filecoin, PDP datasets, USDFC payments, or any decentralized cloud storage task on Filecoin. Triggers on "foc", "filecoin cloud", "synapse", "warm storage", "PDP", "USDFC", "foc-cli", "upload", "store", "wallet", "deposit", "dataset", "piece", "provider". +description: Use when performing Filecoin Onchain Cloud storage or payment operations from the command line with foc-cli — uploading/storing files on Filecoin, managing PDP datasets and pieces, funding a wallet, depositing or withdrawing USDFC, estimating costs, or listing providers via the Synapse SDK stack. Reach for this whenever the user wants to actually run or execute an FOC/Synapse storage action, even if they don't name the tool. Triggers on "foc", "foc-cli", "filecoin cloud", "synapse", "warm storage", "PDP", "USDFC", "upload to filecoin", "store on filecoin", "wallet", "deposit", "withdraw", "dataset", "piece", "provider". For looking up documentation or SDK reference (rather than running a command), use the foc-docs skill instead. --- # foc-cli — Filecoin Onchain Cloud CLI @@ -22,7 +22,7 @@ FOC turns Filecoin into a **programmable cloud** with four layers: **Data model:** Files → **Pieces** (by CID) → grouped into **Data Sets** on PDP providers → funded by **Payment Rails** (continuous USDFC streams). -**Pricing:** $2.5/TiB/month/copy (min 2 copies), minimum 0.06 USDFC/month (~24 GiB). +**Pricing (Synapse v1, per-operation):** storage is billed as a size-based rate per copy per month **plus a flat per-data-set monthly fee** — v1 removed the old fixed per-account minimum, so there is no single "minimum/month" number anymore. Default is 2 copies. Don't hardcode a price from memory: run `wallet costs --extraBytes --extraRunway ` for the live rate, deposit needed, and whether an operator approval is still required. Treat that command as the source of truth. ## Setup @@ -34,15 +34,16 @@ Config: `~/Library/Preferences/foc-cli/config.json` (macOS). Keys: `privateKey`, ## Self-Documenting -Every command supports `-h` for full usage, args, options, and examples: +Every command supports `-h` for full usage, args, options, and examples, and `--schema` for the machine-readable JSON Schema of its args/options/output: ```bash npx foc-cli --help # all commands npx foc-cli upload -h # upload args/options/examples npx foc-cli wallet deposit -h # deposit args/options +npx foc-cli dataset details --schema # full JSON Schema for a command ``` -**Always use `-h` first** to discover the exact interface before running a command. +**Use `-h` (or `--schema`) first** to discover the exact interface before running a command. The CLI is self-describing, so if anything in this file ever disagrees with the live `-h`/`--schema` output, trust the CLI — it is the source of truth, and the tables below are just a fast map. ## Global Options @@ -52,7 +53,7 @@ All commands accept these — not repeated per-command below: |--------|---------|-------------| | `--chain ` / `-c` | `314159` | `314159` = Calibration testnet, `314` = Mainnet | | `--debug` | `false` | Verbose error logging with stack traces | -| `--format ` | `toon` | Output: `toon`, `json`, `yaml`, `md` | +| `--format ` | `toon` | Output: `toon`, `json`, `yaml`, `md`, `jsonl` | | `--json` | | Shorthand for `--format json` | | `-h` / `--help` | | Show help for any command | @@ -81,14 +82,14 @@ npx foc-cli multi-upload ./a.pdf,./b.pdf # all paths must be readable | `wallet deposit ` | Deposit USDFC into payment account | | `wallet withdraw ` | Withdraw USDFC from payment account | | `wallet summary` | Account summary with funding timeline | -| `wallet costs --extraBytes N --extraRunway N` | Calculate upload costs + deposit needed | +| `wallet costs --extraBytes N --extraRunway N` | Live upload cost: per-month rate, `depositNeeded`, `alreadyCovered`, and `needsFwssMaxApproval` (true = funds suffice but a one-time operator approval is still required) | ### Dataset Management | Command | Description | |---------|-------------| | `dataset list` | All datasets with provider, CDN status, state | -| `dataset details -d ` | Dataset metadata + all pieces | +| `dataset details -d [--offset N] [--limit M]` | Dataset metadata + pieces. Lists up to `--limit` pieces (default 100) starting at `--offset`; when more remain it returns `hasMore` + `nextOffset` and a CTA with the exact next-page command | | `dataset create [--cdn]` | Create dataset with a provider from `provider list` | | `dataset upload [--cdn]` | Create dataset + upload in one step | | `dataset terminate ` | Stop PDP service for a dataset | @@ -97,7 +98,7 @@ npx foc-cli multi-upload ./a.pdf,./b.pdf # all paths must be readable | Command | Description | |---------|-------------| -| `piece list ` | Pieces in dataset with CID + metadata | +| `piece list [--offset N] [--limit M]` | Pieces in dataset with CID + metadata. Paginated (default 100/page); when `hasMore` is true, follow the returned next-page CTA (`--offset `) to fetch the rest | | `piece remove ` | Remove piece from dataset | ### Provider Info @@ -132,6 +133,7 @@ npx foc-cli wallet costs --extraBytes 1000000 --extraRunway 1 # check costs fir npx foc-cli dataset list npx foc-cli dataset details -d 42 npx foc-cli piece list 42 +npx foc-cli piece list 42 --offset 100 --limit 100 # next page when hasMore is true npx foc-cli piece remove 42 7 npx foc-cli dataset terminate 42 ``` diff --git a/skills/foc-docs/SKILL.md b/skills/foc-docs/SKILL.md index fe12877..69a5f0d 100644 --- a/skills/foc-docs/SKILL.md +++ b/skills/foc-docs/SKILL.md @@ -1,6 +1,6 @@ --- name: foc-docs -description: Search and fetch Filecoin Onchain Cloud documentation. Use when looking up Synapse SDK APIs, storage guides, payment operations, PDP concepts, or any FOC reference material. Triggers on "docs", "documentation", "how to", "guide", "reference", "API", "Synapse SDK docs". +description: Search and fetch Filecoin Onchain Cloud documentation with `npx foc-cli docs`. Use when the user wants to look up or understand FOC / Synapse SDK reference material — storage and payment guides, PDP concepts, session keys, React hooks, API signatures, or "how does X work" questions — rather than execute a storage operation. Reach for this whenever the user asks how something in FOC/Synapse works, needs an API signature or doc link, or is researching before building. Triggers on "foc docs", "filecoin cloud docs", "synapse docs", "how does ... work", "how to", "guide", "reference", "API". To actually run commands (upload, wallet, dataset, piece), use the foc-cli skill instead. --- # foc-docs — Documentation Search @@ -9,7 +9,7 @@ Fast, filtered access to **Filecoin Onchain Cloud** docs via `npx foc-cli docs`. ## How It Works -Searches a curated index of ~28 doc pages (from 1300+ raw entries). When your search narrows to 1-3 matches, it **auto-fetches** the top result in a single call. +Builds a curated, depth-filtered index live from the docs site's `llms.txt` (dropping the bulk of deep API-reference entries), then ranks it against your `--prompt`. When the search narrows to 1-3 matches it **auto-fetches** the top result in the same call — so a good prompt usually answers the question in one round-trip instead of guessing URLs. ## Command @@ -52,6 +52,8 @@ npx foc-cli docs --url --maxDepth 2 # high-level only ## Doc Map +Common entry points — handy shortcuts, not an exhaustive list. The docs evolve, so if a prompt below returns nothing, fall back to a plain `--prompt` search (the live index is the source of truth). + | Topic | Prompt | URL (relative to `docs.filecoin.cloud/developer-guides/`) | |-------|--------|-----------------------------------------------------------| | Upload files | `"upload"` | `storage/storage-operations.md` | From 1c14943fa19d84f618ad3b13fe342a3991cc53d8 Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 16:23:51 +0300 Subject: [PATCH 10/12] refactor(cli): centralize Synapse construction in synapseClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the seven duplicated `new Synapse({ client, source })` sites with a single `synapseClient(chainId)` helper returning { client, chain, synapse }. The source tag is resolved in one place — from the config store, defaulting to "foc-cli". --- cli/src/commands/multi-upload.ts | 7 ++----- cli/src/commands/upload.ts | 7 ++----- cli/src/commands/wallet/balance.ts | 11 +++++------ cli/src/commands/wallet/costs.ts | 7 ++----- cli/src/commands/wallet/deposit.ts | 7 +++---- cli/src/commands/wallet/fund.ts | 6 ++---- cli/src/commands/wallet/withdraw.ts | 7 +++---- cli/src/config.ts | 4 ++++ cli/src/synapse.ts | 28 ++++++++++++++++++++++++++++ cli/tests/command-mocks.ts | 13 +++++++++++++ 10 files changed, 64 insertions(+), 33 deletions(-) create mode 100644 cli/src/synapse.ts diff --git a/cli/src/commands/multi-upload.ts b/cli/src/commands/multi-upload.ts index 3358490..003e4bf 100644 --- a/cli/src/commands/multi-upload.ts +++ b/cli/src/commands/multi-upload.ts @@ -1,12 +1,11 @@ import { readFile } from 'node:fs/promises' import path from 'node:path' -import { Synapse } from '@filoz/synapse-sdk' import type { StorageContext } from '@filoz/synapse-sdk/storage' import { z } from 'incur' import type { Hex } from 'viem' -import { privateKeyClient } from '../client.ts' import { OutputContext } from '../output.ts' import { selectHealthyProviders } from '../provider-selection.ts' +import { synapseClient } from '../synapse.ts' import { datasetScannerUrl, hashLink, @@ -90,7 +89,7 @@ export const multiUploadCommand = { ], async run(c: any) { const out = new OutputContext(c) - const { client, chain } = privateKeyClient(c.options.chain) + const { client, chain, synapse } = synapseClient(c.options.chain) try { out.step('Reading files') @@ -133,8 +132,6 @@ export const multiUploadCommand = { }) ) - const synapse = new Synapse({ client, source: 'foc-cli' }) - out.step('Checking provider health') const selection = await selectHealthyProviders( client, diff --git a/cli/src/commands/upload.ts b/cli/src/commands/upload.ts index 392d0d8..b94a082 100644 --- a/cli/src/commands/upload.ts +++ b/cli/src/commands/upload.ts @@ -1,11 +1,10 @@ import { readFile } from 'node:fs/promises' import path from 'node:path' import type { FailedAttempt } from '@filoz/synapse-sdk' -import { Synapse } from '@filoz/synapse-sdk' import { z } from 'incur' -import { privateKeyClient } from '../client.ts' import { OutputContext } from '../output.ts' import { selectHealthyProviders } from '../provider-selection.ts' +import { synapseClient } from '../synapse.ts' import { datasetScannerUrl, hashLink, pieceScannerUrl } from '../utils.ts' export const uploadCommand = { @@ -76,7 +75,7 @@ export const uploadCommand = { ], async run(c: any) { const out = new OutputContext(c) - const { client, chain } = privateKeyClient(c.options.chain) + const { client, chain, synapse } = synapseClient(c.options.chain) try { out.step('Reading file') @@ -89,8 +88,6 @@ export const uploadCommand = { }, }) - const synapse = new Synapse({ client, source: 'foc-cli' }) - out.step('Checking provider health') const selection = await selectHealthyProviders( client, diff --git a/cli/src/commands/wallet/balance.ts b/cli/src/commands/wallet/balance.ts index e4e9b53..35ea5d9 100644 --- a/cli/src/commands/wallet/balance.ts +++ b/cli/src/commands/wallet/balance.ts @@ -1,8 +1,8 @@ import { formatBalance } from '@filoz/synapse-core/utils' -import { Synapse, TOKENS } from '@filoz/synapse-sdk' +import { TOKENS } from '@filoz/synapse-sdk' import { z } from 'incur' -import { privateKeyClient } from '../../client.ts' import { OutputContext } from '../../output.ts' +import { synapseClient } from '../../synapse.ts' export const balanceCommand = { description: 'Check FIL and USDFC wallet balances and payment account info', @@ -29,11 +29,11 @@ export const balanceCommand = { ], async run(c: any) { const out = new OutputContext(c) - const { client } = privateKeyClient(c.options.chain) + const { client, synapse } = synapseClient(c.options.chain) try { out.step('Checking wallet balance') - const result = await fetchBalances(client) + const result = await fetchBalances(client, synapse) return out.done(result) } catch (error) { @@ -42,8 +42,7 @@ export const balanceCommand = { }, } -async function fetchBalances(client: any) { - const synapse = new Synapse({ client, source: 'foc-cli' }) +async function fetchBalances(client: any, synapse: any) { const filBalance = await synapse.payments.walletBalance() const usdfcBalance = await synapse.payments.walletBalance({ token: TOKENS.USDFC, diff --git a/cli/src/commands/wallet/costs.ts b/cli/src/commands/wallet/costs.ts index 5bb7a4e..f024263 100644 --- a/cli/src/commands/wallet/costs.ts +++ b/cli/src/commands/wallet/costs.ts @@ -1,8 +1,7 @@ import { formatBalance } from '@filoz/synapse-core/utils' -import { Synapse } from '@filoz/synapse-sdk' import { z } from 'incur' -import { privateKeyClient } from '../../client.ts' import { OutputContext } from '../../output.ts' +import { synapseClient } from '../../synapse.ts' export const costsCommand = { description: 'Get costs for uploading a file to Filecoin warm storage', @@ -34,13 +33,11 @@ export const costsCommand = { ], async run(c: any) { const out = new OutputContext(c) - const { client } = privateKeyClient(c.options.chain) + const { synapse } = synapseClient(c.options.chain) try { out.step('Getting costs') - const synapse = new Synapse({ client, source: 'foc-cli' }) - const prep = await synapse.storage.prepare({ dataSize: BigInt(c.options.extraBytes), extraRunwayEpochs: BigInt(c.options.extraRunway * 30 * 24 * 60 * 2), diff --git a/cli/src/commands/wallet/deposit.ts b/cli/src/commands/wallet/deposit.ts index cd0dedd..6a13329 100644 --- a/cli/src/commands/wallet/deposit.ts +++ b/cli/src/commands/wallet/deposit.ts @@ -1,7 +1,7 @@ -import { parseUnits, Synapse } from '@filoz/synapse-sdk' +import { parseUnits } from '@filoz/synapse-sdk' import { z } from 'incur' -import { privateKeyClient } from '../../client.ts' import { OutputContext } from '../../output.ts' +import { synapseClient } from '../../synapse.ts' import { hashLink, txExplorerUrl } from '../../utils.ts' export const depositCommand = { @@ -32,8 +32,7 @@ export const depositCommand = { ], async run(c: any) { const out = new OutputContext(c) - const { client, chain } = privateKeyClient(c.options.chain) - const synapse = new Synapse({ client, source: 'foc-cli' }) + const { chain, synapse } = synapseClient(c.options.chain) try { out.step('Depositing funds') diff --git a/cli/src/commands/wallet/fund.ts b/cli/src/commands/wallet/fund.ts index 5f73dbf..9524156 100644 --- a/cli/src/commands/wallet/fund.ts +++ b/cli/src/commands/wallet/fund.ts @@ -1,9 +1,8 @@ import { claimTokens, formatBalance } from '@filoz/synapse-core/utils' -import { Synapse } from '@filoz/synapse-sdk' import { z } from 'incur' import { waitForTransactionReceipt } from 'viem/actions' -import { privateKeyClient } from '../../client.ts' import { OutputContext } from '../../output.ts' +import { synapseClient } from '../../synapse.ts' export const fundCommand = { description: 'Request testnet FIL and USDFC from faucet (testnet only)', @@ -21,7 +20,7 @@ export const fundCommand = { hint: 'Only works on Calibration testnet (chain 314159).', async run(c: any) { const out = new OutputContext(c) - const { client } = privateKeyClient(c.options.chain) + const { client, synapse } = synapseClient(c.options.chain) try { out.step('Requesting faucet tokens') @@ -31,7 +30,6 @@ export const fundCommand = { await waitForTransactionReceipt(client, { hash: hashes[0].tx_hash }) out.step('Fetching updated balances') - const synapse = new Synapse({ client, source: 'foc-cli' }) const filBalance = await synapse.payments.walletBalance() const usdfcBalance = await synapse.payments.walletBalance({ token: 'USDFC', diff --git a/cli/src/commands/wallet/withdraw.ts b/cli/src/commands/wallet/withdraw.ts index da1ad53..bafd8e9 100644 --- a/cli/src/commands/wallet/withdraw.ts +++ b/cli/src/commands/wallet/withdraw.ts @@ -1,7 +1,7 @@ -import { parseUnits, Synapse } from '@filoz/synapse-sdk' +import { parseUnits } from '@filoz/synapse-sdk' import { z } from 'incur' -import { privateKeyClient } from '../../client.ts' import { OutputContext } from '../../output.ts' +import { synapseClient } from '../../synapse.ts' import { hashLink, txExplorerUrl } from '../../utils.ts' export const withdrawCommand = { @@ -25,8 +25,7 @@ export const withdrawCommand = { examples: [{ args: { amount: '1' }, description: 'Withdraw 1 USDFC' }], async run(c: any) { const out = new OutputContext(c) - const { client, chain } = privateKeyClient(c.options.chain) - const synapse = new Synapse({ client, source: 'foc-cli' }) + const { chain, synapse } = synapseClient(c.options.chain) try { out.step('Withdrawing funds') diff --git a/cli/src/config.ts b/cli/src/config.ts index babc561..6e915e1 100644 --- a/cli/src/config.ts +++ b/cli/src/config.ts @@ -8,11 +8,15 @@ const schema = { privateKey: { type: 'string', }, + source: { + type: 'string', + }, } const config = new Conf<{ privateKey: string keystore: string + source: string }>({ projectName: packageJson.name, projectVersion: packageJson.version, diff --git a/cli/src/synapse.ts b/cli/src/synapse.ts new file mode 100644 index 0000000..6b0feeb --- /dev/null +++ b/cli/src/synapse.ts @@ -0,0 +1,28 @@ +import { Synapse } from '@filoz/synapse-sdk' +import { privateKeyClient } from './client.ts' +import config from './config.ts' + +/** + * The `source` string Synapse reports to Warm Storage (used for telemetry and + * attribution). Persisted in the CLI config — set it with + * `wallet init --source `; defaults to "foc-cli" when unset. + */ +function synapseSource(): string { + return config.get('source') ?? 'foc-cli' +} + +/** + * Resolve the wallet client + chain for `chainId` together with a Synapse + * instance bound to it — the single setup path for any command that talks to + * Synapse, so the `source` (config `source`, default "foc-cli") is resolved in + * exactly one place. + * + * Commands that only need the raw wallet/public client (e.g. dataset and piece + * reads, which call synapse-core directly) keep using `privateKeyClient` / + * `publicClient` from `./client.ts`. + */ +export function synapseClient(chainId: number) { + const { client, chain } = privateKeyClient(chainId) + const synapse = new Synapse({ client, source: synapseSource() }) + return { client, chain, synapse } +} diff --git a/cli/tests/command-mocks.ts b/cli/tests/command-mocks.ts index 718ffc5..b9035b9 100644 --- a/cli/tests/command-mocks.ts +++ b/cli/tests/command-mocks.ts @@ -252,6 +252,15 @@ export const waitForTransactionReceipt = mock(async () => ({ status: 'success', })) +export const configStore = { + path: '/tmp/foc-cli-test-config.json', + get: mock((_key: string): string | undefined => undefined), + set: mock((_key: string, _value: string) => {}), + delete: mock((_key: string) => {}), +} + +mock.module('../src/config.ts', () => ({ default: configStore })) + mock.module('../src/client.ts', () => ({ privateKeyClient, publicClient, @@ -317,6 +326,10 @@ export function resetCommandMocks() { mock.clearAllMocks() synapseConstructorArgs.length = 0 + configStore.get.mockImplementation(() => undefined) + configStore.set.mockImplementation(() => {}) + configStore.delete.mockImplementation(() => {}) + privateKeyClient.mockImplementation(() => ({ client: fakeWalletClient, chain: fakeChain, From a39fba2ffe235af2fccf7d2193ef6ba4db4d590c Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 16:24:02 +0300 Subject: [PATCH 11/12] feat(wallet): set the Synapse source tag via init Persist `source` in the config store; `wallet init --source ` writes it (defaults to "foc-cli"). Document the setting in the README and skill. --- README.md | 8 ++++++++ cli/src/commands/wallet/init.ts | 21 ++++++++++++++++++++- cli/tests/synapse-commands.test.ts | 21 +++++++++++++++++++++ skills/foc-cli/SKILL.md | 4 +++- 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d99e888..3e8aced 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,14 @@ npx foc-cli docs --url # Fetch specific page | `--format ` | `toon` | Output format: `toon`, `json`, `yaml`, `md` | | `--json` | | Shorthand for `--format json` | +### Source tag + +The `source` string the CLI reports to Synapse/Warm Storage (telemetry & attribution) is stored in your config. Set it to identify your app or integration (defaults to `foc-cli`): + +```bash +npx foc-cli wallet init --source my-app +``` + ## How FOC Works FOC transforms Filecoin into a **programmable cloud storage layer**: diff --git a/cli/src/commands/wallet/init.ts b/cli/src/commands/wallet/init.ts index b7736a4..a055bdc 100644 --- a/cli/src/commands/wallet/init.ts +++ b/cli/src/commands/wallet/init.ts @@ -15,6 +15,12 @@ export const initCommand = { .optional() .describe('Path to a Foundry keystore file (requires foundry)'), privateKey: z.string().optional().describe('Private key (0x-prefixed hex)'), + source: z + .string() + .optional() + .describe( + 'Source tag reported to Synapse/Warm Storage for telemetry (default: foc-cli)' + ), }), alias: { auto: 'a' }, examples: [ @@ -28,11 +34,19 @@ export const initCommand = { options: { privateKey: '0x...' }, description: 'Set private key directly', }, + { + options: { auto: true, source: 'my-app' }, + description: 'Generate a key and set the source tag', + }, ], async run(c: any) { const out = new OutputContext(c) const agent = isAgent(c) + if (c.options.source) { + config.set('source', c.options.source) + } + if (c.options.keystore) { if (existsSync(c.options.keystore)) { out.step('Configuring keystore') @@ -75,6 +89,7 @@ export const initCommand = { return out.done({ status: 'already_configured', configPath: config.path, + source: config.get('source') ?? 'foc-cli', }) } @@ -86,7 +101,11 @@ export const initCommand = { p.log.success(`Private key: ${privateKey}`) p.outro("You're all set!") } - return out.done({ status: 'configured', method: 'auto' }) + return out.done({ + status: 'configured', + method: 'auto', + source: config.get('source') ?? 'foc-cli', + }) } // Agent mode: require explicit options diff --git a/cli/tests/synapse-commands.test.ts b/cli/tests/synapse-commands.test.ts index 0ae8c81..ab9d02e 100644 --- a/cli/tests/synapse-commands.test.ts +++ b/cli/tests/synapse-commands.test.ts @@ -6,6 +6,7 @@ import { calculate, cid, claimTokens, + configStore, createDataSet, createDataSetAndAddPieces, fakeProvider, @@ -72,6 +73,7 @@ const { removeCommand: pieceRemoveCommand } = await import( const { selectHealthyProviders } = await import( '../src/provider-selection.ts' ) +const { synapseClient } = await import('../src/synapse.ts') const tempDirs: string[] = [] @@ -965,3 +967,22 @@ describe('provider health selection', () => { ) }) }) + +describe('synapse client construction', () => { + test('reports the configured source, defaulting to foc-cli', () => { + synapseClient(314159) + expect(synapseConstructorArgs.at(-1)).toEqual({ + client: fakeWalletClient, + source: 'foc-cli', + }) + + configStore.get.mockImplementation((key: string) => + key === 'source' ? 'my-app' : undefined + ) + synapseClient(314159) + expect(synapseConstructorArgs.at(-1)).toEqual({ + client: fakeWalletClient, + source: 'my-app', + }) + }) +}) diff --git a/skills/foc-cli/SKILL.md b/skills/foc-cli/SKILL.md index da8fe91..0c9a004 100644 --- a/skills/foc-cli/SKILL.md +++ b/skills/foc-cli/SKILL.md @@ -30,7 +30,9 @@ FOC turns Filecoin into a **programmable cloud** with four layers: npx foc-cli wallet init --auto # generate wallet (or --keystore , --privateKey ) ``` -Config: `~/Library/Preferences/foc-cli/config.json` (macOS). Keys: `privateKey`, `keystore`. +Config: `~/Library/Preferences/foc-cli/config.json` (macOS). Keys: `privateKey`, `keystore`, `source`. + +**Source tag:** `source` is the tag the CLI reports to Synapse/Warm Storage (telemetry & attribution). Set it with `wallet init --source ` (persisted in config); defaults to `foc-cli`. ## Self-Documenting From 92cc03a47ac8c7cba9347d80f1c75adc49d41a48 Mon Sep 17 00:00:00 2001 From: nijoe1 Date: Tue, 16 Jun 2026 16:44:41 +0300 Subject: [PATCH 12/12] chore(lint): format tests with biome and lint the tests dir The lint script only formatted src/, so test files drifted from biome's style and CI's `biome check src tests` failed. Format them and add tests/ to the lint script so local matches CI. --- cli/package.json | 2 +- cli/tests/command-mocks.ts | 4 +--- cli/tests/output.test.ts | 4 +++- cli/tests/synapse-commands.test.ts | 4 +--- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cli/package.json b/cli/package.json index 56a6616..bd85952 100644 --- a/cli/package.json +++ b/cli/package.json @@ -21,7 +21,7 @@ "prepublishOnly": "rm -rf dist && tsc && cp ../README.md ../LICENSE . && cp -r ../skills .", "postpublish": "rm -f README.md LICENSE && rm -rf skills", "test": "bun test", - "lint": "tsc --noEmit && biome check --fix src/" + "lint": "tsc --noEmit && biome check --fix src/ tests/" }, "keywords": [ "filecoin", diff --git a/cli/tests/command-mocks.ts b/cli/tests/command-mocks.ts index b9035b9..965e015 100644 --- a/cli/tests/command-mocks.ts +++ b/cli/tests/command-mocks.ts @@ -389,9 +389,7 @@ export function resetCommandMocks() { fetchProviderSelectionInput.mockImplementation( async () => fakeProviderSelectionInput ) - fetchMock.mockImplementation( - async () => new Response(null, { status: 200 }) - ) + fetchMock.mockImplementation(async () => new Response(null, { status: 200 })) getPdpDataSets.mockImplementation(async () => [fakeDataSet]) getPdpDataSet.mockImplementation(async () => fakeDataSet) getPiecesWithMetadata.mockImplementation(async () => ({ diff --git a/cli/tests/output.test.ts b/cli/tests/output.test.ts index bc74e47..3e05628 100644 --- a/cli/tests/output.test.ts +++ b/cli/tests/output.test.ts @@ -48,7 +48,9 @@ describe('OutputContext', () => { error: { code: opts.code, message: opts.message, - ...(opts.retryable !== undefined ? { retryable: opts.retryable } : {}), + ...(opts.retryable !== undefined + ? { retryable: opts.retryable } + : {}), }, ...(opts.cta ? { cta: opts.cta } : {}), }), diff --git a/cli/tests/synapse-commands.test.ts b/cli/tests/synapse-commands.test.ts index ab9d02e..5fa5b72 100644 --- a/cli/tests/synapse-commands.test.ts +++ b/cli/tests/synapse-commands.test.ts @@ -70,9 +70,7 @@ const { listCommand: pieceListCommand } = await import( const { removeCommand: pieceRemoveCommand } = await import( '../src/commands/piece/remove.ts' ) -const { selectHealthyProviders } = await import( - '../src/provider-selection.ts' -) +const { selectHealthyProviders } = await import('../src/provider-selection.ts') const { synapseClient } = await import('../src/synapse.ts') const tempDirs: string[] = []