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/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..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", @@ -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" }, 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/multi-upload.ts b/cli/src/commands/multi-upload.ts index 394e622..003e4bf 100644 --- a/cli/src/commands/multi-upload.ts +++ b/cli/src/commands/multi-upload.ts @@ -1,11 +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, @@ -89,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') @@ -132,11 +132,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/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/src/commands/upload.ts b/cli/src/commands/upload.ts index 8d50ac5..b94a082 100644 --- a/cli/src/commands/upload.ts +++ b/cli/src/commands/upload.ts @@ -1,10 +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 = { @@ -75,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') @@ -88,11 +88,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/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 411f7c4..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', @@ -20,6 +19,7 @@ export const costsCommand = { newPerMonthRate: z.string(), depositNeeded: z.string(), alreadyCovered: z.boolean(), + needsFwssMaxApproval: z.boolean(), }), examples: [ { @@ -33,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), @@ -50,8 +48,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/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/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/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/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/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: { 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/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/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/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 diff --git a/cli/tests/command-mocks.ts b/cli/tests/command-mocks.ts index ca76df0..965e015 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 () => ({ @@ -213,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, @@ -244,6 +292,7 @@ mock.module('@filoz/synapse-core/warm-storage', () => ({ getPdpDataSet, getPdpDataSets, terminateServiceSync, + fetchProviderSelectionInput, })) mock.module('@filoz/synapse-core/pdp-verifier', () => ({ @@ -277,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, @@ -320,6 +373,7 @@ export function resetCommandMocks() { }, depositNeeded: 222n, ready: true, + needsFwssMaxApproval: false, }, })) synapseStorage.upload.mockImplementation(async () => ({ @@ -332,14 +386,20 @@ 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/output.test.ts b/cli/tests/output.test.ts index 7cb2e4b..3e05628 100644 --- a/cli/tests/output.test.ts +++ b/cli/tests/output.test.ts @@ -37,11 +37,23 @@ 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 +89,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 +99,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 998ef50..5fa5b72 100644 --- a/cli/tests/synapse-commands.test.ts +++ b/cli/tests/synapse-commands.test.ts @@ -6,10 +6,13 @@ import { calculate, cid, claimTokens, + configStore, createDataSet, createDataSetAndAddPieces, fakeProvider, fakeWalletClient, + fetchMock, + fetchProviderSelectionInput, findPiece, formatBalance, getAccountSummary, @@ -67,6 +70,8 @@ const { listCommand: pieceListCommand } = await import( const { removeCommand: pieceRemoveCommand } = await import( '../src/commands/piece/remove.ts' ) +const { selectHealthyProviders } = await import('../src/provider-selection.ts') +const { synapseClient } = await import('../src/synapse.ts') const tempDirs: string[] = [] @@ -87,8 +92,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 } : {}), + } }, } } @@ -166,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({ @@ -476,6 +495,7 @@ describe('wallet commands', () => { newPerMonthRate: 'formatted:111', depositNeeded: 'formatted:222', alreadyCovered: true, + needsFwssMaxApproval: false, processLog: [{ step: 'Getting costs', status: 'done' }], }) }) @@ -511,7 +531,6 @@ describe('wallet commands', () => { totalLockup: 'formatted:2', rateBasedLockup: 'formatted:3', monthlyAccountRate: 'formatted:4', - monthlyStorageRate: 'formatted:4', funds: 'formatted:5', }) }) @@ -568,7 +587,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', @@ -670,6 +692,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', @@ -692,6 +716,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 () => { @@ -720,6 +745,7 @@ describe('dataset commands', () => { metadata: {}, }, ], + hasMore: false, })) const result = await datasetDetailsCommand.run( @@ -736,6 +762,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', () => { @@ -750,6 +808,8 @@ describe('piece commands', () => { expect(getPiecesWithMetadata).toHaveBeenCalledWith(fakeWalletClient, { dataSet: expect.anything(), address: fakeWalletClient.account.address, + offset: 0n, + limit: 100n, }) expect(result).toMatchObject({ dataSetId: '42', @@ -763,6 +823,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 () => { @@ -795,3 +892,95 @@ 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/ + ) + }) +}) + +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 8aa48df..0c9a004 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 @@ -30,19 +30,22 @@ 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 -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 +55,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 +84,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 +100,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 +135,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` |