@sage-protocol/cli 0.8.0 → 0.8.3

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.

Potentially problematic release.


This version of @sage-protocol/cli might be problematic. Click here for more details.

Files changed (34) hide show
  1. package/README.md +12 -11
  2. package/dist/cli/commands/boost.js +339 -62
  3. package/dist/cli/commands/bounty.js +28 -4
  4. package/dist/cli/commands/config.js +10 -1
  5. package/dist/cli/commands/contributor.js +16 -6
  6. package/dist/cli/commands/dao.js +1 -1
  7. package/dist/cli/commands/discover.js +3 -3
  8. package/dist/cli/commands/governance.js +141 -58
  9. package/dist/cli/commands/install.js +178 -36
  10. package/dist/cli/commands/ipfs.js +12 -2
  11. package/dist/cli/commands/library.js +277 -268
  12. package/dist/cli/commands/members.js +132 -18
  13. package/dist/cli/commands/multiplier.js +101 -13
  14. package/dist/cli/commands/nft.js +16 -3
  15. package/dist/cli/commands/personal.js +69 -2
  16. package/dist/cli/commands/prompt.js +1 -1
  17. package/dist/cli/commands/proposals.js +153 -3
  18. package/dist/cli/commands/stake-status.js +130 -56
  19. package/dist/cli/commands/sxxx.js +37 -4
  20. package/dist/cli/commands/wallet.js +5 -10
  21. package/dist/cli/contracts/index.js +2 -1
  22. package/dist/cli/index.js +5 -0
  23. package/dist/cli/privy-auth-wallet-manager.js +3 -2
  24. package/dist/cli/services/config/chain-defaults.js +1 -1
  25. package/dist/cli/services/config/manager.js +3 -0
  26. package/dist/cli/services/config/schema.js +1 -0
  27. package/dist/cli/services/ipfs/onboarding.js +11 -0
  28. package/dist/cli/utils/aliases.js +62 -3
  29. package/dist/cli/utils/cli-ui.js +1 -1
  30. package/dist/cli/utils/provider.js +7 -3
  31. package/dist/cli/wallet-manager.js +7 -12
  32. package/dist/prompts/e2e-test-prompt.md +22 -0
  33. package/dist/prompts/skills/build-web3/plugin.json +11 -0
  34. package/package.json +1 -1
