@sage-protocol/cli 0.8.2 → 0.8.4

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.

@@ -11,19 +11,21 @@ function register(program) {
11
11
  .option('--governor <address>', 'Governor address to check')
12
12
  .option('--address <address>', 'Address to check (defaults to connected wallet)')
13
13
  .option('-v, --verbose', 'Show detailed output')
14
+ .option('--json', 'Output JSON', false)
14
15
  .action(async (opts) => {
15
16
  try {
16
- ui.configure({ verbose: opts.verbose });
17
+ ui.configure({ verbose: opts.verbose, json: opts.json });
18
+ const show = !opts.json;
17
19
 
18
20
  const WalletManager = require('../wallet-manager');
19
21
  const { resolveGovContext } = require('../utils/gov-context');
20
22
 
21
23
  const invokedAsStakeStatus = process.argv.includes('stake-status');
22
- if (invokedAsStakeStatus) {
24
+ if (invokedAsStakeStatus && show) {
23
25
  ui.warn('`sage stake-status` is deprecated. Use `sage voting-status` (governance uses delegation, not staking).');
24
26
  }
25
27
 
26
- const rpcUrl = process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || 'https://base-sepolia.publicnode.com';
28
+ const rpcUrl = process.env.SAGE_RPC_URL || process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || 'https://base-sepolia.publicnode.com';
27
29
  const provider = new ethers.JsonRpcProvider(rpcUrl);
28
30
 
29
31
  // Get wallet manager and connect
@@ -32,11 +34,13 @@ function register(program) {
32
34
  const signer = walletManager.getSigner();
33
35
  const address = opts.address || await signer.getAddress();
34
36
 
35
- ui.header('Voting Power Status');
36
- ui.keyValue([
37
- ['Address', address],
38
- ['RPC', rpcUrl]
39
- ]);
37
+ if (show) {
38
+ ui.header('Voting Power Status');
39
+ ui.keyValue([
40
+ ['Address', address],
41
+ ['RPC', rpcUrl]
42
+ ]);
43
+ }
40
44
 
41
45
  // Resolve governance context
42
46
  const ctx = await resolveGovContext({
@@ -48,14 +52,17 @@ function register(program) {
48
52
  });
49
53
 
50
54
  if (!ctx.governor) {
51
- ui.error('No Governor found');
55
+ if (show) ui.error('No Governor found');
56
+ else ui.json({ ok: false, error: 'No Governor found' });
52
57
  return;
53
58
  }
54
59
 
55
- ui.keyValue([
56
- ['Governor', ctx.governor],
57
- ['SubDAO', ctx.subdao || 'N/A']
58
- ]);
60
+ if (show) {
61
+ ui.keyValue([
62
+ ['Governor', ctx.governor],
63
+ ['SubDAO', ctx.subdao || 'N/A']
64
+ ]);
65
+ }
59
66
 
60
67
  // Get Governor's voting token and multiplier status
61
68
  const govABI = [
@@ -78,19 +85,21 @@ function register(program) {
78
85
  const baseVotesTokenAddr = await governor.baseVotesToken().catch(() => votingToken);
79
86
  const isWrapperMode = isMultiplierEnabled || (baseVotesTokenAddr && baseVotesTokenAddr !== votingToken);
80
87
 
81
- if (isWrapperMode) {
82
- ui.keyValue([
83
- ['Voting Token (wrapper)', votingToken],
84
- ['Base Token (SXXX)', baseVotesTokenAddr],
85
- ['NFT Multipliers', isMultiplierEnabled ? 'Enabled' : 'Unknown']
86
- ]);
87
- } else {
88
- ui.keyValue([
89
- ['Voting Token', votingToken],
90
- ['NFT Multipliers', 'Disabled']
91
- ]);
88
+ if (show) {
89
+ if (isWrapperMode) {
90
+ ui.keyValue([
91
+ ['Voting Token (wrapper)', votingToken],
92
+ ['Base Token (SXXX)', baseVotesTokenAddr],
93
+ ['NFT Multipliers', isMultiplierEnabled ? 'Enabled' : 'Unknown']
94
+ ]);
95
+ } else {
96
+ ui.keyValue([
97
+ ['Voting Token', votingToken],
98
+ ['NFT Multipliers', 'Disabled']
99
+ ]);
100
+ }
101
+ ui.info(`Proposal Threshold: ${ethers.formatEther(threshold)} tokens`);
92
102
  }
93
- ui.info(`Proposal Threshold: ${ethers.formatEther(threshold)} tokens`);
94
103
 
95
104
  // In multiplier mode, governor.token() is an IVotes wrapper (MultipliedVotes) which is NOT an ERC20,
96
105
  // so ERC20 calls (name/symbol/decimals/balanceOf) can revert. Always read ERC20 metadata from the base token.
@@ -117,35 +126,39 @@ function register(program) {
117
126
  provider.getBlockNumber()
118
127
  ]);
119
128
 
120
- ui.newline();
121
- ui.header('Token Details');
122
- ui.keyValue([
123
- ['Name', name],
124
- ['Symbol', symbol],
125
- ['Decimals', decimals],
126
- ['Balance', `${ethers.formatUnits(balance, decimals)} ${symbol}`],
127
- ['Delegates to', delegates]
128
- ]);
129
+ if (show) {
130
+ ui.newline();
131
+ ui.header('Token Details');
132
+ ui.keyValue([
133
+ ['Name', name],
134
+ ['Symbol', symbol],
135
+ ['Decimals', decimals],
136
+ ['Balance', `${ethers.formatUnits(balance, decimals)} ${symbol}`],
137
+ ['Delegates to', delegates]
138
+ ]);
139
+ }
129
140
 
130
141
  // Check voting power at different blocks
131
142
  const blocksToCheck = [currentBlock - 1, currentBlock - 2, currentBlock - 5];
132
- ui.newline();
133
- ui.header('Voting Power History');
143
+ if (show) {
144
+ ui.newline();
145
+ ui.header('Voting Power History');
146
+ }
134
147
 
135
148
  for (const block of blocksToCheck) {
136
149
  try {
137
150
  const votes = await votesToken.getPastVotes(address, block);
138
151
  const formattedVotes = ethers.formatUnits(votes, decimals);
139
152
  const status = votes >= threshold ? ui.symbols.check : ui.symbols.cross;
140
- ui.output(` Block ${block}: ${formattedVotes} ${symbol} ${status}`);
153
+ if (show) ui.output(` Block ${block}: ${formattedVotes} ${symbol} ${status}`);
141
154
  } catch (e) {
142
- ui.output(` Block ${block}: Error - ${e.message}`);
155
+ if (show) ui.output(` Block ${block}: Error - ${e.message}`);
143
156
  }
144
157
  }
145
158
 
146
159
  // Note: stakeToken() removed from SubDAO contracts (threshold-based governance)
147
160
  // Voting power now comes from SXXX token delegation, not staking
148
- if (ctx.subdao) {
161
+ if (ctx.subdao && show) {
149
162
  ui.newline();
150
163
  ui.header('Governance Model');
151
164
  ui.output(' This SubDAO uses threshold-based governance.');
@@ -154,8 +167,10 @@ function register(program) {
154
167
 
155
168
  // Summary
156
169
  const currentVotes = await votesToken.getPastVotes(address, currentBlock - 1).catch(() => 0n);
157
- ui.newline();
158
- ui.header('Summary');
170
+ if (show) {
171
+ ui.newline();
172
+ ui.header('Summary');
173
+ }
159
174
 
160
175
  // If multipliers are enabled, show both base and effective voting power
161
176
  if (isWrapperMode) {
@@ -163,32 +178,91 @@ function register(program) {
163
178
  const [baseVotes, multiplier, effectiveVotes] = await governor.getVotingBreakdown(address);
164
179
  const multiplierDisplay = (Number(multiplier) / 10000).toFixed(2);
165
180
  const canPropose = effectiveVotes >= threshold;
166
- ui.keyValue([
167
- ['Base Voting Power', `${ethers.formatUnits(baseVotes, decimals)} ${symbol}`],
168
- ['NFT Multiplier', `${multiplierDisplay}x`],
169
- ['Effective Voting Power', `${ethers.formatUnits(effectiveVotes, decimals)} votes`],
170
- ['Proposal Threshold', `${ethers.formatEther(threshold)} tokens`],
171
- ['Can Propose', canPropose ? 'YES' : 'NO']
172
- ]);
181
+ if (show) {
182
+ ui.keyValue([
183
+ ['Base Voting Power', `${ethers.formatUnits(baseVotes, decimals)} ${symbol}`],
184
+ ['NFT Multiplier', `${multiplierDisplay}x`],
185
+ ['Effective Voting Power', `${ethers.formatUnits(effectiveVotes, decimals)} votes`],
186
+ ['Proposal Threshold', `${ethers.formatEther(threshold)} tokens`],
187
+ ['Can Propose', canPropose ? 'YES' : 'NO']
188
+ ]);
189
+ } else {
190
+ ui.json({
191
+ ok: true,
192
+ address,
193
+ rpcUrl,
194
+ governor: ctx.governor,
195
+ subdao: ctx.subdao || null,
196
+ votingToken,
197
+ baseToken: baseVotesTokenAddr,
198
+ isMultiplierEnabled: !!isMultiplierEnabled,
199
+ threshold: threshold.toString(),
200
+ balance: balance.toString(),
201
+ delegates,
202
+ baseVotes: baseVotes.toString(),
203
+ multiplier: multiplier.toString(),
204
+ effectiveVotes: effectiveVotes.toString(),
205
+ canPropose
206
+ });
207
+ return;
208
+ }
173
209
  } catch (e) {
174
210
  // Fallback if getVotingBreakdown fails
175
211
  const canPropose = currentVotes >= threshold;
212
+ if (show) {
213
+ ui.keyValue([
214
+ ['Current Voting Power', `${ethers.formatUnits(currentVotes, decimals)} ${symbol}`],
215
+ ['Proposal Threshold', `${ethers.formatEther(threshold)} tokens`],
216
+ ['Can Propose', canPropose ? 'YES' : 'NO']
217
+ ]);
218
+ } else {
219
+ ui.json({
220
+ ok: true,
221
+ address,
222
+ rpcUrl,
223
+ governor: ctx.governor,
224
+ subdao: ctx.subdao || null,
225
+ votingToken,
226
+ baseToken: baseVotesTokenAddr,
227
+ isMultiplierEnabled: !!isMultiplierEnabled,
228
+ threshold: threshold.toString(),
229
+ balance: balance.toString(),
230
+ delegates,
231
+ currentVotes: currentVotes.toString(),
232
+ canPropose
233
+ });
234
+ return;
235
+ }
236
+ }
237
+ } else {
238
+ const canPropose = currentVotes >= threshold;
239
+ if (show) {
176
240
  ui.keyValue([
177
241
  ['Current Voting Power', `${ethers.formatUnits(currentVotes, decimals)} ${symbol}`],
178
242
  ['Proposal Threshold', `${ethers.formatEther(threshold)} tokens`],
179
243
  ['Can Propose', canPropose ? 'YES' : 'NO']
180
244
  ]);
245
+ } else {
246
+ ui.json({
247
+ ok: true,
248
+ address,
249
+ rpcUrl,
250
+ governor: ctx.governor,
251
+ subdao: ctx.subdao || null,
252
+ votingToken,
253
+ baseToken: baseVotesTokenAddr,
254
+ isMultiplierEnabled: !!isMultiplierEnabled,
255
+ threshold: threshold.toString(),
256
+ balance: balance.toString(),
257
+ delegates,
258
+ currentVotes: currentVotes.toString(),
259
+ canPropose
260
+ });
261
+ return;
181
262
  }
182
- } else {
183
- const canPropose = currentVotes >= threshold;
184
- ui.keyValue([
185
- ['Current Voting Power', `${ethers.formatUnits(currentVotes, decimals)} ${symbol}`],
186
- ['Proposal Threshold', `${ethers.formatEther(threshold)} tokens`],
187
- ['Can Propose', canPropose ? 'YES' : 'NO']
188
- ]);
189
263
  }
190
264
 
191
- if (currentVotes < threshold) {
265
+ if (show && currentVotes < threshold) {
192
266
  ui.newline();
193
267
  ui.info('To gain voting power:');
194
268
  if (balance === 0n) {
@@ -22,10 +22,14 @@ function register(program) {
22
22
  try {
23
23
  ui.configure({ verbose: opts.verbose });
24
24
  const { ethers } = require('ethers');
25
- const rpcUrl = cliConfig.resolveRpcUrl();
25
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
26
26
  const provider = new ethers.JsonRpcProvider(rpcUrl);
27
27
  const tokenAddress = cliConfig.resolveAddress('SXXX_TOKEN_ADDRESS');
28
28
  if (!tokenAddress) throw new Error('SXXX_TOKEN_ADDRESS not set');
29
+ const code = await provider.getCode(tokenAddress);
30
+ if (!code || code === '0x' || code === '0x0') {
31
+ throw new Error(`SXXX contract not found at ${tokenAddress}. Check SAGE_RPC_URL/CHAIN_ID or import addresses.`);
32
+ }
29
33
  let targetAddress = address;
30
34
  // 1) Prefer configured default account from profile
31
35
  if (!targetAddress) {
@@ -46,9 +50,38 @@ function register(program) {
46
50
  } catch (_) {}
47
51
  }
48
52
  if (!targetAddress) throw new Error('No address provided and could not resolve wallet account');
49
- const abi = ['function balanceOf(address) view returns (uint256)'];
53
+ const abi = ['function balanceOf(address) view returns (uint256)', 'function symbol() view returns (string)'];
50
54
  const token = new ethers.Contract(tokenAddress, abi, provider);
51
- const bal = await token.balanceOf(targetAddress);
55
+
56
+ // Verify contract exists and is an ERC20
57
+ try {
58
+ const code = await provider.getCode(tokenAddress);
59
+ if (code === '0x') {
60
+ ui.error(`SXXX contract not deployed at ${tokenAddress}`);
61
+ ui.info(' This may mean the address is incorrect or the network is wrong.');
62
+ ui.info(' Check: RPC_URL, SXXX_TOKEN_ADDRESS, or run `sage doctor` for diagnostics.');
63
+ process.exit(1);
64
+ }
65
+ } catch (codeErr) {
66
+ // Ignore code check failure, proceed with balance check
67
+ }
68
+
69
+ let bal;
70
+ try {
71
+ bal = await token.balanceOf(targetAddress);
72
+ } catch (balErr) {
73
+ const errMsg = balErr.message || String(balErr);
74
+ if (errMsg.includes('BAD_DATA') || errMsg.includes('could not decode result')) {
75
+ ui.error(`Contract at ${tokenAddress} returned invalid data for balanceOf()`);
76
+ ui.info(' This usually means the address is not an ERC20 token contract.');
77
+ ui.info(' Possible fixes:');
78
+ ui.info(' - Check SXXX_TOKEN_ADDRESS is correct for your network');
79
+ ui.info(' - Verify RPC_URL points to the correct chain (Base Sepolia)');
80
+ ui.info(' - Run `sage doctor` for full diagnostics');
81
+ process.exit(1);
82
+ }
83
+ throw balErr;
84
+ }
52
85
  ui.success(`SXXX Balance for ${ui.formatAddress(targetAddress)}: ${ui.formatToken(bal, { symbol: 'SXXX' })}`);
53
86
  } catch (error) {
54
87
  handleCLIError('sxxx:balance', error, { exit: true });
@@ -62,7 +95,7 @@ function register(program) {
62
95
  .action(async (opts) => {
63
96
  try {
64
97
  ui.configure({ verbose: opts.verbose });
65
- const rpcUrl = cliConfig.resolveRpcUrl();
98
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
66
99
  const tokenAddress = cliConfig.resolveAddress('SXXX_TOKEN_ADDRESS');
67
100
  if (!tokenAddress) throw new Error('SXXX_TOKEN_ADDRESS not set (configure via .env or .sage profile)');
68
101
  const provider = new ethers.JsonRpcProvider(rpcUrl);
@@ -8,7 +8,8 @@
8
8
  const { ethers } = require('ethers');
9
9
 
10
10
  // Default RPC URL
11
- const DEFAULT_RPC = process.env.RPC_URL
11
+ const DEFAULT_RPC = process.env.SAGE_RPC_URL
12
+ || process.env.RPC_URL
12
13
  || process.env.BASE_SEPOLIA_RPC
13
14
  || 'https://base-sepolia.publicnode.com';
14
15
 
@@ -56,6 +56,8 @@ const COMMAND_CATALOG = {
56
56
  'execute',
57
57
  'status',
58
58
  'inspect',
59
+ 'decode',
60
+ 'summary',
59
61
  'watch',
60
62
  'power',
61
63
  'preflight',
@@ -83,14 +85,22 @@ const COMMAND_CATALOG = {
83
85
  'votes',
84
86
  'snapshot-power',
85
87
  'config',
86
- 'timelock-info',
87
- 'authority'
88
+ 'timelock',
89
+ 'authority',
90
+ // Added missing subcommands
91
+ 'decode',
92
+ 'summary',
93
+ 'cache-show',
94
+ 'last',
95
+ 'cancel',
96
+ 'cancelability'
88
97
  ],
89
98
  subcommandAliases: {
90
99
  ls: 'list',
91
100
  exec: 'execute',
92
101
  q: 'queue',
93
- status: 'status'
102
+ status: 'status',
103
+ 'timelock-info': 'timelock'
94
104
  }
95
105
  },
96
106
  library: {
@@ -225,6 +235,54 @@ const COMMAND_CATALOG = {
225
235
  needsSubcommand: false,
226
236
  subcommands: ['list', 'run'],
227
237
  subcommandAliases: { ls: 'list' }
238
+ },
239
+ nft: {
240
+ aliases: [],
241
+ needsSubcommand: true,
242
+ subcommands: ['doctor', 'list-tiers', 'my-multiplier', 'tier', 'mint', 'public-mint', 'auction'],
243
+ subcommandAliases: { list: 'list-tiers', tiers: 'list-tiers' }
244
+ },
245
+ sbt: {
246
+ aliases: [],
247
+ needsSubcommand: true,
248
+ subcommands: ['list-reasons', 'doctor', 'mint', 'revoke', 'propose-mint', 'propose-revoke'],
249
+ subcommandAliases: { list: 'list-reasons' }
250
+ },
251
+ council: {
252
+ aliases: [],
253
+ needsSubcommand: true,
254
+ subcommands: ['doctor', 'set-config', 'allow', 'show', 'suggest-allowlist', 'exec', 'schedule'],
255
+ subcommandAliases: { status: 'doctor' }
256
+ },
257
+ profile: {
258
+ aliases: [],
259
+ needsSubcommand: true,
260
+ subcommands: ['get', 'set', 'upload', 'interactive'],
261
+ subcommandAliases: { show: 'get' }
262
+ },
263
+ members: {
264
+ aliases: [],
265
+ needsSubcommand: true,
266
+ subcommands: ['list', 'current-stake'],
267
+ subcommandAliases: {}
268
+ },
269
+ multiplier: {
270
+ aliases: [],
271
+ needsSubcommand: true,
272
+ subcommands: ['status', 'calculate', 'describe', 'propose-tier', 'auction'],
273
+ subcommandAliases: {}
274
+ },
275
+ boost: {
276
+ aliases: [],
277
+ needsSubcommand: true,
278
+ subcommands: ['list', 'create', 'set-merkle-root', 'status', 'finalize', 'claim', 'fund'],
279
+ subcommandAliases: {}
280
+ },
281
+ 'voting-status': {
282
+ aliases: [],
283
+ needsSubcommand: false,
284
+ subcommands: [],
285
+ subcommandAliases: {}
228
286
  }
229
287
  };
230
288
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-protocol/cli",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Sage Protocol CLI for managing AI prompt libraries",
5
5
  "bin": {
6
6
  "sage": "./bin/sage.js"