arc402-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/README.md +245 -0
  2. package/dist/abis.d.ts +19 -0
  3. package/dist/abis.d.ts.map +1 -0
  4. package/dist/abis.js +177 -0
  5. package/dist/abis.js.map +1 -0
  6. package/dist/bundler.d.ts +65 -0
  7. package/dist/bundler.d.ts.map +1 -0
  8. package/dist/bundler.js +181 -0
  9. package/dist/bundler.js.map +1 -0
  10. package/dist/client.d.ts +14 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +24 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/coinbase-smart-wallet.d.ts +28 -0
  15. package/dist/coinbase-smart-wallet.d.ts.map +1 -0
  16. package/dist/coinbase-smart-wallet.js +38 -0
  17. package/dist/coinbase-smart-wallet.js.map +1 -0
  18. package/dist/commands/accept.d.ts +3 -0
  19. package/dist/commands/accept.d.ts.map +1 -0
  20. package/dist/commands/accept.js +26 -0
  21. package/dist/commands/accept.js.map +1 -0
  22. package/dist/commands/agent-handshake.d.ts +3 -0
  23. package/dist/commands/agent-handshake.d.ts.map +1 -0
  24. package/dist/commands/agent-handshake.js +61 -0
  25. package/dist/commands/agent-handshake.js.map +1 -0
  26. package/dist/commands/agent.d.ts +3 -0
  27. package/dist/commands/agent.d.ts.map +1 -0
  28. package/dist/commands/agent.js +417 -0
  29. package/dist/commands/agent.js.map +1 -0
  30. package/dist/commands/agreements.d.ts +3 -0
  31. package/dist/commands/agreements.d.ts.map +1 -0
  32. package/dist/commands/agreements.js +344 -0
  33. package/dist/commands/agreements.js.map +1 -0
  34. package/dist/commands/arbitrator.d.ts +3 -0
  35. package/dist/commands/arbitrator.d.ts.map +1 -0
  36. package/dist/commands/arbitrator.js +157 -0
  37. package/dist/commands/arbitrator.js.map +1 -0
  38. package/dist/commands/arena-handshake.d.ts +3 -0
  39. package/dist/commands/arena-handshake.d.ts.map +1 -0
  40. package/dist/commands/arena-handshake.js +187 -0
  41. package/dist/commands/arena-handshake.js.map +1 -0
  42. package/dist/commands/cancel.d.ts +3 -0
  43. package/dist/commands/cancel.d.ts.map +1 -0
  44. package/dist/commands/cancel.js +30 -0
  45. package/dist/commands/cancel.js.map +1 -0
  46. package/dist/commands/channel.d.ts +3 -0
  47. package/dist/commands/channel.d.ts.map +1 -0
  48. package/dist/commands/channel.js +238 -0
  49. package/dist/commands/channel.js.map +1 -0
  50. package/dist/commands/coldstart.d.ts +3 -0
  51. package/dist/commands/coldstart.d.ts.map +1 -0
  52. package/dist/commands/coldstart.js +148 -0
  53. package/dist/commands/coldstart.js.map +1 -0
  54. package/dist/commands/config.d.ts +3 -0
  55. package/dist/commands/config.d.ts.map +1 -0
  56. package/dist/commands/config.js +40 -0
  57. package/dist/commands/config.js.map +1 -0
  58. package/dist/commands/contract-interaction.d.ts +3 -0
  59. package/dist/commands/contract-interaction.d.ts.map +1 -0
  60. package/dist/commands/contract-interaction.js +165 -0
  61. package/dist/commands/contract-interaction.js.map +1 -0
  62. package/dist/commands/daemon.d.ts +3 -0
  63. package/dist/commands/daemon.d.ts.map +1 -0
  64. package/dist/commands/daemon.js +891 -0
  65. package/dist/commands/daemon.js.map +1 -0
  66. package/dist/commands/deliver.d.ts +3 -0
  67. package/dist/commands/deliver.d.ts.map +1 -0
  68. package/dist/commands/deliver.js +156 -0
  69. package/dist/commands/deliver.js.map +1 -0
  70. package/dist/commands/discover.d.ts +3 -0
  71. package/dist/commands/discover.d.ts.map +1 -0
  72. package/dist/commands/discover.js +224 -0
  73. package/dist/commands/discover.js.map +1 -0
  74. package/dist/commands/dispute.d.ts +3 -0
  75. package/dist/commands/dispute.d.ts.map +1 -0
  76. package/dist/commands/dispute.js +348 -0
  77. package/dist/commands/dispute.js.map +1 -0
  78. package/dist/commands/endpoint.d.ts +3 -0
  79. package/dist/commands/endpoint.d.ts.map +1 -0
  80. package/dist/commands/endpoint.js +604 -0
  81. package/dist/commands/endpoint.js.map +1 -0
  82. package/dist/commands/hire.d.ts +3 -0
  83. package/dist/commands/hire.d.ts.map +1 -0
  84. package/dist/commands/hire.js +189 -0
  85. package/dist/commands/hire.js.map +1 -0
  86. package/dist/commands/migrate.d.ts +3 -0
  87. package/dist/commands/migrate.d.ts.map +1 -0
  88. package/dist/commands/migrate.js +163 -0
  89. package/dist/commands/migrate.js.map +1 -0
  90. package/dist/commands/negotiate.d.ts +3 -0
  91. package/dist/commands/negotiate.d.ts.map +1 -0
  92. package/dist/commands/negotiate.js +247 -0
  93. package/dist/commands/negotiate.js.map +1 -0
  94. package/dist/commands/openshell.d.ts +3 -0
  95. package/dist/commands/openshell.d.ts.map +1 -0
  96. package/dist/commands/openshell.js +952 -0
  97. package/dist/commands/openshell.js.map +1 -0
  98. package/dist/commands/owner.d.ts +3 -0
  99. package/dist/commands/owner.d.ts.map +1 -0
  100. package/dist/commands/owner.js +32 -0
  101. package/dist/commands/owner.js.map +1 -0
  102. package/dist/commands/policy.d.ts +4 -0
  103. package/dist/commands/policy.d.ts.map +1 -0
  104. package/dist/commands/policy.js +248 -0
  105. package/dist/commands/policy.js.map +1 -0
  106. package/dist/commands/relay.d.ts +3 -0
  107. package/dist/commands/relay.d.ts.map +1 -0
  108. package/dist/commands/relay.js +279 -0
  109. package/dist/commands/relay.js.map +1 -0
  110. package/dist/commands/remediate.d.ts +3 -0
  111. package/dist/commands/remediate.d.ts.map +1 -0
  112. package/dist/commands/remediate.js +42 -0
  113. package/dist/commands/remediate.js.map +1 -0
  114. package/dist/commands/reputation.d.ts +4 -0
  115. package/dist/commands/reputation.d.ts.map +1 -0
  116. package/dist/commands/reputation.js +72 -0
  117. package/dist/commands/reputation.js.map +1 -0
  118. package/dist/commands/setup.d.ts +3 -0
  119. package/dist/commands/setup.d.ts.map +1 -0
  120. package/dist/commands/setup.js +332 -0
  121. package/dist/commands/setup.js.map +1 -0
  122. package/dist/commands/trust.d.ts +3 -0
  123. package/dist/commands/trust.d.ts.map +1 -0
  124. package/dist/commands/trust.js +23 -0
  125. package/dist/commands/trust.js.map +1 -0
  126. package/dist/commands/verify.d.ts +3 -0
  127. package/dist/commands/verify.d.ts.map +1 -0
  128. package/dist/commands/verify.js +88 -0
  129. package/dist/commands/verify.js.map +1 -0
  130. package/dist/commands/wallet.d.ts +3 -0
  131. package/dist/commands/wallet.d.ts.map +1 -0
  132. package/dist/commands/wallet.js +2520 -0
  133. package/dist/commands/wallet.js.map +1 -0
  134. package/dist/commands/watchtower.d.ts +3 -0
  135. package/dist/commands/watchtower.d.ts.map +1 -0
  136. package/dist/commands/watchtower.js +238 -0
  137. package/dist/commands/watchtower.js.map +1 -0
  138. package/dist/commands/workroom.d.ts +3 -0
  139. package/dist/commands/workroom.d.ts.map +1 -0
  140. package/dist/commands/workroom.js +855 -0
  141. package/dist/commands/workroom.js.map +1 -0
  142. package/dist/config.d.ts +62 -0
  143. package/dist/config.d.ts.map +1 -0
  144. package/dist/config.js +141 -0
  145. package/dist/config.js.map +1 -0
  146. package/dist/daemon/config.d.ts +74 -0
  147. package/dist/daemon/config.d.ts.map +1 -0
  148. package/dist/daemon/config.js +271 -0
  149. package/dist/daemon/config.js.map +1 -0
  150. package/dist/daemon/hire-listener.d.ts +31 -0
  151. package/dist/daemon/hire-listener.d.ts.map +1 -0
  152. package/dist/daemon/hire-listener.js +207 -0
  153. package/dist/daemon/hire-listener.js.map +1 -0
  154. package/dist/daemon/index.d.ts +29 -0
  155. package/dist/daemon/index.d.ts.map +1 -0
  156. package/dist/daemon/index.js +535 -0
  157. package/dist/daemon/index.js.map +1 -0
  158. package/dist/daemon/job-lifecycle.d.ts +62 -0
  159. package/dist/daemon/job-lifecycle.d.ts.map +1 -0
  160. package/dist/daemon/job-lifecycle.js +201 -0
  161. package/dist/daemon/job-lifecycle.js.map +1 -0
  162. package/dist/daemon/notify.d.ts +22 -0
  163. package/dist/daemon/notify.d.ts.map +1 -0
  164. package/dist/daemon/notify.js +148 -0
  165. package/dist/daemon/notify.js.map +1 -0
  166. package/dist/daemon/token-metering.d.ts +42 -0
  167. package/dist/daemon/token-metering.d.ts.map +1 -0
  168. package/dist/daemon/token-metering.js +178 -0
  169. package/dist/daemon/token-metering.js.map +1 -0
  170. package/dist/daemon/userops.d.ts +21 -0
  171. package/dist/daemon/userops.d.ts.map +1 -0
  172. package/dist/daemon/userops.js +88 -0
  173. package/dist/daemon/userops.js.map +1 -0
  174. package/dist/daemon/wallet-monitor.d.ts +16 -0
  175. package/dist/daemon/wallet-monitor.d.ts.map +1 -0
  176. package/dist/daemon/wallet-monitor.js +57 -0
  177. package/dist/daemon/wallet-monitor.js.map +1 -0
  178. package/dist/drain-v4.d.ts +2 -0
  179. package/dist/drain-v4.d.ts.map +1 -0
  180. package/dist/drain-v4.js +167 -0
  181. package/dist/drain-v4.js.map +1 -0
  182. package/dist/endpoint-config.d.ts +36 -0
  183. package/dist/endpoint-config.d.ts.map +1 -0
  184. package/dist/endpoint-config.js +96 -0
  185. package/dist/endpoint-config.js.map +1 -0
  186. package/dist/index.d.ts +3 -0
  187. package/dist/index.d.ts.map +1 -0
  188. package/dist/index.js +79 -0
  189. package/dist/index.js.map +1 -0
  190. package/dist/openshell-runtime.d.ts +55 -0
  191. package/dist/openshell-runtime.d.ts.map +1 -0
  192. package/dist/openshell-runtime.js +268 -0
  193. package/dist/openshell-runtime.js.map +1 -0
  194. package/dist/signing.d.ts +2 -0
  195. package/dist/signing.d.ts.map +1 -0
  196. package/dist/signing.js +23 -0
  197. package/dist/signing.js.map +1 -0
  198. package/dist/telegram-notify.d.ts +23 -0
  199. package/dist/telegram-notify.d.ts.map +1 -0
  200. package/dist/telegram-notify.js +106 -0
  201. package/dist/telegram-notify.js.map +1 -0
  202. package/dist/ui/banner.d.ts +7 -0
  203. package/dist/ui/banner.d.ts.map +1 -0
  204. package/dist/ui/banner.js +37 -0
  205. package/dist/ui/banner.js.map +1 -0
  206. package/dist/ui/colors.d.ts +14 -0
  207. package/dist/ui/colors.d.ts.map +1 -0
  208. package/dist/ui/colors.js +29 -0
  209. package/dist/ui/colors.js.map +1 -0
  210. package/dist/ui/format.d.ts +26 -0
  211. package/dist/ui/format.d.ts.map +1 -0
  212. package/dist/ui/format.js +77 -0
  213. package/dist/ui/format.js.map +1 -0
  214. package/dist/ui/spinner.d.ts +8 -0
  215. package/dist/ui/spinner.d.ts.map +1 -0
  216. package/dist/ui/spinner.js +43 -0
  217. package/dist/ui/spinner.js.map +1 -0
  218. package/dist/utils/format.d.ts +10 -0
  219. package/dist/utils/format.d.ts.map +1 -0
  220. package/dist/utils/format.js +61 -0
  221. package/dist/utils/format.js.map +1 -0
  222. package/dist/utils/hash.d.ts +3 -0
  223. package/dist/utils/hash.d.ts.map +1 -0
  224. package/dist/utils/hash.js +43 -0
  225. package/dist/utils/hash.js.map +1 -0
  226. package/dist/utils/time.d.ts +3 -0
  227. package/dist/utils/time.d.ts.map +1 -0
  228. package/dist/utils/time.js +21 -0
  229. package/dist/utils/time.js.map +1 -0
  230. package/dist/wallet-router.d.ts +25 -0
  231. package/dist/wallet-router.d.ts.map +1 -0
  232. package/dist/wallet-router.js +153 -0
  233. package/dist/wallet-router.js.map +1 -0
  234. package/dist/walletconnect-session.d.ts +12 -0
  235. package/dist/walletconnect-session.d.ts.map +1 -0
  236. package/dist/walletconnect-session.js +26 -0
  237. package/dist/walletconnect-session.js.map +1 -0
  238. package/dist/walletconnect.d.ts +46 -0
  239. package/dist/walletconnect.d.ts.map +1 -0
  240. package/dist/walletconnect.js +267 -0
  241. package/dist/walletconnect.js.map +1 -0
  242. package/package.json +38 -0
  243. package/scripts/authorize-machine-key.ts +43 -0
  244. package/scripts/drain-wallet.ts +149 -0
  245. package/scripts/execute-spend-only.ts +81 -0
  246. package/scripts/register-agent-userop.ts +186 -0
  247. package/src/abis.ts +187 -0
  248. package/src/bundler.ts +235 -0
  249. package/src/client.ts +34 -0
  250. package/src/coinbase-smart-wallet.ts +51 -0
  251. package/src/commands/accept.ts +25 -0
  252. package/src/commands/agent-handshake.ts +67 -0
  253. package/src/commands/agent.ts +458 -0
  254. package/src/commands/agreements.ts +324 -0
  255. package/src/commands/arbitrator.ts +129 -0
  256. package/src/commands/arena-handshake.ts +217 -0
  257. package/src/commands/cancel.ts +26 -0
  258. package/src/commands/channel.ts +208 -0
  259. package/src/commands/coldstart.ts +156 -0
  260. package/src/commands/config.ts +35 -0
  261. package/src/commands/contract-interaction.ts +166 -0
  262. package/src/commands/daemon.ts +971 -0
  263. package/src/commands/deliver.ts +116 -0
  264. package/src/commands/discover.ts +295 -0
  265. package/src/commands/dispute.ts +373 -0
  266. package/src/commands/endpoint.ts +619 -0
  267. package/src/commands/hire.ts +200 -0
  268. package/src/commands/migrate.ts +175 -0
  269. package/src/commands/negotiate.ts +270 -0
  270. package/src/commands/openshell.ts +1053 -0
  271. package/src/commands/owner.ts +30 -0
  272. package/src/commands/policy.ts +252 -0
  273. package/src/commands/relay.ts +272 -0
  274. package/src/commands/remediate.ts +22 -0
  275. package/src/commands/reputation.ts +71 -0
  276. package/src/commands/setup.ts +343 -0
  277. package/src/commands/trust.ts +15 -0
  278. package/src/commands/verify.ts +88 -0
  279. package/src/commands/wallet.ts +2892 -0
  280. package/src/commands/watchtower.ts +232 -0
  281. package/src/commands/workroom.ts +889 -0
  282. package/src/config.ts +153 -0
  283. package/src/daemon/config.ts +308 -0
  284. package/src/daemon/hire-listener.ts +226 -0
  285. package/src/daemon/index.ts +609 -0
  286. package/src/daemon/job-lifecycle.ts +215 -0
  287. package/src/daemon/notify.ts +157 -0
  288. package/src/daemon/token-metering.ts +183 -0
  289. package/src/daemon/userops.ts +119 -0
  290. package/src/daemon/wallet-monitor.ts +90 -0
  291. package/src/drain-v4.ts +159 -0
  292. package/src/endpoint-config.ts +83 -0
  293. package/src/index.ts +75 -0
  294. package/src/openshell-runtime.ts +277 -0
  295. package/src/signing.ts +28 -0
  296. package/src/telegram-notify.ts +88 -0
  297. package/src/ui/banner.ts +41 -0
  298. package/src/ui/colors.ts +30 -0
  299. package/src/ui/format.ts +77 -0
  300. package/src/ui/spinner.ts +46 -0
  301. package/src/utils/format.ts +48 -0
  302. package/src/utils/hash.ts +5 -0
  303. package/src/utils/time.ts +15 -0
  304. package/src/wallet-router.ts +178 -0
  305. package/src/walletconnect-session.ts +27 -0
  306. package/src/walletconnect.ts +294 -0
  307. package/test/time.test.js +11 -0
  308. package/tsconfig.json +19 -0