@@ -20,6 +20,7 @@ function register(program) {
20
20
  .option('--all', 'List members for all SubDAOs', false)
21
21
  .option('--limit <n>', 'Max members per SubDAO to print', '100')
22
22
  .option('--include-inactive', 'Include inactive members', false)
23
+ .option('--json', 'Output JSON', false)
23
24
  .action(async (opts) => {
24
25
  try {
25
26
  const { ethers } = require('ethers');
@@ -34,26 +35,52 @@ function register(program) {
34
35
  subdaos = [addr];
35
36
  }
36
37
  const first = Number(opts.limit || '100');
38
+ const results = [];
37
39
  for (const s of subdaos) {
38
40
  const where = opts.includeInactive ? `subdao: "${ethers.getAddress(s)}"` : `subdao: "${ethers.getAddress(s)}", active: true`;
39
- const q = `query($first:Int,$skip:Int){ members(where:{${where}}, first:$first, skip:$skip, orderBy: account, orderDirection: asc){ account currentStake active } }`;
41
+ const qWithStake = `query($first:Int,$skip:Int){ members(where:{${where}}, first:$first, skip:$skip, orderBy: account, orderDirection: asc){ account currentStake active } }`;
42
+ const qNoStake = `query($first:Int,$skip:Int){ members(where:{${where}}, first:$first, skip:$skip, orderBy: account, orderDirection: asc){ account active } }`;
43
+ let stakeSupported = true;
40
44
  let skip = 0; let printed = 0; let batch;
41
- ui.newline();
42
- ui.output(`SubDAO: ${s}`);
43
- ui.output('Account Active CurrentStake');
44
- ui.output('----------------------------------------------------------');
45
+ const members = [];
46
+ if (!opts.json) {
47
+ ui.newline();
48
+ ui.output(`SubDAO: ${s}`);
49
+ ui.output('Account Active CurrentStake');
50
+ ui.output('----------------------------------------------------------');
51
+ }
45
52
  do {
46
- batch = (await fetchGraph(q, { first: Math.min(first - printed, 1000), skip })).members || [];
53
+ try {
54
+ batch = (await fetchGraph(stakeSupported ? qWithStake : qNoStake, { first: Math.min(first - printed, 1000), skip })).members || [];
55
+ } catch (e) {
56
+ const msg = String(e && e.message ? e.message : e);
57
+ if (stakeSupported && /currentStake/i.test(msg)) {
58
+ stakeSupported = false;
59
+ batch = (await fetchGraph(qNoStake, { first: Math.min(first - printed, 1000), skip })).members || [];
60
+ } else {
61
+ throw e;
62
+ }
63
+ }
47
64
  for (const m of batch) {
48
65
  const acct = m.account;
49
- const stake = m.currentStake;
66
+ const stake = stakeSupported ? m.currentStake : '0';
50
67
  const active = m.active ? 'yes ' : 'no ';
51
- ui.output(`${acct} ${active} ${stake}`);
68
+ members.push(opts.json
69
+ ? { account: acct, active: !!m.active, currentStake: stake }
70
+ : { account: acct, active: !!m.active, currentStake: stake }
71
+ );
72
+ if (!opts.json) {
73
+ ui.output(`${acct} ${active} ${stake}`);
74
+ }
52
75
  printed += 1; if (printed >= first) break;
53
76
  }
54
77
  skip += batch.length;
55
78
  } while (batch.length && printed < first);
56
- if (printed === 0) ui.output('(no members found)');
79
+ if (!opts.json && printed === 0) ui.output('(no members found)');
80
+ results.push({ subdao: s, count: members.length, members });
81
+ }
82
+ if (opts.json) {
83
+ ui.json({ ok: true, subdaos: results });
57
84
  }
58
85
  } catch (e) {
59
86
  handleCLIError('members:list', e);
@@ -65,6 +92,7 @@ function register(program) {
65
92
  .option('--subdao <address>', 'SubDAO address')
66
93
  .option('--all', 'List stakes for all SubDAOs', false)
67
94
  .option('--limit <n>', 'Max rows per SubDAO', '100')
95
+ .option('--json', 'Output JSON', false)
68
96
  .action(async (opts) => {
69
97
  try {
70
98
  const { ethers } = require('ethers');
@@ -79,32 +107,118 @@ function register(program) {
79
107
  subdaos = [addr];
80
108
  }
81
109
  const first = Number(opts.limit || '100');
110
+ const results = [];
82
111
  for (const s of subdaos) {
83
112
  const q = `query($first:Int,$skip:Int){ members(where:{subdao: "${ethers.getAddress(s)}", currentStake_gt: "0"}, first:$first, skip:$skip, orderBy: currentStake, orderDirection: desc){ account currentStake active } }`;
84
113
  let skip = 0; let printed = 0; let batch; let total = 0n;
85
- ui.newline();
86
- ui.output(`SubDAO: ${s}`);
87
- ui.output('Account Active CurrentStake');
88
- ui.output('----------------------------------------------------------');
114
+ const members = [];
115
+ if (!opts.json) {
116
+ ui.newline();
117
+ ui.output(`SubDAO: ${s}`);
118
+ ui.output('Account Active CurrentStake');
119
+ ui.output('----------------------------------------------------------');
120
+ }
89
121
  do {
90
- batch = (await fetchGraph(q, { first: Math.min(first - printed, 1000), skip })).members || [];
122
+ try {
123
+ batch = (await fetchGraph(q, { first: Math.min(first - printed, 1000), skip })).members || [];
124
+ } catch (e) {
125
+ const msg = String(e && e.message ? e.message : e);
126
+ if (/currentStake/i.test(msg)) {
127
+ throw new Error('Subgraph schema missing currentStake. Redeploy subgraph or use `sage members list` instead.');
128
+ }
129
+ throw e;
130
+ }
91
131
  for (const m of batch) {
92
132
  const acct = m.account; const stake = BigInt(m.currentStake); const active = m.active ? 'yes ' : 'no ';
93
133
  total += stake;
94
- ui.output(`${acct} ${active} ${m.currentStake}`);
134
+ members.push({ account: acct, active: !!m.active, currentStake: m.currentStake });
135
+ if (!opts.json) {
136
+ ui.output(`${acct} ${active} ${m.currentStake}`);
137
+ }
95
138
  printed += 1; if (printed >= first) break;
96
139
  }
97
140
  skip += batch.length;
98
141
  } while (batch.length && printed < first);
99
- ui.output('----------------------------------------------------------');
100
- ui.output(`Total current stake (visible subset): ${total.toString()}`);
101
- if (printed === 0) ui.output('(no stakers found)');
142
+ if (!opts.json) {
143
+ ui.output('----------------------------------------------------------');
144
+ ui.output(`Total current stake (visible subset): ${total.toString()}`);
145
+ if (printed === 0) ui.output('(no stakers found)');
146
+ }
147
+ results.push({ subdao: s, count: members.length, totalStake: total.toString(), members });
148
+ }
149
+ if (opts.json) {
150
+ ui.json({ ok: true, subdaos: results });
102
151
  }
103
152
  } catch (e) {
104
153
  handleCLIError('members:current-stake', e);
105
154
  }
106
155
  });
107
156
 
157
+ cmd.command('voting-power')
158
+ .description('List delegated SXXX voting power for members (via token getVotes)')
159
+ .option('--subdao <address>', 'SubDAO address; omit to select interactively or use --all')
160
+ .option('--all', 'List members for all SubDAOs', false)
161
+ .option('--limit <n>', 'Max members per SubDAO to print', '100')
162
+ .option('--include-inactive', 'Include inactive members', false)
163
+ .option('--json', 'Output JSON', false)
164
+ .action(async (opts) => {
165
+ try {
166
+ const { ethers } = require('ethers');
167
+ const cliConfig = require('../config');
168
+ const SubDAOManager = require('../subdao-manager');
169
+ const manager = new SubDAOManager(); await manager.initialize();
170
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
171
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
172
+ const sxxxAddress = cliConfig.resolveAddress('SXXX_TOKEN_ADDRESS');
173
+ if (!sxxxAddress) throw new Error('SXXX_TOKEN_ADDRESS not configured');
174
+ const sxxx = new ethers.Contract(sxxxAddress, ['function getVotes(address) view returns (uint256)'], provider);
175
+
176
+ let subdaos = [];
177
+ if (opts.all) {
178
+ const data = await fetchGraph('{ subDAOs(first: 1000) { id address } }');
179
+ subdaos = (data.subDAOs || []).map(s => s.address || s.id);
180
+ } else {
181
+ const addr = await manager.resolveSubDAOAddress(opts.subdao);
182
+ subdaos = [addr];
183
+ }
184
+ const first = Number(opts.limit || '100');
185
+ const results = [];
186
+ for (const s of subdaos) {
187
+ const where = opts.includeInactive ? `subdao: "${ethers.getAddress(s)}"` : `subdao: "${ethers.getAddress(s)}", active: true`;
188
+ const q = `query($first:Int,$skip:Int){ members(where:{${where}}, first:$first, skip:$skip, orderBy: account, orderDirection: asc){ account active } }`;
189
+ let skip = 0; let printed = 0; let batch;
190
+ const members = [];
191
+ if (!opts.json) {
192
+ ui.newline();
193
+ ui.output(`SubDAO: ${s}`);
194
+ ui.output('Account Active VotingPower(SXXX)');
195
+ ui.output('----------------------------------------------------------------');
196
+ }
197
+ do {
198
+ batch = (await fetchGraph(q, { first: Math.min(first - printed, 1000), skip })).members || [];
199
+ for (const m of batch) {
200
+ const acct = m.account;
201
+ const active = m.active ? 'yes ' : 'no ';
202
+ const votes = await sxxx.getVotes(acct).catch(() => 0n);
203
+ members.push({ account: acct, active: !!m.active, votingPower: votes.toString() });
204
+ if (!opts.json) {
205
+ ui.output(`${acct} ${active} ${ethers.formatEther(votes)}`);
206
+ }
207
+ printed += 1; if (printed >= first) break;
208
+ }
209
+ skip += batch.length;
210
+ } while (batch.length && printed < first);
211
+ if (!opts.json && printed === 0) ui.output('(no members found)');
212
+ results.push({ subdao: s, count: members.length, members });
213
+ }
214
+ if (opts.json) {
215
+ ui.json({ ok: true, subdaos: results });
216
+ }
217
+ } catch (e) {
218
+ handleCLIError('members:voting-power', e);
219
+ }
220
+ });
221
+
108
222
  program.addCommand(cmd);
109
223
  }
110
224
 
@@ -52,6 +52,10 @@ const AUCTION_ABI = [
52
52
  'function settleCurrentAndCreateNewAuction()'
53
53
  ];
54
54
 
55
+ const SUBDAO_ABI = [
56
+ 'function governor() view returns (address)'
57
+ ];
58
+
55
59
  const TOKEN_ABI = [
56
60
  'function name() view returns (string)',
57
61
  'function symbol() view returns (string)',
@@ -61,10 +65,38 @@ const TOKEN_ABI = [
61
65
  ];
62
66
 
63
67
  function getProvider() {
64
- const rpcUrl = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
68
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
65
69
  return new ethers.JsonRpcProvider(rpcUrl);
66
70
  }
67
71
 
72
+ /**
73
+ * Resolve a Governor address from a SubDAO or Governor address.
74
+ * If the address is a SubDAO, calls governor() to get the Governor.
75
+ * If the address is already a Governor, returns it directly.
76
+ */
77
+ async function resolveGovernorAddress(address, provider) {
78
+ const normalized = ethers.getAddress(address);
79
+ // Try calling governor() - if it works, the address is a SubDAO
80
+ try {
81
+ const subdao = new ethers.Contract(normalized, SUBDAO_ABI, provider);
82
+ const governorAddr = await subdao.governor();
83
+ if (governorAddr && governorAddr !== ethers.ZeroAddress) {
84
+ return { governor: governorAddr, subdao: normalized, isSubDAO: true };
85
+ }
86
+ } catch (_) {
87
+ // Not a SubDAO or doesn't have governor() - assume it's a Governor
88
+ }
89
+
90
+ // Verify governor interface before returning
91
+ try {
92
+ const gov = new ethers.Contract(normalized, GOVERNOR_ABI, provider);
93
+ await gov.token();
94
+ return { governor: normalized, subdao: null, isSubDAO: false };
95
+ } catch (err) {
96
+ throw new Error(`Address ${normalized} is not a SubDAO or Governor. Use --gov for governor address or --subdao for SubDAO.`);
97
+ }
98
+ }
99
+
68
100
  async function resolveAccountAddress(address) {
69
101
  if (address) return address;
70
102
 
@@ -92,11 +124,21 @@ function register(program) {
92
124
  addDAOAddressOptions(statusCmd);
93
125
  statusCmd.action(async (options) => {
94
126
  try {
95
- const governorAddr = resolveDAOAddress(options, { required: true });
127
+ const inputAddr = resolveDAOAddress(options, { required: true });
96
128
  const provider = getProvider();
97
- const governor = new ethers.Contract(governorAddr, GOVERNOR_ABI, provider);
98
129
 
99
- ui.info(`Checking multiplier status for governor: ${governorAddr}`);
130
+ // Auto-resolve Governor from SubDAO if needed
131
+ const resolved = await resolveGovernorAddress(inputAddr, provider);
132
+ const governorAddr = resolved.governor;
133
+
134
+ if (resolved.isSubDAO) {
135
+ ui.info(`SubDAO detected: ${inputAddr}`);
136
+ ui.info(`Resolved Governor: ${governorAddr}`);
137
+ } else {
138
+ ui.info(`Checking multiplier status for governor: ${governorAddr}`);
139
+ }
140
+
141
+ const governor = new ethers.Contract(governorAddr, GOVERNOR_ABI, provider);
100
142
 
101
143
  // Check if multipliers are enabled
102
144
  const isEnabled = await governor.isMultiplierEnabled().catch(() => false);
@@ -158,30 +200,44 @@ function register(program) {
158
200
  // Build calculate command
159
201
  const calculateCmd = new Command('calculate')
160
202
  .description('Show voting power breakdown for an account')
161
- .argument('[address]', 'Account address (defaults to connected wallet)');
203
+ .argument('[address]', 'Account address (defaults to connected wallet)')
204
+ .option('--json', 'Output JSON', false);
162
205
  addDAOAddressOptions(calculateCmd);
163
206
  calculateCmd.action(async (address, options) => {
164
207
  try {
165
- const governorAddr = resolveDAOAddress(options, { required: true });
208
+ const inputAddr = resolveDAOAddress(options, { required: true });
166
209
  const account = await resolveAccountAddress(address);
167
210
  if (!account) {
168
211
  throw new Error('No address provided. Pass an address or connect a wallet.');
169
212
  }
170
213
 
171
214
  const provider = getProvider();
215
+
216
+ // Auto-resolve Governor from SubDAO if needed
217
+ const resolved = await resolveGovernorAddress(inputAddr, provider);
218
+ const governorAddr = resolved.governor;
219
+
172
220
  const governor = new ethers.Contract(governorAddr, GOVERNOR_ABI, provider);
173
221
 
174
- ui.output('\nVoting Power Breakdown');
175
- ui.keyValue({
176
- 'Account': account,
177
- 'Governor': governorAddr
178
- });
222
+ if (!options.json) {
223
+ ui.output('\nVoting Power Breakdown');
224
+ const displayInfo = { 'Account': account };
225
+ if (resolved.isSubDAO) {
226
+ displayInfo['SubDAO'] = resolved.subdao;
227
+ displayInfo['Governor'] = governorAddr;
228
+ } else {
229
+ displayInfo['Governor'] = governorAddr;
230
+ }
231
+ ui.keyValue(displayInfo);
232
+ }
179
233
 
180
234
  // Check if multipliers are enabled
181
235
  const isEnabled = await governor.isMultiplierEnabled().catch(() => false);
182
236
  const votingToken = await governor.token();
237
+ const baseVotesToken = await governor.baseVotesToken().catch(() => null);
238
+ const isWrapperMode = isEnabled || (baseVotesToken && baseVotesToken.toLowerCase() !== votingToken.toLowerCase());
183
239
 
184
- if (!isEnabled) {
240
+ if (!isWrapperMode) {
185
241
  // Simple ERC20Votes - no multiplier
186
242
  const token = new ethers.Contract(votingToken, TOKEN_ABI, provider);
187
243
  const [name, symbol, decimals, balance, votes] = await Promise.all([
@@ -192,6 +248,22 @@ function register(program) {
192
248
  token.getVotes(account)
193
249
  ]);
194
250
 
251
+ if (options.json) {
252
+ ui.json({
253
+ ok: true,
254
+ account,
255
+ governor: governorAddr,
256
+ token: votingToken,
257
+ name,
258
+ symbol,
259
+ decimals,
260
+ balance: balance.toString(),
261
+ votingPower: votes.toString(),
262
+ multiplier: '1x'
263
+ });
264
+ return;
265
+ }
266
+
195
267
  ui.output(`\n${name} (${symbol})`);
196
268
  ui.keyValue({
197
269
  'Balance': `${ethers.formatUnits(balance, decimals)}`,
@@ -203,7 +275,7 @@ function register(program) {
203
275
 
204
276
  // Multiplied votes - get breakdown
205
277
  const multipliedVotes = new ethers.Contract(votingToken, MULTIPLIED_VOTES_ABI, provider);
206
- const baseTokenAddr = await multipliedVotes.baseToken();
278
+ const baseTokenAddr = baseVotesToken || await multipliedVotes.baseToken();
207
279
  const nftAddr = await multipliedVotes.multiplierNFT();
208
280
  const basis = await multipliedVotes.BASIS();
209
281
 
@@ -221,6 +293,22 @@ function register(program) {
221
293
  const baseDisplay = ethers.formatUnits(baseVotes, decimals);
222
294
  const effectiveDisplay = ethers.formatUnits(effectiveVotes, decimals);
223
295
 
296
+ if (options.json) {
297
+ ui.json({
298
+ ok: true,
299
+ account,
300
+ governor: governorAddr,
301
+ baseToken: baseTokenAddr,
302
+ votingToken,
303
+ baseVotes: baseVotes.toString(),
304
+ multiplier: multiplier.toString(),
305
+ multiplierDisplay: multDisplay,
306
+ effectiveVotes: effectiveVotes.toString(),
307
+ hasBonus: !!hasBonus
308
+ });
309
+ return;
310
+ }
311
+
224
312
  ui.output(`\n${name} (${symbol}) with NFT Multipliers`);
225
313
  ui.keyValue({
226
314
  'Base Votes': `${baseDisplay} ${symbol}`,
@@ -176,9 +176,10 @@ Automation:
176
176
  .option('--dao <address>', 'DAO address (auto-detected from context if not provided)')
177
177
  .option('--account <address>', 'Account to check (default: your wallet)')
178
178
  .option('-v, --verbose', 'Show detailed output')
179
+ .option('--json', 'Output JSON', false)
179
180
  .action(async (opts) => {
180
181
  try {
181
- ui.configure({ verbose: opts.verbose });
182
+ ui.configure({ verbose: opts.verbose, json: opts.json });
182
183
  const rpcUrl = cliConfig.resolveRpcUrl();
183
184
  const provider = new ethers.JsonRpcProvider(rpcUrl);
184
185
  const nftAddr = cliConfig.resolveAddress('VOTING_MULTIPLIER_NFT_ADDRESS');
@@ -208,6 +209,18 @@ Automation:
208
209
  const multiplier = await contract.getMultiplier(account, daoAddr);
209
210
  const multiplierDisplay = (Number(multiplier) / 10000).toFixed(4);
210
211
 
212
+ if (opts.json) {
213
+ ui.json({
214
+ ok: true,
215
+ account,
216
+ dao: daoAddr,
217
+ nft: nftAddr,
218
+ multiplier: multiplier.toString(),
219
+ multiplierDisplay: `${multiplierDisplay}x`
220
+ });
221
+ return;
222
+ }
223
+
211
224
  ui.header(`Voting Multiplier for ${ui.formatAddress(account)}`);
212
225
  ui.keyValue([
213
226
  ['DAO', ui.formatAddress(daoAddr)],
@@ -269,10 +282,10 @@ Automation:
269
282
  throw new Error('Multiplier must be >= 100 (100 = 1x)');
270
283
  }
271
284
 
272
- // Build calldata for createTier
285
+ // Build calldata for governance-safe createTierViaGovernance
273
286
  const abi = resolveArtifact('contracts/VotingMultiplierNFT.sol/VotingMultiplierNFT.json').abi;
274
287
  const iface = new ethers.Interface(abi);
275
- const calldata = iface.encodeFunctionData('createTier', [
288
+ const calldata = iface.encodeFunctionData('createTierViaGovernance', [
276
289
  daoAddr,
277
290
  String(opts.name),
278
291
  BigInt(multiplier),
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const axios = require('axios');
4
- const { getAddress, id, parseUnits, formatUnits, Contract, MaxUint256 } = require('ethers');
4
+ const { getAddress, id, parseUnits, formatUnits, Contract, MaxUint256, ZeroAddress } = require('ethers');
5
5
  const { decryptAesGcm, fromB64 } = require('../utils/aes');
6
6
  const sdk = require('@sage-protocol/sdk');
7
7
  const { formatJson } = require('../utils/format');
@@ -244,6 +244,42 @@ const PERSONAL_REGISTRY_QUERY = `
244
244
  async function resolveOrCreatePersonalRegistry(wallet, opts = {}) {
245
245
  const ownerAddress = getAddress(await wallet.getAddress());
246
246
  const subgraphUrl = resolveSubgraphUrl(opts.subgraph);
247
+ const provider = wallet.provider || await getProvider();
248
+
249
+ const { resolveArtifact } = require('../utils/artifacts');
250
+ const PromptRegistryABI = resolveArtifact('contracts/PromptRegistry.sol/PromptRegistry.json').abi;
251
+
252
+ async function validateRegistryCandidate(registryAddr) {
253
+ try {
254
+ const code = await provider.getCode(registryAddr);
255
+ if (!code || code === '0x') {
256
+ return { ok: false, reason: 'no_code' };
257
+ }
258
+
259
+ const read = new Contract(registryAddr, PromptRegistryABI, provider);
260
+
261
+ // Ensure this registry is actually usable by the current wallet.
262
+ const govRole = await read.GOVERNANCE_ROLE().catch(() => null);
263
+ if (!govRole) return { ok: false, reason: 'missing_governance_role' };
264
+
265
+ const hasGovRole = await read.hasRole(govRole, ownerAddress).catch(() => null);
266
+ if (!hasGovRole) return { ok: false, reason: 'missing_governance_role_for_owner' };
267
+
268
+ // Personal registries should not be linked to a SubDAO.
269
+ const subdao = await read.subDAO().catch(() => null);
270
+ if (subdao && getAddress(subdao) !== getAddress(ZeroAddress)) {
271
+ return { ok: false, reason: 'linked_to_subdao', subdao: getAddress(subdao) };
272
+ }
273
+
274
+ // If paused, addPrompt will revert.
275
+ const paused = await read.paused().catch(() => false);
276
+ if (paused) return { ok: false, reason: 'paused' };
277
+
278
+ return { ok: true };
279
+ } catch (e) {
280
+ return { ok: false, reason: 'validation_error', error: e };
281
+ }
282
+ }
247
283
 
248
284
  // Try subgraph lookup first
249
285
  if (subgraphUrl) {
@@ -254,7 +290,14 @@ async function resolveOrCreatePersonalRegistry(wallet, opts = {}) {
254
290
  });
255
291
  const registries = resp.data?.data?.personalRegistries || [];
256
292
  if (registries.length > 0) {
257
- return getAddress(registries[0].id);
293
+ const candidate = getAddress(registries[0].id);
294
+ const validation = await validateRegistryCandidate(candidate);
295
+ if (validation.ok) return candidate;
296
+
297
+ if (!opts.json) {
298
+ const extra = validation.subdao ? ` (subDAO=${validation.subdao})` : '';
299
+ ui.warn(`Resolved personal registry ${candidate} is not usable (${validation.reason}${extra}). Creating a fresh registry...`);
300
+ }
258
301
  }
259
302
  } catch (_) {
260
303
  // Subgraph unavailable, fall through to create
@@ -309,6 +352,30 @@ async function addPromptToRegistry(wallet, registryAddr, promptData, opts = {})
309
352
  const PromptRegistryABI = resolveArtifact('contracts/PromptRegistry.sol/PromptRegistry.json').abi;
310
353
  const registry = new Contract(registryAddr, PromptRegistryABI, wallet);
311
354
 
355
+ // Proactive checks to surface actionable errors before spending gas.
356
+ try {
357
+ const ownerAddress = getAddress(await wallet.getAddress());
358
+ const govRole = await registry.GOVERNANCE_ROLE().catch(() => null);
359
+ if (!govRole) {
360
+ throw new Error(`Resolved registry ${registryAddr} does not expose GOVERNANCE_ROLE(); is this a valid PromptRegistry?`);
361
+ }
362
+ const hasGov = await registry.hasRole(govRole, ownerAddress).catch(() => null);
363
+ if (!hasGov) {
364
+ throw new Error(`Wallet ${ownerAddress} lacks GOVERNANCE_ROLE on registry ${registryAddr}. This usually means the registry is not your personal registry (or it was not initialized as personal).`);
365
+ }
366
+ const subdao = await registry.subDAO().catch(() => null);
367
+ if (subdao && getAddress(subdao) !== getAddress(ZeroAddress)) {
368
+ throw new Error(`Registry ${registryAddr} is linked to SubDAO ${getAddress(subdao)}; personal publish requires a personal registry (subDAO=0x0).`);
369
+ }
370
+ const paused = await registry.paused().catch(() => false);
371
+ if (paused) {
372
+ throw new Error(`Registry ${registryAddr} is paused; unpause it (DEFAULT_ADMIN_ROLE) or create a fresh personal registry.`);
373
+ }
374
+ } catch (e) {
375
+ // Re-throw with a cleaner message for JSON/non-JSON callers.
376
+ throw e;
377
+ }
378
+
312
379
  const title = promptData.title || promptData.name || promptData.key || 'Untitled';
313
380
  const tags = promptData.tags || [];
314
381
  const contentCID = promptData.contentCID || '';
@@ -105,7 +105,7 @@ async function fetchPlatformPrompts(opts = {}) {
105
105
 
106
106
  if (opts.search) {
107
107
  params.set('q', opts.search);
108
- const { body } = await client._fetchJson(`/discover/search?${params}`);
108
+ const { body } = await client._fetchJson(`/prompts/search?${params}`);
109
109
  return (body.results || []).map(r => ({
110
110
  ...r,
111
111
  source: 'platform',