@sage-protocol/cli 0.6.5 → 0.6.6

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.
@@ -479,7 +479,7 @@ function register(program) {
479
479
  )
480
480
  .addCommand(
481
481
  new Command('delegate-self')
482
- .description('Delegate voting power to yourself')
482
+ .description('Delegate voting power to yourself (enables you to vote)')
483
483
  .action(async () => {
484
484
  try {
485
485
  const manager = new SXXXManager();
@@ -491,6 +491,50 @@ function register(program) {
491
491
  }
492
492
  })
493
493
  )
494
+ .addCommand(
495
+ new Command('delegate')
496
+ .description('Delegate voting power to an address (use delegate-self to take it back)')
497
+ .argument('<address>', 'Address to delegate voting power to')
498
+ .option('-y, --yes', 'Skip confirmation prompt')
499
+ .option('--confirm', 'Confirm delegation (required without -y)')
500
+ .action(async (address, opts) => {
501
+ try {
502
+ const manager = new SXXXManager();
503
+ await manager.initialize();
504
+
505
+ // Get user's own address for comparison
506
+ let userAddress;
507
+ if (manager.signer) {
508
+ userAddress = await manager.signer.getAddress();
509
+ } else {
510
+ const WalletManager = require('../wallet-manager');
511
+ const walletManager = new WalletManager();
512
+ await walletManager.connect();
513
+ userAddress = walletManager.getAccount();
514
+ }
515
+
516
+ // If delegating to self, no confirmation needed
517
+ const isSelfDelegation = address.toLowerCase() === userAddress.toLowerCase();
518
+
519
+ if (!isSelfDelegation && !opts.yes && !opts.confirm) {
520
+ console.log('\n⚠️ WARNING: You are about to delegate your voting power to another address.');
521
+ console.log(` Delegatee: ${address}`);
522
+ console.log(' This means THEY will vote on your behalf, not you.\n');
523
+ console.log('To proceed, run one of:');
524
+ console.log(` sage sxxx delegate ${address} --confirm`);
525
+ console.log(` sage sxxx delegate ${address} -y\n`);
526
+ console.log('To vote yourself instead:');
527
+ console.log(' sage sxxx delegate-self\n');
528
+ process.exit(1);
529
+ }
530
+
531
+ await manager.delegateTo(address);
532
+ printSuggestions('sxxx:delegate');
533
+ } catch (error) {
534
+ handleCLIError('sxxx:delegate', error);
535
+ }
536
+ })
537
+ )
494
538
  .addCommand(
495
539
  new Command('delegation')
496
540
  .description('Check vote delegation status')
@@ -498,39 +498,68 @@ class PrivyAuthWalletManager {
498
498
  .loader { border: 3px solid #f3f3f3; border-top: 3px solid #000; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin: 0 auto; }
499
499
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
500
500
  #error-display { position: fixed; top: 0; left: 0; right: 0; background: #fee2e2; color: #dc2626; padding: 1rem; display: none; z-index: 1000; }
501
- /* Fix Privy modal CSS issues from esm.sh bundling */
502
- /* Target Privy modal by common patterns */
501
+
502
+ /* Privy CSS variable overrides for proper modal sizing */
503
+ :root {
504
+ --privy-border-radius-sm: 4px;
505
+ --privy-border-radius-md: 8px;
506
+ --privy-border-radius-lg: 12px;
507
+ --privy-color-background: #ffffff;
508
+ --privy-color-foreground: #1a1a1a;
509
+ --privy-color-accent: #000000;
510
+ }
511
+
512
+ /* Fix Privy modal height and positioning */
513
+ [data-privy-dialog],
514
+ [id*="privy-modal"],
515
+ [class*="privy-modal"],
516
+ div[role="dialog"] {
517
+ max-height: 90vh !important;
518
+ overflow-y: auto !important;
519
+ }
520
+
521
+ /* Ensure modal content has proper height */
522
+ [data-privy-dialog] > div,
523
+ [id*="privy-modal"] > div,
524
+ div[role="dialog"] > div {
525
+ min-height: 400px !important;
526
+ max-height: 85vh !important;
527
+ }
528
+
529
+ /* Fix iframe sizing issues */
530
+ [data-privy-dialog] iframe,
531
+ [id*="privy"] iframe,
532
+ div[role="dialog"] iframe {
533
+ min-height: 500px !important;
534
+ width: 100% !important;
535
+ border: none !important;
536
+ }
537
+
538
+ /* Fix input field styling */
503
539
  [id*="privy"] input,
504
540
  [class*="privy"] input,
505
- [class*="Privy"] input,
506
541
  div[role="dialog"] input {
507
- padding-right: 80px !important;
542
+ padding: 12px 16px !important;
508
543
  box-sizing: border-box !important;
544
+ font-size: 16px !important;
545
+ width: 100% !important;
509
546
  }
510
- /* Hide the submit button text that appears inline */
511
- [id*="privy"] button[type="submit"],
512
- [class*="privy"] button[type="submit"],
513
- [class*="Privy"] button[type="submit"],
514
- div[role="dialog"] button[type="submit"] {
515
- position: absolute !important;
516
- right: 12px !important;
517
- font-size: 0 !important;
518
- background: transparent !important;
519
- border: none !important;
520
- padding: 8px !important;
521
- }
522
- /* But keep the icon/arrow visible */
523
- [id*="privy"] button[type="submit"] svg,
524
- [class*="privy"] button[type="submit"] svg,
525
- div[role="dialog"] button[type="submit"] svg {
526
- width: 20px !important;
527
- height: 20px !important;
547
+
548
+ /* Fix button positioning */
549
+ [id*="privy"] button,
550
+ [class*="privy"] button,
551
+ div[role="dialog"] button {
552
+ padding: 12px 24px !important;
553
+ font-size: 14px !important;
528
554
  }
529
- /* Ensure forms have relative positioning */
555
+
556
+ /* Ensure forms have proper layout */
530
557
  [id*="privy"] form,
531
558
  [class*="privy"] form,
532
559
  div[role="dialog"] form {
533
- position: relative !important;
560
+ display: flex !important;
561
+ flex-direction: column !important;
562
+ gap: 12px !important;
534
563
  }
535
564
  </style>
536
565
  <script>
@@ -552,7 +581,7 @@ class PrivyAuthWalletManager {
552
581
  "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime",
553
582
  "react-dom": "https://esm.sh/react-dom@18.2.0",
554
583
  "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
555
- "@privy-io/react-auth": "https://esm.sh/@privy-io/react-auth@1.73.1?external=react,react-dom"
584
+ "@privy-io/react-auth": "https://esm.sh/@privy-io/react-auth@2.4.0?external=react,react-dom"
556
585
  }
557
586
  }
558
587
  </script>
@@ -447,16 +447,25 @@ class GovModeDetector {
447
447
  const proposerRole = await tl.PROPOSER_ROLE();
448
448
  const hasGovProposer = out.governor ? await tl.hasRole(proposerRole, out.governor) : false;
449
449
 
450
- if (out.governanceKind === 'TOKEN' && hasGovProposer) {
450
+ // If Governor has PROPOSER_ROLE on Timelock, route via governor-proposal
451
+ // This is the primary signal - governanceKind may be unknown if subdao context is missing
452
+ if (hasGovProposer) {
451
453
  out.route = 'governor-proposal';
452
- out.rationale.push('route: token governance + governor is proposer on timelock');
453
- } else {
454
- out.route = 'timelock-schedule';
455
- if (out.governanceKind === 'TOKEN' && !hasGovProposer) {
456
- out.rationale.push('route: token governance but governor lacks PROPOSER_ROLE (misconfigured)');
454
+ out.governorIsProposer = true;
455
+ if (out.governanceKind === 'TOKEN') {
456
+ out.rationale.push('route: token governance + governor is proposer on timelock');
457
457
  } else {
458
- out.rationale.push('route: schedule/execute directly via timelock');
458
+ // Governor has PROPOSER_ROLE but governanceKind wasn't detected - infer token governance
459
+ out.rationale.push('route: governor has PROPOSER_ROLE on timelock (inferred token governance)');
460
+ if (!out.governanceKind) out.governanceKind = 'TOKEN';
459
461
  }
462
+ } else if (out.governanceKind === 'OPERATOR' || !out.governanceKind) {
463
+ out.route = 'timelock-schedule';
464
+ out.rationale.push('route: schedule/execute directly via timelock (operator mode)');
465
+ } else {
466
+ // TOKEN governance but governor lacks PROPOSER_ROLE - misconfigured
467
+ out.route = 'timelock-schedule';
468
+ out.rationale.push('route: token governance but governor lacks PROPOSER_ROLE (misconfigured)');
460
469
  }
461
470
  return;
462
471
  } catch (_) {
@@ -46,7 +46,7 @@ function createOperatorScheduler(overrides = {}) {
46
46
 
47
47
  const timelock = new ethersLib.Contract(timelockAddress, timelockAbi, signer);
48
48
 
49
- logger.log('\n⚠️ Operator mode detected: Governor is not a Timelock proposer.');
49
+ logger.log('\n⚠️ Operator mode: scheduling directly via Timelock (Governor lacks PROPOSER_ROLE or is not configured).');
50
50
 
51
51
  const proposerRole = await timelock.PROPOSER_ROLE();
52
52
  const hasRole = await timelock.hasRole(proposerRole, proposerAddress);
@@ -123,8 +123,69 @@ function createProposalSubmitter(overrides = {}) {
123
123
  return { proposalId, transaction: tx, receipt };
124
124
  }
125
125
 
126
- function handleKnownError(error, { govAddress, proposalStake, sxxxAddress, rpcUrl }) {
126
+ const PROPOSAL_STATES = ['Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'];
127
+
128
+ function getProposalStateName(stateNum) {
129
+ return PROPOSAL_STATES[Number(stateNum)] || `Unknown (${stateNum})`;
130
+ }
131
+
132
+ async function handleKnownError(error, { govAddress, proposalStake, sxxxAddress, rpcUrl, governor, targets, values, calldatas, description, subdaoAddress }) {
127
133
  const message = (error?.message || String(error)).toLowerCase();
134
+ const errorData = error?.data || error?.error?.data || '';
135
+
136
+ // Detect GovernorUnexpectedProposalState (0x31b75e4d) - duplicate proposal
137
+ if (message.includes('0x31b75e4d') || message.includes('governorunexpectedproposalstate') || errorData.includes('0x31b75e4d')) {
138
+ logger.log?.('');
139
+ logger.log?.('❌ A proposal with the same content already exists.');
140
+ logger.log?.('');
141
+
142
+ // Try to compute and look up the existing proposal
143
+ if (governor && targets && calldatas && description) {
144
+ try {
145
+ const descHash = ethersLib.keccak256(ethersLib.toUtf8Bytes(description));
146
+ const proposalId = await governor.hashProposal(targets, values || [0n], calldatas, descHash);
147
+ const state = await governor.state(proposalId);
148
+ const stateName = getProposalStateName(state);
149
+
150
+ logger.log?.('📋 Existing proposal:');
151
+ logger.log?.(` ID: ${proposalId}`);
152
+ logger.log?.(` State: ${stateName}`);
153
+
154
+ // Get deadline if available
155
+ try {
156
+ const deadline = await governor.proposalDeadline(proposalId);
157
+ logger.log?.(` Deadline: block ${deadline}`);
158
+ } catch (_) { /* deadline not available */ }
159
+
160
+ logger.log?.('');
161
+ logger.log?.('💡 Next steps:');
162
+
163
+ const subdaoFlag = subdaoAddress ? ` --subdao ${subdaoAddress}` : '';
164
+ if (state === 0n || state === 1n) {
165
+ // Pending or Active - can vote
166
+ logger.log?.(` • Vote FOR: sage gov vote ${proposalId} 1${subdaoFlag}`);
167
+ logger.log?.(` • Check status: sage gov status ${proposalId}${subdaoFlag}`);
168
+ } else if (state === 5n) {
169
+ // Queued - ready to execute
170
+ logger.log?.(` • Execute: sage gov execute ${proposalId}${subdaoFlag}`);
171
+ } else if (state === 7n) {
172
+ // Already executed
173
+ logger.log?.(' • This proposal was already executed successfully.');
174
+ logger.log?.(' • If you need a new update, modify the description to create a unique proposal.');
175
+ } else {
176
+ logger.log?.(` • Check status: sage gov status ${proposalId}${subdaoFlag}`);
177
+ }
178
+ } catch (lookupErr) {
179
+ logger.log?.(' Could not look up existing proposal details.');
180
+ logger.log?.(' Use `sage proposals inbox` to find active proposals.');
181
+ }
182
+ } else {
183
+ logger.log?.('💡 Use `sage proposals inbox` to find and vote on existing proposals.');
184
+ }
185
+ logger.log?.('');
186
+ return { aborted: true, reason: 'duplicate-proposal' };
187
+ }
188
+
128
189
  if (message.includes('allowance')) {
129
190
  logger.log?.('❌ Unexpected allowance error during proposal submission');
130
191
  logger.log?.('Please ensure you have approved SXXX for the Governor:');
@@ -157,7 +218,7 @@ function createProposalSubmitter(overrides = {}) {
157
218
  try {
158
219
  return await submitProposal(options);
159
220
  } catch (error) {
160
- const known = handleKnownError(error, options);
221
+ const known = await handleKnownError(error, options);
161
222
  if (known) return known;
162
223
  throw error;
163
224
  }
@@ -708,6 +708,119 @@ class SXXXManager {
708
708
  }
709
709
  }
710
710
 
711
+ /**
712
+ * Delegate voting power to a specific address.
713
+ * Use this to delegate to a representative, or delegate-self to take back your voting power.
714
+ * @param {string} delegatee - The address to delegate voting power to
715
+ */
716
+ async delegateTo(delegatee) {
717
+ try {
718
+ // Validate delegatee address
719
+ if (!delegatee || !ethers.isAddress(delegatee)) {
720
+ throw new Error(`Invalid delegatee address: ${delegatee}`);
721
+ }
722
+
723
+ console.log(colors.blue('🗳️ Delegating votes to address...'));
724
+ console.log('Delegatee:', delegatee);
725
+
726
+ // Get user address - handle both Cast and regular signers
727
+ let userAddress;
728
+ if (!this.signer) {
729
+ const WalletManager = require('./wallet-manager');
730
+ const walletManager = new WalletManager();
731
+ await walletManager.connect();
732
+ userAddress = walletManager.getAccount();
733
+ } else {
734
+ userAddress = await this.signer.getAddress();
735
+ }
736
+
737
+ console.log('Your address:', userAddress);
738
+
739
+ // Check if delegating to self
740
+ const isSelfDelegation = delegatee.toLowerCase() === userAddress.toLowerCase();
741
+ if (isSelfDelegation) {
742
+ console.log('ℹ️ Delegating to yourself (same as delegate-self)');
743
+ }
744
+
745
+ // Use base votes token
746
+ const vt = this.votesToken;
747
+ if (!vt) {
748
+ console.log('⚠️ No base voting token resolved for this context.');
749
+ throw new Error('No base voting token resolved for delegation.');
750
+ }
751
+
752
+ // Check current delegation
753
+ try {
754
+ const currentDelegate = await vt.delegates(userAddress);
755
+ console.log('Currently delegates to:', currentDelegate);
756
+
757
+ if (currentDelegate.toLowerCase() === delegatee.toLowerCase()) {
758
+ console.log('✅ Already delegated to this address!');
759
+ return;
760
+ }
761
+
762
+ // Warn if revoking self-delegation
763
+ if (currentDelegate.toLowerCase() === userAddress.toLowerCase() && !isSelfDelegation) {
764
+ console.log(colors.yellow('⚠️ You are transferring your voting power to someone else.'));
765
+ console.log(colors.yellow(' To take back your voting power later: sage sxxx delegate-self'));
766
+ }
767
+ } catch (e) {
768
+ console.log('Could not check current delegation');
769
+ }
770
+
771
+ // Build transaction
772
+ const to = vt.target;
773
+ const data = vt.interface.encodeFunctionData('delegate', [delegatee]);
774
+
775
+ // Estimate gas
776
+ let gasLimit;
777
+ try {
778
+ const est = await (this.provider || vt.runner?.provider).estimateGas({ from: userAddress, to, data, value: 0 }).catch(() => null);
779
+ if (est) gasLimit = est + (est / 5n);
780
+ } catch (_) { /* non-fatal */ }
781
+
782
+ const withTimeout = (p, ms, label = 'operation') => {
783
+ const t = Number(ms || 15000);
784
+ return Promise.race([
785
+ p,
786
+ new Promise((_, rej) => setTimeout(() => rej(new Error(`${label} timed out after ${t}ms`)), t))
787
+ ]);
788
+ };
789
+
790
+ const actionLabel = isSelfDelegation ? 'self-delegation' : `delegation to ${delegatee.slice(0, 10)}...`;
791
+
792
+ if (!this.signer) {
793
+ console.log(colors.yellow('🔐 Using Cast wallet for delegation transaction...'));
794
+ const CastWalletManager = require('./cast-wallet-manager');
795
+ const castWallet = new CastWalletManager();
796
+ await castWallet.connect();
797
+ const transaction = { to, data, gasLimit: gasLimit || 120000 };
798
+ const result = await withSpinner(`Submitting ${actionLabel}`, async () => {
799
+ return withTimeout(castWallet.signTransaction(transaction), process.env.SAGE_SEND_TIMEOUT_MS, 'delegate(send)');
800
+ }, { successText: ({ hash }) => `Delegation submitted - tx ${hash}` });
801
+ return result;
802
+ } else {
803
+ const sent = await withSpinner(`Submitting ${actionLabel}`, async () => {
804
+ const send = this.signer.sendTransaction({ to, data, gasLimit, value: 0 });
805
+ const tx = await withTimeout(send, process.env.SAGE_SEND_TIMEOUT_MS, 'delegate(send)');
806
+ const rc = await waitForReceipt(this.provider, tx, Number(process.env.SAGE_TX_WAIT_MS || 60000));
807
+ return { tx, receipt: rc };
808
+ }, { successText: ({ tx }) => `Delegation confirmed - tx ${tx.hash}` });
809
+
810
+ if (isSelfDelegation) {
811
+ console.log('✅ Voting power returned to you.');
812
+ } else {
813
+ console.log(`✅ Voting power delegated to ${delegatee}`);
814
+ console.log('💡 To take back your voting power: sage sxxx delegate-self');
815
+ }
816
+ return sent.receipt;
817
+ }
818
+ } catch (error) {
819
+ console.error('❌ Failed to delegate votes:', error.message);
820
+ throw error;
821
+ }
822
+ }
823
+
711
824
  async checkDelegation(address) {
712
825
  try {
713
826
  // Get target address - handle both Cast and regular signers
@@ -39,7 +39,7 @@ async function deriveDestination({ provider, subdaoOpt, workspaceSubdao, aliasRe
39
39
  } else {
40
40
  const mode = await detectGovMode({ provider, subdao: out.subdao, governor: out.governor, timelock: out.timelock });
41
41
  // Prefer the explicit 3-axis profile when available
42
- if (mode?.governanceKind === 'TOKEN') {
42
+ if (mode?.governanceKind === 'TOKEN' || mode?.route === 'governor-proposal') {
43
43
  out.route = 'community';
44
44
  out.reason = mode.route || 'token-governance';
45
45
  } else if (mode?.playbook === 'council-closed' || mode?.route === 'safe-multisig') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-protocol/cli",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Sage Protocol CLI for managing AI prompt libraries",
5
5
  "bin": {
6
6
  "sage": "./bin/sage.js"