@@ -0,0 +1,116 @@
1
+ import { Command } from "commander";
2
+ import { ServiceAgreementClient, uploadEncryptedIPFS } from "@arc402/sdk";
3
+ import { ethers } from "ethers";
4
+ import { loadConfig } from "../config";
5
+ import { getClient, requireSigner } from "../client";
6
+ import { hashFile, hashString } from "../utils/hash";
7
+ import { printSenderInfo, executeContractWriteViaWallet } from "../wallet-router";
8
+ import { SERVICE_AGREEMENT_ABI } from "../abis";
9
+ import { readFile } from "fs/promises";
10
+ import prompts from "prompts";
11
+
12
+ export function registerDeliverCommand(program: Command): void {
13
+ program
14
+ .command("deliver <id>")
15
+ .description("Provider commits a deliverable for verification; legacy fulfill mode is compatibility-only")
16
+ .option("--output <filepath>")
17
+ .option("--message <text>")
18
+ .option("--fulfill", "Use legacy trusted-only fulfill() instead of commitDeliverable()", false)
19
+ .option("--encrypt", "Encrypt the deliverable before uploading to IPFS (prompts for recipient public key)", false)
20
+ .action(async (id, opts) => {
21
+ const config = loadConfig();
22
+ if (!config.serviceAgreementAddress) throw new Error("serviceAgreementAddress missing in config");
23
+ const { signer, address: signerAddress } = await requireSigner(config);
24
+ printSenderInfo(config);
25
+
26
+ // Pre-flight: check deadline and legacyFulfillEnabled (J3-01, J3-02)
27
+ {
28
+ const { provider: prefProvider } = await getClient(config);
29
+ const saAbi = [
30
+ "function getAgreement(uint256 id) external view returns (tuple(uint256 id, address client, address provider, string serviceType, string description, uint256 price, address token, uint256 deadline, bytes32 deliverablesHash, uint8 status, uint256 createdAt, uint256 resolvedAt, uint256 verifyWindowEnd, bytes32 committedHash))",
31
+ "function legacyFulfillEnabled() external view returns (bool)",
32
+ "function legacyFulfillProviders(address) external view returns (bool)",
33
+ ];
34
+ const saContract = new ethers.Contract(config.serviceAgreementAddress!, saAbi, prefProvider);
35
+ try {
36
+ const ag = await saContract.getAgreement(BigInt(id));
37
+ const deadline = Number(ag.deadline);
38
+ const nowSec = Math.floor(Date.now() / 1000);
39
+ if (nowSec > deadline) {
40
+ const deadlineDate = new Date(deadline * 1000).toISOString();
41
+ console.error(`Delivery deadline has passed (${deadlineDate}). This transaction will revert.`);
42
+ console.error(`Contact the client to open a new agreement.`);
43
+ process.exit(1);
44
+ }
45
+ } catch (e) {
46
+ // If it's a contract read failure, skip the check (let the tx reveal the error)
47
+ if (e instanceof Error && !e.message.includes("CALL_EXCEPTION") && !e.message.includes("could not decode")) throw e;
48
+ }
49
+
50
+ if (opts.fulfill) {
51
+ let legacyEnabled = false;
52
+ try {
53
+ legacyEnabled = await saContract.legacyFulfillEnabled();
54
+ } catch { /* assume enabled if read fails */ legacyEnabled = true; }
55
+ if (!legacyEnabled) {
56
+ console.error("Legacy fulfill is disabled on this deployment. Use `arc402 deliver <id>` (without --fulfill) to deliver via the standard flow.");
57
+ process.exit(1);
58
+ }
59
+ let isLegacyProvider = false;
60
+ try {
61
+ isLegacyProvider = await saContract.legacyFulfillProviders(signerAddress);
62
+ } catch { /* assume allowed if read fails */ isLegacyProvider = true; }
63
+ if (!isLegacyProvider) {
64
+ console.error("You are not in the legacy fulfill providers list for this agreement.");
65
+ process.exit(1);
66
+ }
67
+ }
68
+ }
69
+
70
+ if (opts.encrypt) {
71
+ if (!opts.output) throw new Error("--encrypt requires --output <filepath>");
72
+
73
+ const { recipientPubKeyHex } = await prompts({
74
+ type: "text",
75
+ name: "recipientPubKeyHex",
76
+ message: "Recipient NaCl box public key (32 bytes, hex):",
77
+ validate: (v: string) => /^[0-9a-fA-F]{64}$/.test(v.trim()) || "Must be 64 hex characters (32 bytes)",
78
+ });
79
+ if (!recipientPubKeyHex) throw new Error("Recipient public key is required for encrypted delivery");
80
+
81
+ const recipientPublicKey = Uint8Array.from(Buffer.from(recipientPubKeyHex.trim(), "hex"));
82
+ const buffer = await readFile(opts.output);
83
+ const { cid, uri } = await uploadEncryptedIPFS(buffer, recipientPublicKey);
84
+ // Hash plaintext so recipient can verify integrity after decryption
85
+ const { ethers } = await import("ethers");
86
+ const hash = ethers.keccak256(buffer);
87
+
88
+ console.log(`encrypted upload: ${uri} (CID: ${cid})`);
89
+
90
+ if (config.walletContractAddress) {
91
+ await executeContractWriteViaWallet(
92
+ config.walletContractAddress, signer, config.serviceAgreementAddress,
93
+ SERVICE_AGREEMENT_ABI, "commitDeliverable", [BigInt(id), hash],
94
+ );
95
+ } else {
96
+ const client = new ServiceAgreementClient(config.serviceAgreementAddress, signer);
97
+ await client.commitDeliverable(BigInt(id), hash);
98
+ }
99
+ console.log(`committed ${id} hash=${hash} (plaintext hash; encrypted at ${uri})`);
100
+ return;
101
+ }
102
+
103
+ const hash = opts.output ? hashFile(opts.output) : hashString(opts.message ?? `agreement:${id}`);
104
+ if (config.walletContractAddress) {
105
+ const fn = opts.fulfill ? "fulfill" : "commitDeliverable";
106
+ await executeContractWriteViaWallet(
107
+ config.walletContractAddress, signer, config.serviceAgreementAddress,
108
+ SERVICE_AGREEMENT_ABI, fn, [BigInt(id), hash],
109
+ );
110
+ } else {
111
+ const client = new ServiceAgreementClient(config.serviceAgreementAddress, signer);
112
+ if (opts.fulfill) await client.fulfill(BigInt(id), hash); else await client.commitDeliverable(BigInt(id), hash);
113
+ }
114
+ console.log(`${opts.fulfill ? 'fulfilled' : 'committed'} ${id} hash=${hash}`);
115
+ });
116
+ }
@@ -0,0 +1,295 @@
1
+ import { Command } from "commander";
2
+ import { ethers } from "ethers";
3
+ import { AgentRegistryClient, CapabilityRegistryClient, ReputationOracleClient, SponsorshipAttestationClient } from "@arc402/sdk";
4
+ import { loadConfig } from "../config";
5
+ import { getClient } from "../client";
6
+ import { getTrustTier, printTable, truncateAddress } from "../utils/format";
7
+
8
+ // Minimal ABI for the new getAgentsWithCapability function (Spec 18)
9
+ const CAPABILITY_REGISTRY_EXTRA_ABI = [
10
+ "function getAgentsWithCapability(string calldata capability) external view returns (address[])",
11
+ ];
12
+
13
+ // ─── Composite scoring helpers ────────────────────────────────────────────────
14
+
15
+ interface ScoredAgent {
16
+ wallet: string;
17
+ name: string;
18
+ serviceType: string;
19
+ endpoint: string;
20
+ metadataURI: string;
21
+ capabilities: string[];
22
+ canonicalCapabilities: string[];
23
+ trustScore: number;
24
+ stake: bigint;
25
+ completedJobs: number;
26
+ priceUsd: number | null;
27
+ compositeScore: number;
28
+ rank: number;
29
+ operational: { uptimeScore: number; responseScore: number };
30
+ highestTier?: unknown;
31
+ reputation?: unknown;
32
+ }
33
+
34
+ function normalise(values: number[]): number[] {
35
+ const min = Math.min(...values);
36
+ const max = Math.max(...values);
37
+ const range = max - min;
38
+ if (range === 0) return values.map(() => 1);
39
+ return values.map((v) => (v - min) / range);
40
+ }
41
+
42
+ function computeCompositeScores(agents: Omit<ScoredAgent, "compositeScore" | "rank">[]): ScoredAgent[] {
43
+ if (agents.length === 0) return [];
44
+
45
+ const trustVals = normalise(agents.map((a) => a.trustScore));
46
+ const stakeVals = normalise(agents.map((a) => Number(a.stake)));
47
+ const jobsVals = normalise(agents.map((a) => a.completedJobs));
48
+
49
+ // Price: lower is better — invert. Missing price gets neutral 0.5.
50
+ const rawPrices = agents.map((a) => (a.priceUsd !== null ? a.priceUsd : -1));
51
+ const validPrices = rawPrices.filter((p) => p >= 0);
52
+ const maxPrice = validPrices.length > 0 ? Math.max(...validPrices) : 1;
53
+ const priceInvVals = rawPrices.map((p) =>
54
+ p < 0 ? 0.5 : maxPrice > 0 ? 1 - p / maxPrice : 1
55
+ );
56
+
57
+ return agents.map((agent, i) => ({
58
+ ...agent,
59
+ compositeScore:
60
+ trustVals[i] * 0.5 +
61
+ stakeVals[i] * 0.2 +
62
+ jobsVals[i] * 0.2 +
63
+ priceInvVals[i] * 0.1,
64
+ rank: 0, // filled after sort
65
+ }));
66
+ }
67
+
68
+ // ─── Command ──────────────────────────────────────────────────────────────────
69
+
70
+ export function registerDiscoverCommand(program: Command): void {
71
+ program
72
+ .command("discover")
73
+ .description(
74
+ "Discover agents by capability with trust/price/stake filters and composite ranking (Specs 16, 18)"
75
+ )
76
+ .option("--capability <cap>", "Exact canonical capability (e.g. legal.patent-analysis.us.v1)")
77
+ .option("--capability-prefix <pfx>", "Prefix match against registered capabilities")
78
+ .option("--service-type <type>", "Filter by serviceType substring")
79
+ .option("--type <type>", "Filter by serviceType substring (alias for --service-type)")
80
+ .option("--min-trust <score>", "Minimum trust score", "0")
81
+ .option("--max-price <usd>", "Maximum price in USD (from agent metadataURI, best-effort)", "0")
82
+ .option("--min-stake <wei>", "Minimum stake in wei", "0")
83
+ .option("--top <n>", "Show top N agents by trust score")
84
+ .option("--sort <field>", "Sort by: trust | price | jobs | stake | composite", "composite")
85
+ .option("--limit <n>", "Max results", "20")
86
+ .option("--json", "Machine-parseable output")
87
+ .action(async (opts) => {
88
+ const config = loadConfig();
89
+ if (!config.agentRegistryAddress) throw new Error("agentRegistryAddress missing in config");
90
+ const { provider } = await getClient(config);
91
+
92
+ const registry = new AgentRegistryClient(config.agentRegistryAddress, provider);
93
+ const capabilitySDK = config.capabilityRegistryAddress
94
+ ? new CapabilityRegistryClient(config.capabilityRegistryAddress, provider)
95
+ : null;
96
+ const sponsorship = config.sponsorshipAttestationAddress
97
+ ? new SponsorshipAttestationClient(config.sponsorshipAttestationAddress, provider)
98
+ : null;
99
+ const reputation = config.reputationOracleAddress
100
+ ? new ReputationOracleClient(config.reputationOracleAddress, provider)
101
+ : null;
102
+
103
+ // --top <n> sets sort to trust and overrides limit
104
+ const effectiveSort = opts.top ? "trust" : opts.sort;
105
+ const limit = opts.top ? Number(opts.top) : Number(opts.limit);
106
+ const minTrust = Number(opts.minTrust);
107
+ const maxPriceUsd = Number(opts.maxPrice); // 0 = no filter
108
+ const minStakeWei = BigInt(opts.minStake);
109
+ // --type is an alias for --service-type
110
+ if (opts.type && !opts.serviceType) opts.serviceType = opts.type;
111
+
112
+ // ── Step 1: Get candidate addresses ─────────────────────────────────────
113
+
114
+ let candidateAddresses: string[] | null = null;
115
+
116
+ if (opts.capability && config.capabilityRegistryAddress) {
117
+ // Use the reverse index for O(1) exact-match (Spec 18: getAgentsWithCapability)
118
+ const capContract = new ethers.Contract(
119
+ config.capabilityRegistryAddress,
120
+ CAPABILITY_REGISTRY_EXTRA_ABI,
121
+ provider
122
+ );
123
+ try {
124
+ const addrs: string[] = await capContract.getAgentsWithCapability(opts.capability);
125
+ candidateAddresses = addrs;
126
+ } catch {
127
+ // Contract may not have been upgraded yet; fall through to listAgents
128
+ }
129
+ }
130
+
131
+ // ── Step 2: Load agent data ───────────────────────────────────────────
132
+
133
+ let agentInfos: Awaited<ReturnType<typeof registry.listAgents>>;
134
+
135
+ if (candidateAddresses !== null) {
136
+ const results = await Promise.allSettled(
137
+ candidateAddresses.map((addr) => registry.getAgent(addr))
138
+ );
139
+ agentInfos = results
140
+ .filter((r): r is PromiseFulfilledResult<typeof agentInfos[number]> => r.status === "fulfilled")
141
+ .map((r) => r.value);
142
+ } else {
143
+ agentInfos = await registry.listAgents(limit * 10);
144
+ }
145
+
146
+ // ── Step 3: Apply filters ────────────────────────────────────────────
147
+
148
+ let filtered = agentInfos.filter((a) => a.active !== false);
149
+
150
+ if (opts.capability) {
151
+ filtered = filtered.filter((a) =>
152
+ a.capabilities.some((c: string) => c === opts.capability)
153
+ );
154
+ }
155
+
156
+ if (opts.capabilityPrefix) {
157
+ filtered = filtered.filter((a) =>
158
+ a.capabilities.some((c: string) => c.startsWith(opts.capabilityPrefix as string))
159
+ );
160
+ }
161
+
162
+ if (opts.serviceType) {
163
+ filtered = filtered.filter((a) =>
164
+ a.serviceType.toLowerCase().includes(String(opts.serviceType).toLowerCase())
165
+ );
166
+ }
167
+
168
+ filtered = filtered.filter((a) => Number(a.trustScore ?? 0n) >= minTrust);
169
+
170
+ if (minStakeWei > 0n) {
171
+ filtered = filtered.filter((a) => {
172
+ const stake = (a as unknown as { stake?: bigint }).stake ?? 0n;
173
+ return BigInt(stake) >= minStakeWei;
174
+ });
175
+ }
176
+
177
+ // ── Step 4: Enrich ──────────────────────────────────────────────────
178
+
179
+ const enriched = await Promise.all(
180
+ filtered.slice(0, limit * 5).map(async (agent) => {
181
+ const [operational, canonicalCapabilities, highestTier, rep] = await Promise.all([
182
+ registry.getOperationalMetrics(agent.wallet),
183
+ capabilitySDK ? capabilitySDK.getCapabilities(agent.wallet) : Promise.resolve([]),
184
+ sponsorship ? sponsorship.getHighestTier(agent.wallet) : Promise.resolve(undefined),
185
+ reputation ? reputation.getReputation(agent.wallet) : Promise.resolve(undefined),
186
+ ]);
187
+
188
+ // Best-effort metadata fetch for priceUsd
189
+ let priceUsd: number | null = null;
190
+ if (agent.metadataURI && /^https?:\/\//.test(agent.metadataURI)) {
191
+ try {
192
+ const ctrl = new AbortController();
193
+ const tid = setTimeout(() => ctrl.abort(), 2000);
194
+ const resp = await fetch(agent.metadataURI, { signal: ctrl.signal });
195
+ clearTimeout(tid);
196
+ if (resp.ok) {
197
+ const meta = await resp.json() as { pricing?: { priceUsd?: number } };
198
+ if (meta.pricing?.priceUsd != null) priceUsd = meta.pricing.priceUsd;
199
+ }
200
+ } catch { /* ignore — advisory only */ }
201
+ }
202
+
203
+ // Apply max-price filter post-enrichment
204
+ if (maxPriceUsd > 0 && priceUsd !== null && priceUsd > maxPriceUsd) {
205
+ return null;
206
+ }
207
+
208
+ const repObj = rep && typeof rep === "object" ? (rep as unknown) as Record<string, unknown> : {};
209
+
210
+ return {
211
+ wallet: agent.wallet,
212
+ name: agent.name,
213
+ serviceType: agent.serviceType,
214
+ endpoint: agent.endpoint,
215
+ metadataURI: agent.metadataURI,
216
+ capabilities: agent.capabilities as string[],
217
+ canonicalCapabilities: canonicalCapabilities as string[],
218
+ trustScore: Number(agent.trustScore ?? 0n),
219
+ stake: (agent as unknown as { stake?: bigint }).stake ?? 0n,
220
+ completedJobs: Number(repObj.completedJobs ?? 0),
221
+ priceUsd,
222
+ compositeScore: 0,
223
+ rank: 0,
224
+ operational: {
225
+ uptimeScore: Number(operational.uptimeScore),
226
+ responseScore: Number(operational.responseScore),
227
+ },
228
+ highestTier,
229
+ reputation: rep,
230
+ } as Omit<ScoredAgent, "compositeScore" | "rank">;
231
+ })
232
+ );
233
+
234
+ const validAgents = enriched.filter((a): a is Omit<ScoredAgent, "compositeScore" | "rank"> => a !== null);
235
+
236
+ // ── Step 5: Score ──────────────────────────────────────────────────────
237
+
238
+ let scored = computeCompositeScores(validAgents);
239
+
240
+ // Sort
241
+ switch (effectiveSort) {
242
+ case "trust":
243
+ scored.sort((a, b) => b.trustScore - a.trustScore);
244
+ break;
245
+ case "price":
246
+ scored.sort((a, b) => {
247
+ if (a.priceUsd === null && b.priceUsd === null) return 0;
248
+ if (a.priceUsd === null) return 1;
249
+ if (b.priceUsd === null) return -1;
250
+ return a.priceUsd - b.priceUsd;
251
+ });
252
+ break;
253
+ case "jobs":
254
+ scored.sort((a, b) => b.completedJobs - a.completedJobs);
255
+ break;
256
+ case "stake":
257
+ scored.sort((a, b) => (b.stake > a.stake ? 1 : b.stake < a.stake ? -1 : 0));
258
+ break;
259
+ default: // "composite"
260
+ scored.sort((a, b) => b.compositeScore - a.compositeScore);
261
+ }
262
+
263
+ // Assign 1-based ranks after sort
264
+ scored = scored.slice(0, limit).map((a, i) => ({ ...a, rank: i + 1 }));
265
+
266
+ // ── Step 6: Output ─────────────────────────────────────────────────────
267
+
268
+ if (opts.json) {
269
+ return console.log(JSON.stringify(
270
+ scored,
271
+ (_k, value) => typeof value === "bigint" ? value.toString() : value,
272
+ 2
273
+ ));
274
+ }
275
+
276
+ printTable(
277
+ ["RANK", "ADDRESS", "NAME", "SERVICE", "TRUST", "SCORE", "CAPABILITIES"],
278
+ scored.map((agent) => {
279
+ const caps = (agent.canonicalCapabilities.length
280
+ ? agent.canonicalCapabilities
281
+ : agent.capabilities
282
+ ).slice(0, 2).join(", ");
283
+ return [
284
+ String(agent.rank),
285
+ truncateAddress(agent.wallet),
286
+ agent.name,
287
+ agent.serviceType,
288
+ `${agent.trustScore} ${getTrustTier(agent.trustScore)}`,
289
+ agent.compositeScore.toFixed(3),
290
+ caps,
291
+ ];
292
+ })
293
+ );
294
+ });
295
+ }