From 7d643a0f63b3c3b3318c409e736a242f1ed88f1e Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 15 Mar 2026 13:57:39 +0000 Subject: [PATCH 1/3] [#34] Scaffold plotlink-cli with all commands Adds packages/cli/ with commander.js-based CLI that wraps @plotlink/sdk: - plotlink create: upload content to IPFS and create storyline - plotlink chain: upload content and chain plot onto storyline - plotlink status: query storyline data and list plots - plotlink claim: claim royalties for a storyline token - plotlink agent register: register agent on ERC-8004 and set wallet Config loaded from env vars (PLOTLINK_PRIVATE_KEY, PLOTLINK_RPC_URL, etc.) or .plotlinkrc JSON file. Adds npm workspaces to root package.json. Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 481 ++++++++++++++++++++ package.json | 3 + packages/cli/package.json | 26 ++ packages/cli/src/commands/agent-register.ts | 64 +++ packages/cli/src/commands/chain.ts | 28 ++ packages/cli/src/commands/claim.ts | 33 ++ packages/cli/src/commands/create.ts | 35 ++ packages/cli/src/commands/status.ts | 44 ++ packages/cli/src/config.ts | 92 ++++ packages/cli/src/index.ts | 21 + packages/cli/src/sdk.ts | 36 ++ packages/cli/tsconfig.json | 20 + packages/cli/tsup.config.ts | 14 + 13 files changed, 897 insertions(+) create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/commands/agent-register.ts create mode 100644 packages/cli/src/commands/chain.ts create mode 100644 packages/cli/src/commands/claim.ts create mode 100644 packages/cli/src/commands/create.ts create mode 100644 packages/cli/src/commands/status.ts create mode 100644 packages/cli/src/config.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/sdk.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/cli/tsup.config.ts diff --git a/package-lock.json b/package-lock.json index 88d3390c..7c1cc7ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "plotlink", "version": "0.1.0", + "workspaces": [ + "packages/*" + ], "dependencies": { "@aws-sdk/client-s3": "^3.1009.0", "@supabase/supabase-js": "^2.99.1", @@ -2570,6 +2573,10 @@ "node": ">=12.4.0" } }, + "node_modules/@plotlink/sdk": { + "resolved": "packages/sdk", + "link": true + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -4963,6 +4970,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5286,6 +5300,22 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -5420,6 +5450,22 @@ "node": ">= 16" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -5446,6 +5492,15 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5453,6 +5508,23 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6524,6 +6596,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -7414,6 +7498,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7802,6 +7896,36 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7942,6 +8066,19 @@ } } }, + "node_modules/mlly": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", + "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7949,6 +8086,18 @@ "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -8409,6 +8558,32 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/plotlink-cli": { + "resolved": "packages/cli", + "link": true + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8448,6 +8623,49 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8529,6 +8747,20 @@ "dev": true, "license": "MIT" }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8978,6 +9210,16 @@ "dev": true, "license": "ISC" }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -9213,6 +9455,39 @@ } } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9260,6 +9535,29 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -9365,6 +9663,16 @@ "node": ">=8.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -9378,6 +9686,13 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -9539,6 +9854,13 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -10211,6 +10533,165 @@ "optional": true } } + }, + "packages/cli": { + "name": "plotlink-cli", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@plotlink/sdk": "*", + "commander": "^13.1.0", + "viem": "^2.47.2" + }, + "bin": { + "plotlink": "dist/index.js" + }, + "devDependencies": { + "tsup": "^8.4.0", + "typescript": "^5" + } + }, + "packages/cli/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/cli/node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "packages/sdk": { + "name": "@plotlink/sdk", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-s3": "^3.1009.0" + }, + "devDependencies": { + "eslint": "^9", + "tsup": "^8.4.0", + "typescript": "^5" + }, + "peerDependencies": { + "viem": "^2.0.0" + } + }, + "packages/sdk/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/sdk/node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index d7ec715d..ebffd042 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "plotlink", "version": "0.1.0", "private": true, + "workspaces": [ + "packages/*" + ], "scripts": { "dev": "next dev", "build": "next build", diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..1d0c9579 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,26 @@ +{ + "name": "plotlink-cli", + "version": "0.1.0", + "description": "CLI for the PlotLink protocol — create storylines, chain plots, register agents, and claim royalties.", + "type": "module", + "bin": { + "plotlink": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@plotlink/sdk": "*", + "commander": "^13.1.0", + "viem": "^2.47.2" + }, + "devDependencies": { + "tsup": "^8.4.0", + "typescript": "^5" + }, + "license": "MIT" +} diff --git a/packages/cli/src/commands/agent-register.ts b/packages/cli/src/commands/agent-register.ts new file mode 100644 index 00000000..80b066ee --- /dev/null +++ b/packages/cli/src/commands/agent-register.ts @@ -0,0 +1,64 @@ +import type { Command } from "commander"; +import type { Address } from "viem"; +import { buildClient } from "../sdk.js"; + +export function registerAgentRegister(program: Command): void { + const agent = program + .command("agent") + .description("Agent identity commands"); + + agent + .command("register") + .description("Register an AI agent on the ERC-8004 registry and set its wallet") + .requiredOption("-n, --name ", "Agent display name") + .requiredOption("-d, --description ", "Short description of the agent") + .requiredOption("-g, --genre ", "Primary genre the agent writes in") + .requiredOption("-m, --model ", "LLM model identifier (e.g. \"Claude Opus 4\")") + .option("-w, --wallet
", "Agent wallet address (defaults to the configured wallet)") + .option("--wallet-key ", "Agent wallet private key for EIP-712 signature (required if --wallet is set)") + .action( + async (opts: { + name: string; + description: string; + genre: string; + model: string; + wallet?: string; + walletKey?: string; + }) => { + try { + const client = buildClient({ ipfs: false }); + + console.log(`Registering agent "${opts.name}"...`); + const result = await client.registerAgent( + opts.name, + opts.description, + opts.genre, + opts.model, + ); + + console.log("Agent registered!"); + console.log(` Agent ID: ${result.agentId}`); + console.log(` TX: ${result.txHash}`); + + if (opts.wallet) { + if (!opts.walletKey) { + console.error("Error: --wallet-key is required when --wallet is set"); + process.exit(1); + } + + const walletAddress = opts.wallet as Address; + console.log(`Setting agent wallet to ${walletAddress}...`); + const walletResult = await client.setAgentWallet( + result.agentId, + walletAddress, + opts.walletKey, + ); + console.log(`Agent wallet set! TX: ${walletResult.txHash}`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + }, + ); +} diff --git a/packages/cli/src/commands/chain.ts b/packages/cli/src/commands/chain.ts new file mode 100644 index 00000000..b90d7a70 --- /dev/null +++ b/packages/cli/src/commands/chain.ts @@ -0,0 +1,28 @@ +import { readFileSync } from "node:fs"; +import type { Command } from "commander"; +import { buildClient } from "../sdk.js"; + +export function registerChain(program: Command): void { + program + .command("chain") + .description("Chain a new plot onto an existing storyline") + .requiredOption("-s, --storyline ", "Storyline ID") + .requiredOption("-f, --file ", "Path to content file (plain text)") + .action(async (opts: { storyline: string; file: string }) => { + try { + const content = readFileSync(opts.file, "utf-8"); + const storylineId = BigInt(opts.storyline); + const client = buildClient({ ipfs: true }); + + console.log(`Chaining plot onto storyline ${storylineId}...`); + const result = await client.chainPlot(storylineId, content); + + console.log("Plot chained!"); + console.log(` TX: ${result.txHash}`); + console.log(` CID: ${result.contentCid}`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + }); +} diff --git a/packages/cli/src/commands/claim.ts b/packages/cli/src/commands/claim.ts new file mode 100644 index 00000000..e6150bb8 --- /dev/null +++ b/packages/cli/src/commands/claim.ts @@ -0,0 +1,33 @@ +import type { Command } from "commander"; +import type { Address } from "viem"; +import { buildClient } from "../sdk.js"; + +export function registerClaim(program: Command): void { + program + .command("claim") + .description("Claim accumulated royalties for a storyline token") + .requiredOption("-a, --address ", "Storyline ERC-20 token address") + .action(async (opts: { address: string }) => { + try { + const tokenAddress = opts.address as Address; + const client = buildClient({ ipfs: false }); + + console.log("Checking royalties..."); + const info = await client.getRoyaltyInfo(tokenAddress); + console.log(` Unclaimed: ${info.unclaimed}`); + console.log(` Beneficiary: ${info.beneficiary}`); + + if (info.unclaimed === 0n) { + console.log("No royalties to claim."); + return; + } + + console.log("Claiming royalties..."); + const txHash = await client.claimRoyalties(tokenAddress); + console.log(`Royalties claimed! TX: ${txHash}`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + }); +} diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts new file mode 100644 index 00000000..59146969 --- /dev/null +++ b/packages/cli/src/commands/create.ts @@ -0,0 +1,35 @@ +import { readFileSync } from "node:fs"; +import type { Command } from "commander"; +import { buildClient } from "../sdk.js"; + +export function registerCreate(program: Command): void { + program + .command("create") + .description("Create a new storyline from a content file") + .requiredOption("-t, --title ", "Storyline title") + .requiredOption("-f, --file <path>", "Path to content file (plain text)") + .requiredOption("-g, --genre <genre>", "Genre label") + .option("-d, --deadline", "Enable sunset deadline", false) + .action(async (opts: { title: string; file: string; genre: string; deadline: boolean }) => { + try { + const content = readFileSync(opts.file, "utf-8"); + const client = buildClient({ ipfs: true }); + + console.log(`Creating storyline "${opts.title}"...`); + const result = await client.createStoryline( + opts.title, + content, + opts.genre, + opts.deadline, + ); + + console.log("Storyline created!"); + console.log(` ID: ${result.storylineId}`); + console.log(` TX: ${result.txHash}`); + console.log(` CID: ${result.contentCid}`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + }); +} diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts new file mode 100644 index 00000000..13722d25 --- /dev/null +++ b/packages/cli/src/commands/status.ts @@ -0,0 +1,44 @@ +import type { Command } from "commander"; +import { buildClient } from "../sdk.js"; + +export function registerStatus(program: Command): void { + program + .command("status") + .description("Query storyline data (plots, deadline, token address)") + .requiredOption("-s, --storyline <id>", "Storyline ID") + .action(async (opts: { storyline: string }) => { + try { + const storylineId = BigInt(opts.storyline); + const client = buildClient({ ipfs: false }); + + console.log(`Fetching storyline ${storylineId}...`); + + const info = await client.getStoryline(storylineId); + if (!info) { + console.error(`Storyline ${storylineId} not found.`); + process.exit(1); + } + + const plots = await client.getPlots(storylineId); + + console.log(); + console.log(`Title: ${info.title}`); + console.log(`Creator: ${info.creator}`); + console.log(`Token: ${info.tokenAddress}`); + console.log(`Has deadline: ${info.hasDeadline ? "yes" : "no"}`); + console.log(`Opening CID: ${info.openingCID}`); + console.log(`Plot count: ${plots.length}`); + + if (plots.length > 0) { + console.log(); + console.log("Plots:"); + for (const plot of plots) { + console.log(` #${plot.plotIndex} by ${plot.writer} — CID: ${plot.contentCID}`); + } + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + }); +} diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts new file mode 100644 index 00000000..55a4f043 --- /dev/null +++ b/packages/cli/src/config.ts @@ -0,0 +1,92 @@ +import { readFileSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { homedir } from "node:os"; + +/** + * Resolved configuration for the CLI. + * Loaded from environment variables, falling back to `.plotlinkrc` in cwd or home dir. + */ +export interface CliConfig { + privateKey: string; + rpcUrl: string; + chainId?: number; + filebaseAccessKey?: string; + filebaseSecretKey?: string; + filebaseBucket?: string; +} + +/** + * Load CLI config by merging env vars over `.plotlinkrc` values. + * + * Priority (highest first): + * 1. Environment variables (PLOTLINK_PRIVATE_KEY, PLOTLINK_RPC_URL, etc.) + * 2. `.plotlinkrc` JSON file in cwd + * 3. `.plotlinkrc` JSON file in home dir + */ +export function loadConfig(): CliConfig { + const rc = loadRcFile(); + + const privateKey = env("PLOTLINK_PRIVATE_KEY") ?? rc.privateKey; + const rpcUrl = env("PLOTLINK_RPC_URL") ?? rc.rpcUrl; + + if (!privateKey) { + throw new Error( + "Missing private key. Set PLOTLINK_PRIVATE_KEY env var or add \"privateKey\" to .plotlinkrc", + ); + } + if (!rpcUrl) { + throw new Error( + "Missing RPC URL. Set PLOTLINK_RPC_URL env var or add \"rpcUrl\" to .plotlinkrc", + ); + } + + const chainIdRaw = env("PLOTLINK_CHAIN_ID") ?? rc.chainId; + const chainId = chainIdRaw ? Number(chainIdRaw) : undefined; + + return { + privateKey, + rpcUrl, + chainId, + filebaseAccessKey: env("PLOTLINK_FILEBASE_ACCESS_KEY") ?? rc.filebaseAccessKey, + filebaseSecretKey: env("PLOTLINK_FILEBASE_SECRET_KEY") ?? rc.filebaseSecretKey, + filebaseBucket: env("PLOTLINK_FILEBASE_BUCKET") ?? rc.filebaseBucket, + }; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function env(name: string): string | undefined { + const val = process.env[name]; + return val && val.trim().length > 0 ? val.trim() : undefined; +} + +interface RcData { + privateKey?: string; + rpcUrl?: string; + chainId?: string; + filebaseAccessKey?: string; + filebaseSecretKey?: string; + filebaseBucket?: string; +} + +function loadRcFile(): RcData { + const candidates = [ + resolve(process.cwd(), ".plotlinkrc"), + resolve(homedir(), ".plotlinkrc"), + ]; + + for (const filepath of candidates) { + if (existsSync(filepath)) { + try { + const raw = readFileSync(filepath, "utf-8"); + return JSON.parse(raw) as RcData; + } catch { + // Ignore malformed rc files + } + } + } + + return {}; +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 00000000..c7f9ecdc --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,21 @@ +import { Command } from "commander"; +import { registerCreate } from "./commands/create.js"; +import { registerChain } from "./commands/chain.js"; +import { registerStatus } from "./commands/status.js"; +import { registerClaim } from "./commands/claim.js"; +import { registerAgentRegister } from "./commands/agent-register.js"; + +const program = new Command(); + +program + .name("plotlink") + .description("CLI for the PlotLink protocol on Base") + .version("0.1.0"); + +registerCreate(program); +registerChain(program); +registerStatus(program); +registerClaim(program); +registerAgentRegister(program); + +program.parse(); diff --git a/packages/cli/src/sdk.ts b/packages/cli/src/sdk.ts new file mode 100644 index 00000000..ba442edf --- /dev/null +++ b/packages/cli/src/sdk.ts @@ -0,0 +1,36 @@ +import { PlotLink } from "@plotlink/sdk"; +import type { PlotLinkConfig } from "@plotlink/sdk"; +import { loadConfig } from "./config.js"; + +/** + * Build a PlotLink SDK client from CLI config. + * + * @param options.ipfs - Whether IPFS (Filebase) credentials are required. + * Commands that upload content (create, chain) need this; read-only commands don't. + */ +export function buildClient(options: { ipfs: boolean }): PlotLink { + const cfg = loadConfig(); + + const config: PlotLinkConfig = { + privateKey: cfg.privateKey, + rpcUrl: cfg.rpcUrl, + chainId: cfg.chainId, + }; + + if (options.ipfs) { + if (!cfg.filebaseAccessKey || !cfg.filebaseSecretKey || !cfg.filebaseBucket) { + throw new Error( + "Filebase credentials required for this command. " + + "Set PLOTLINK_FILEBASE_ACCESS_KEY, PLOTLINK_FILEBASE_SECRET_KEY, " + + "and PLOTLINK_FILEBASE_BUCKET env vars or add them to .plotlinkrc", + ); + } + config.filebase = { + accessKey: cfg.filebaseAccessKey, + secretKey: cfg.filebaseSecretKey, + bucket: cfg.filebaseBucket, + }; + } + + return new PlotLink(config); +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 00000000..1358d953 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2021", + "lib": ["ES2021"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "noEmit": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts new file mode 100644 index 00000000..c2b854b4 --- /dev/null +++ b/packages/cli/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: false, + splitting: false, + sourcemap: true, + clean: true, + outDir: "dist", + banner: { + js: "#!/usr/bin/env node", + }, +}); From 45bd552174b0d110c128be386813dd247bf02832 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi <cyh76507707@gmail.com> Date: Sun, 15 Mar 2026 14:01:46 +0000 Subject: [PATCH 2/3] [#34] Add Supabase status query and token price to CLI status command Status command now queries Supabase for indexed storyline metadata (plot count, last plot time, sunset, writer type, creation date) and MCV2_Bond for current token price via new SDK getTokenPrice() method. Falls back to event-derived plot count when Supabase is not configured. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- package-lock.json | 1 + packages/cli/package.json | 1 + packages/cli/src/commands/status.ts | 98 ++++++++++++++++++++++++----- packages/cli/src/config.ts | 6 ++ packages/sdk/src/client.ts | 40 ++++++++++++ packages/sdk/src/index.ts | 1 + 6 files changed, 132 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c1cc7ee..869c4f3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10540,6 +10540,7 @@ "license": "MIT", "dependencies": { "@plotlink/sdk": "*", + "@supabase/supabase-js": "^2.49.4", "commander": "^13.1.0", "viem": "^2.47.2" }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 1d0c9579..dd661f85 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@plotlink/sdk": "*", + "@supabase/supabase-js": "^2.49.4", "commander": "^13.1.0", "viem": "^2.47.2" }, diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts index 13722d25..cd84e660 100644 --- a/packages/cli/src/commands/status.ts +++ b/packages/cli/src/commands/status.ts @@ -1,41 +1,109 @@ import type { Command } from "commander"; +import { createClient } from "@supabase/supabase-js"; +import { type Address, formatUnits } from "viem"; import { buildClient } from "../sdk.js"; +import { loadConfig } from "../config.js"; export function registerStatus(program: Command): void { program .command("status") - .description("Query storyline data (plots, deadline, token address)") + .description("Query storyline data (plots, deadline, token price) from Supabase and on-chain") .requiredOption("-s, --storyline <id>", "Storyline ID") .action(async (opts: { storyline: string }) => { try { const storylineId = BigInt(opts.storyline); + const cfg = loadConfig(); const client = buildClient({ ipfs: false }); console.log(`Fetching storyline ${storylineId}...`); + // ----------------------------------------------------------------- + // 1. On-chain event data (always available) + // ----------------------------------------------------------------- const info = await client.getStoryline(storylineId); if (!info) { - console.error(`Storyline ${storylineId} not found.`); + console.error(`Storyline ${storylineId} not found on-chain.`); process.exit(1); } - const plots = await client.getPlots(storylineId); + // ----------------------------------------------------------------- + // 2. Supabase metadata (optional — richer data when configured) + // ----------------------------------------------------------------- + let dbRow: { + plot_count: number; + last_plot_time: string | null; + has_deadline: boolean; + sunset: boolean; + writer_type: number | null; + block_timestamp: string | null; + } | null = null; + if (cfg.supabaseUrl && cfg.supabaseAnonKey) { + const supabase = createClient(cfg.supabaseUrl, cfg.supabaseAnonKey); + const { data } = await supabase + .from("storylines") + .select("plot_count, last_plot_time, has_deadline, sunset, writer_type, block_timestamp") + .eq("storyline_id", Number(storylineId)) + .single(); + dbRow = data; + } + + // ----------------------------------------------------------------- + // 3. On-chain token price (MCV2_Bond) + // ----------------------------------------------------------------- + const tokenPrice = await client.getTokenPrice(info.tokenAddress); + + // ----------------------------------------------------------------- + // 4. On-chain royalty info + // ----------------------------------------------------------------- + let unclaimedRoyalty: bigint | null = null; + try { + const royalty = await client.getRoyaltyInfo(info.tokenAddress); + unclaimedRoyalty = royalty.unclaimed; + } catch { + // Token may not have a bond yet + } + + // ----------------------------------------------------------------- + // 5. Fall back to event-derived plot count if no Supabase + // ----------------------------------------------------------------- + let plotCount: number; + if (dbRow) { + plotCount = dbRow.plot_count; + } else { + const plots = await client.getPlots(storylineId); + plotCount = plots.length; + } + + // ----------------------------------------------------------------- + // Display + // ----------------------------------------------------------------- console.log(); - console.log(`Title: ${info.title}`); - console.log(`Creator: ${info.creator}`); - console.log(`Token: ${info.tokenAddress}`); - console.log(`Has deadline: ${info.hasDeadline ? "yes" : "no"}`); - console.log(`Opening CID: ${info.openingCID}`); - console.log(`Plot count: ${plots.length}`); - - if (plots.length > 0) { - console.log(); - console.log("Plots:"); - for (const plot of plots) { - console.log(` #${plot.plotIndex} by ${plot.writer} — CID: ${plot.contentCID}`); + console.log(`Title: ${info.title}`); + console.log(`Creator: ${info.creator}`); + console.log(`Token: ${info.tokenAddress}`); + console.log(`Has deadline: ${info.hasDeadline ? "yes" : "no"}`); + console.log(`Opening CID: ${info.openingCID}`); + console.log(`Plot count: ${plotCount}`); + + if (dbRow) { + console.log(`Sunset: ${dbRow.sunset ? "yes" : "no"}`); + console.log(`Writer type: ${dbRow.writer_type === 1 ? "agent" : dbRow.writer_type === 0 ? "human" : "unknown"}`); + if (dbRow.block_timestamp) { + console.log(`Created: ${new Date(dbRow.block_timestamp).toISOString()}`); + } + if (dbRow.last_plot_time) { + console.log(`Last plot: ${new Date(dbRow.last_plot_time).toISOString()}`); } } + + if (tokenPrice) { + console.log(`Token price: ${tokenPrice.priceFormatted} ETH`); + } + + if (unclaimedRoyalty !== null && unclaimedRoyalty > 0n) { + console.log(`Unclaimed royalty: ${formatUnits(unclaimedRoyalty, 18)} ETH`); + } } catch (err) { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 55a4f043..10530285 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -13,6 +13,8 @@ export interface CliConfig { filebaseAccessKey?: string; filebaseSecretKey?: string; filebaseBucket?: string; + supabaseUrl?: string; + supabaseAnonKey?: string; } /** @@ -50,6 +52,8 @@ export function loadConfig(): CliConfig { filebaseAccessKey: env("PLOTLINK_FILEBASE_ACCESS_KEY") ?? rc.filebaseAccessKey, filebaseSecretKey: env("PLOTLINK_FILEBASE_SECRET_KEY") ?? rc.filebaseSecretKey, filebaseBucket: env("PLOTLINK_FILEBASE_BUCKET") ?? rc.filebaseBucket, + supabaseUrl: env("PLOTLINK_SUPABASE_URL") ?? rc.supabaseUrl, + supabaseAnonKey: env("PLOTLINK_SUPABASE_ANON_KEY") ?? rc.supabaseAnonKey, }; } @@ -69,6 +73,8 @@ interface RcData { filebaseAccessKey?: string; filebaseSecretKey?: string; filebaseBucket?: string; + supabaseUrl?: string; + supabaseAnonKey?: string; } function loadRcFile(): RcData { diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index ab9acbd2..472589d7 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -5,6 +5,7 @@ import { keccak256, toHex, decodeEventLog, + formatUnits, type PublicClient, type WalletClient, type Address, @@ -103,6 +104,13 @@ export interface RoyaltyInfo { beneficiary: Address; } +export interface TokenPriceInfo { + /** Cost (in reserve token wei) to mint 1 unit of the storyline token. */ + priceRaw: bigint; + /** priceRaw formatted with 18 decimals. */ + priceFormatted: string; +} + // --------------------------------------------------------------------------- // Client // --------------------------------------------------------------------------- @@ -525,6 +533,38 @@ export class PlotLink { return txHash; } + // ------------------------------------------------------------------------- + // Price methods + // ------------------------------------------------------------------------- + + /** + * Get the current bonding-curve price for a storyline token. + * + * Calls MCV2_Bond.priceForNextMint() to get the cost (in reserve token) + * to mint 1 unit of the given storyline token. + * + * @param tokenAddress - The storyline's ERC-20 token address + * @returns Price info or null if the token has no bond / query fails + */ + async getTokenPrice(tokenAddress: Address): Promise<TokenPriceInfo | null> { + try { + const result = await this.publicClient.readContract({ + address: this.mcv2Bond, + abi: mcv2BondAbi, + functionName: "priceForNextMint", + args: [tokenAddress], + }); + + const priceRaw = BigInt(result as bigint); + return { + priceRaw, + priceFormatted: formatUnits(priceRaw, 18), + }; + } catch { + return null; + } + } + // ------------------------------------------------------------------------- // Internal helpers // ------------------------------------------------------------------------- diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 53fa7dfe..6addc1f0 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -29,6 +29,7 @@ export type { RegisterAgentResult, SetAgentWalletResult, RoyaltyInfo, + TokenPriceInfo, } from "./client"; export type { FilebaseConfig } from "./ipfs"; From 3d08bbbf77a7ff64835c38c4b933e65f20d0d84a Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi <cyh76507707@gmail.com> Date: Sun, 15 Mar 2026 14:04:28 +0000 Subject: [PATCH 3/3] [#34] Add deadline remaining display, fix agent wallet key handling - status: compute human-readable deadline remaining (e.g. "2d 5h remaining" or "expired") from last_plot_time using the 72h sunset window - agent-register: remove --wallet-key CLI flag (leaked into shell history); read key from PLOTLINK_AGENT_WALLET_KEY env var instead; validate presence BEFORE registerAgent() spends gas; add --skip-wallet flag Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- packages/cli/src/commands/agent-register.ts | 26 ++++++++++++++------- packages/cli/src/commands/status.ts | 21 +++++++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/commands/agent-register.ts b/packages/cli/src/commands/agent-register.ts index 80b066ee..1efd30bb 100644 --- a/packages/cli/src/commands/agent-register.ts +++ b/packages/cli/src/commands/agent-register.ts @@ -15,7 +15,7 @@ export function registerAgentRegister(program: Command): void { .requiredOption("-g, --genre <genre>", "Primary genre the agent writes in") .requiredOption("-m, --model <model>", "LLM model identifier (e.g. \"Claude Opus 4\")") .option("-w, --wallet <address>", "Agent wallet address (defaults to the configured wallet)") - .option("--wallet-key <privateKey>", "Agent wallet private key for EIP-712 signature (required if --wallet is set)") + .option("--skip-wallet", "Skip wallet binding step", false) .action( async (opts: { name: string; @@ -23,9 +23,22 @@ export function registerAgentRegister(program: Command): void { genre: string; model: string; wallet?: string; - walletKey?: string; + skipWallet?: boolean; }) => { try { + // Resolve wallet key from env BEFORE spending gas on registration + let walletKey: string | undefined; + if (opts.wallet && !opts.skipWallet) { + walletKey = process.env.PLOTLINK_AGENT_WALLET_KEY; + if (!walletKey) { + console.error( + "Error: PLOTLINK_AGENT_WALLET_KEY env var is required when --wallet is set.\n" + + "Set it before running this command, or pass --skip-wallet to register without binding.", + ); + process.exit(1); + } + } + const client = buildClient({ ipfs: false }); console.log(`Registering agent "${opts.name}"...`); @@ -40,18 +53,13 @@ export function registerAgentRegister(program: Command): void { console.log(` Agent ID: ${result.agentId}`); console.log(` TX: ${result.txHash}`); - if (opts.wallet) { - if (!opts.walletKey) { - console.error("Error: --wallet-key is required when --wallet is set"); - process.exit(1); - } - + if (opts.wallet && !opts.skipWallet && walletKey) { const walletAddress = opts.wallet as Address; console.log(`Setting agent wallet to ${walletAddress}...`); const walletResult = await client.setAgentWallet( result.agentId, walletAddress, - opts.walletKey, + walletKey, ); console.log(`Agent wallet set! TX: ${walletResult.txHash}`); } diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts index cd84e660..c3e821b4 100644 --- a/packages/cli/src/commands/status.ts +++ b/packages/cli/src/commands/status.ts @@ -95,6 +95,27 @@ export function registerStatus(program: Command): void { if (dbRow.last_plot_time) { console.log(`Last plot: ${new Date(dbRow.last_plot_time).toISOString()}`); } + + // Deadline remaining (72h from last plot) + if (dbRow.has_deadline && dbRow.last_plot_time && !dbRow.sunset) { + const DEADLINE_HOURS = 72; + const deadlineMs = + new Date(dbRow.last_plot_time).getTime() + DEADLINE_HOURS * 60 * 60 * 1000; + const remainingMs = deadlineMs - Date.now(); + if (remainingMs <= 0) { + console.log(`Deadline: expired`); + } else { + const totalMin = Math.floor(remainingMs / 60_000); + const days = Math.floor(totalMin / 1440); + const hours = Math.floor((totalMin % 1440) / 60); + const mins = totalMin % 60; + const parts: string[] = []; + if (days > 0) parts.push(`${days}d`); + if (hours > 0) parts.push(`${hours}h`); + if (mins > 0 || parts.length === 0) parts.push(`${mins}m`); + console.log(`Deadline: ${parts.join(" ")} remaining`); + } + } } if (tokenPrice) {