@sage-protocol/cli 0.4.8 → 0.4.9

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.
@@ -376,19 +376,27 @@ class CastWalletManager {
376
376
  let walletData = this.loadCastWallet();
377
377
 
378
378
  if (!walletData) {
379
- // Create new Cast wallet with keystore
380
- console.log(colors.yellow('📝 No Cast wallet found. Creating new secure wallet...'));
381
- try {
382
- walletData = await this.createCastWallet();
383
- this.saveCastWallet(walletData);
384
- } catch (err) {
385
- const message = err?.message || '';
386
- if (/not a directory|No such file or directory/i.test(message)) {
387
- console.error(colors.yellow('💡 Cast expects the keystore directory to exist.'));
388
- console.error(colors.yellow(` Run: cast wallet new ${this.keystoreDir} ${this.keystoreName}`));
389
- console.error(colors.yellow(' or set SAGE_CAST_KEYSTORE_DIR to use an existing Foundry keystore (e.g., ~/.foundry/keystores).'));
379
+ // Try to auto-detect existing keystores before creating a new one
380
+ const detectedWallet = await this.detectExistingKeystore();
381
+ if (detectedWallet) {
382
+ walletData = detectedWallet;
383
+ this.saveCastWallet(walletData, { silent: true });
384
+ console.log(colors.green(`✅ Auto-detected existing keystore: ${path.basename(walletData.keystorePath)}`));
385
+ } else {
386
+ // Create new Cast wallet with keystore
387
+ console.log(colors.yellow('📝 No Cast wallet found. Creating new secure wallet...'));
388
+ try {
389
+ walletData = await this.createCastWallet();
390
+ this.saveCastWallet(walletData);
391
+ } catch (err) {
392
+ const message = err?.message || '';
393
+ if (/not a directory|No such file or directory/i.test(message)) {
394
+ console.error(colors.yellow('💡 Cast expects the keystore directory to exist.'));
395
+ console.error(colors.yellow(` Run: cast wallet new ${this.keystoreDir} ${this.keystoreName}`));
396
+ console.error(colors.yellow(' or set SAGE_CAST_KEYSTORE_DIR to use an existing Foundry keystore (e.g., ~/.foundry/keystores).'));
397
+ }
398
+ throw err;
390
399
  }
391
- throw err;
392
400
  }
393
401
  }
394
402
 
@@ -433,6 +441,128 @@ class CastWalletManager {
433
441
  }
434
442
  }
435
443
 
444
+ /**
445
+ * @dev Auto-detect existing keystores in the keystore directory
446
+ * @returns {Promise<Object|null>} Wallet data if found, null otherwise
447
+ */
448
+ async detectExistingKeystore() {
449
+ // Check both foundry keystores dir and project-local dir
450
+ const dirsToCheck = [
451
+ path.join(os.homedir(), '.foundry', 'keystores'),
452
+ this.keystoreDir
453
+ ].filter((dir, idx, arr) => fs.existsSync(dir) && arr.indexOf(dir) === idx);
454
+
455
+ const keystores = [];
456
+
457
+ for (const dir of dirsToCheck) {
458
+ try {
459
+ const files = fs.readdirSync(dir);
460
+ for (const f of files) {
461
+ // Skip hidden files and non-keystore files
462
+ if (f.startsWith('.')) continue;
463
+ const ksPath = path.join(dir, f);
464
+ const stat = fs.statSync(ksPath);
465
+ if (!stat.isFile()) continue;
466
+
467
+ try {
468
+ // First try to parse keystore JSON for address (geth format)
469
+ const content = fs.readFileSync(ksPath, 'utf8');
470
+ const ks = JSON.parse(content);
471
+ let addr = ks.address;
472
+ if (addr && !addr.startsWith('0x')) {
473
+ addr = '0x' + addr;
474
+ }
475
+ if (addr && addr.length === 42) {
476
+ keystores.push({ address: addr, keystorePath: ksPath, name: f });
477
+ continue;
478
+ }
479
+
480
+ // Cast keystores don't have address - need password to derive
481
+ // Try using CAST_PASSWORD env var if available
482
+ const password = process.env.CAST_PASSWORD || process.env.SAGE_CAST_PASSWORD;
483
+ if (password) {
484
+ const derivedAddr = await new Promise((resolve, reject) => {
485
+ const p = spawn(this.castCommand, ['wallet', 'address', '--keystore', ksPath, '--password', password]);
486
+ let out = ''; let err = '';
487
+ p.stdout.on('data', d => out += d.toString());
488
+ p.stderr.on('data', d => err += d.toString());
489
+ p.on('close', code => code === 0 ? resolve(out.trim()) : reject(new Error(err || `cast exited ${code}`)));
490
+ });
491
+ if (derivedAddr && derivedAddr.startsWith('0x')) {
492
+ keystores.push({ address: derivedAddr, keystorePath: ksPath, name: f });
493
+ }
494
+ } else {
495
+ // No password available - still add keystore but without address
496
+ // We'll resolve address later during connect
497
+ keystores.push({ address: null, keystorePath: ksPath, name: f });
498
+ }
499
+ } catch (_) {
500
+ // Not a valid keystore, skip
501
+ }
502
+ }
503
+ } catch (_) {
504
+ // Directory read failed, skip
505
+ }
506
+ }
507
+
508
+ if (keystores.length === 0) {
509
+ return null;
510
+ }
511
+
512
+ // If only one keystore, use it automatically
513
+ if (keystores.length === 1) {
514
+ return {
515
+ address: keystores[0].address,
516
+ keystorePath: keystores[0].keystorePath,
517
+ createdAt: new Date().toISOString()
518
+ };
519
+ }
520
+
521
+ // Multiple keystores found - prompt user to select (or use first if non-interactive)
522
+ if (!process.stdin.isTTY) {
523
+ // Non-interactive: use first keystore
524
+ console.log(colors.yellow(`ℹ️ Multiple keystores found, using first: ${keystores[0].name}`));
525
+ return {
526
+ address: keystores[0].address,
527
+ keystorePath: keystores[0].keystorePath,
528
+ createdAt: new Date().toISOString()
529
+ };
530
+ }
531
+
532
+ // Interactive: let user choose
533
+ console.log(colors.blue('\n📋 Multiple keystores found:'));
534
+ keystores.forEach((ks, idx) => {
535
+ const addrDisplay = ks.address || '(address will be resolved)';
536
+ console.log(` ${idx + 1}. ${ks.name} (${addrDisplay})`);
537
+ });
538
+
539
+ const rl = readline.createInterface({
540
+ input: process.stdin,
541
+ output: process.stdout
542
+ });
543
+
544
+ return new Promise((resolve) => {
545
+ rl.question(colors.blue('Select keystore (1-' + keystores.length + '): '), (answer) => {
546
+ rl.close();
547
+ const idx = parseInt(answer, 10) - 1;
548
+ if (idx >= 0 && idx < keystores.length) {
549
+ resolve({
550
+ address: keystores[idx].address,
551
+ keystorePath: keystores[idx].keystorePath,
552
+ createdAt: new Date().toISOString()
553
+ });
554
+ } else {
555
+ // Default to first
556
+ resolve({
557
+ address: keystores[0].address,
558
+ keystorePath: keystores[0].keystorePath,
559
+ createdAt: new Date().toISOString()
560
+ });
561
+ }
562
+ });
563
+ });
564
+ }
565
+
436
566
  /**
437
567
  * @dev Create a new Cast wallet with keystore
438
568
  * @returns {Promise<Object>} Wallet data
@@ -5,8 +5,8 @@
5
5
  "args": ["/Users/twells/Dev/sage-protocol/cli/mcp-server-stdio.js"],
6
6
  "env": {
7
7
  "RPC_URL": "https://sepolia.base.org",
8
- "LIBRARY_REGISTRY_ADDRESS": "0x490CA76839CFF14455C9fD0680c445A10D96e04d",
9
- "SUBDAO_FACTORY_ADDRESS": "0xafE34a581cc64393704D40bE964390119E58654c",
8
+ "LIBRARY_REGISTRY_ADDRESS": "0x93addf18f90894c18F2912dbDB9D5bdCA202430a",
9
+ "SUBDAO_FACTORY_ADDRESS": "0x15523C6232e6e63E50811e37eb63A4ddD1484f12",
10
10
  "SUBGRAPH_URL": "https://api.goldsky.com/api/public/project_cmhxp0fppsbdd01q56xp2gqw9/subgraphs/sxxx-protocol/1.0.5/gn",
11
11
  "PINATA_JWT": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mb3JtYXRpb24iOnsiaWQiOiI4ODE2MWM1Ni05NTYzLTQyNTUtYTU4Ni0xMzBiMTUyMWIxNmIiLCJlbWFpbCI6InRlcnJlbmV0d2VsbHM0N0BnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGluX3BvbGljeSI6eyJyZWdpb25zIjpbeyJkZXNpcmVkUmVwbGljYXRpb25Db3VudCI6MSwiaWQiOiJGUkExIn0seyJkZXNpcmVkUmVwbGljYXRpb25Db3VudCI6MSwiaWQiOiJOWUMxIn1dLCJ2ZXJzaW9uIjoxfSwibWZhX2VuYWJsZWQiOmZhbHNlLCJzdGF0dXMiOiJBQ1RJVkUifSwiYXV0aGVudGljYXRpb25UeXBlIjoic2NvcGVkS2V5Iiwic2NvcGVkS2V5S2V5IjoiMDRjZWY4NzE5OTg1ODE1M2U5Y2IiLCJzY29wZWRLZXlTZWNyZXQiOiJmNDFjMDVhZTNjN2EzMTliOTAxZjhiNGI3YjMxMDljMThmNjgyZmRkZGY3OGU1ZmZkZjQ4MTM4OTQ0OWUyOGVjIiwiZXhwIjoxNzk1NTMzNTA4fQ.tQKFU7aZ3EKb7RkB99QTaWVduaaKaKjEa1nk43aeSlo"
12
12
  }
@@ -295,6 +295,7 @@ function register(program) {
295
295
  .option('--owners <addresses>', 'Safe owners (comma-separated)')
296
296
  .option('--threshold <n>', 'Safe threshold')
297
297
  .option('--min-stake <amount>', 'Minimum stake amount (SXXX)')
298
+ .option('--burn-amount <amount>', 'SXXX burn amount in tokens (default: 1000)')
298
299
  .option('--voting-period <duration>', 'Voting period (e.g., "3 days", "36 hours", "720 minutes")')
299
300
  .option('--quorum-bps <bps>', 'Quorum basis points (e.g., 200 for 2%)')
300
301
  .option('--proposal-threshold <amount>', 'Proposal threshold (wei or token units as string)')
@@ -0,0 +1,250 @@
1
+ const { Command } = require('commander');
2
+ const { WorkerClient } = require('../services/ipfs/worker-client');
3
+ const config = require('../config');
4
+ const { handleCLIError } = require('../utils/error-handler');
5
+ const { DEFAULT_WORKER_BASE } = require('../services/ipfs/onboarding');
6
+
7
+ function register(program) {
8
+ const discover = new Command('discover')
9
+ .description('Discover and search prompts');
10
+
11
+ // Search subcommand
12
+ discover
13
+ .command('search <query>')
14
+ .description('Search prompts with keyword and semantic matching')
15
+ .option('-c, --category <category>', 'Filter by category (coding, writing, analysis, creative, assistant)')
16
+ .option('-t, --tags <tags>', 'Filter by tags (comma-separated)')
17
+ .option('-a, --author <address>', 'Filter by author address')
18
+ .option('-l, --limit <number>', 'Number of results', '20')
19
+ .option('--no-vector', 'Disable semantic/vector search')
20
+ .option('--json', 'Output as JSON')
21
+ .action(async (query, options) => {
22
+ try {
23
+ const baseUrl = config.readIpfsConfig?.().workerBaseUrl || process.env.SAGE_IPFS_WORKER_URL || process.env.SAGE_IPFS_UPLOAD_URL || DEFAULT_WORKER_BASE;
24
+ const token = process.env.SAGE_IPFS_UPLOAD_TOKEN;
25
+ const address = process.env.SAGE_PAY_TO_PIN_ADDRESS;
26
+
27
+ const client = new WorkerClient({ baseUrl, token, address });
28
+
29
+ const params = new URLSearchParams({
30
+ q: query,
31
+ limit: options.limit,
32
+ });
33
+
34
+ if (options.category) params.set('category', options.category);
35
+ if (options.author) params.set('author', options.author);
36
+ if (options.tags) params.set('tags', options.tags);
37
+ if (options.vector === false) params.set('useVector', 'false');
38
+
39
+ const { body } = await client._fetchJson(`/discover/search?${params}`);
40
+
41
+ if (options.json) {
42
+ console.log(JSON.stringify(body, null, 2));
43
+ return;
44
+ }
45
+
46
+ const results = body.results || [];
47
+ console.log(`Found ${results.length} results:\n`);
48
+
49
+ for (const r of results) {
50
+ console.log(` ${r.name || r.cid || 'Untitled'}`);
51
+ console.log(` CID: ${r.cid}`);
52
+ const category = r.category || 'N/A';
53
+ const score = r.combinedScore != null ? r.combinedScore.toFixed(2) : r.score != null ? r.score.toFixed(2) : 'N/A';
54
+ console.log(` Category: ${category} | Score: ${score}`);
55
+ if (r.tags && r.tags.length) {
56
+ console.log(` Tags: ${r.tags.join(', ')}`);
57
+ }
58
+ if (r.description) {
59
+ console.log(` Description: ${r.description}`);
60
+ }
61
+ console.log('');
62
+ }
63
+ } catch (error) {
64
+ handleCLIError('discover:search', error, { exit: true });
65
+ }
66
+ });
67
+
68
+ // Trending subcommand
69
+ discover
70
+ .command('trending')
71
+ .description('Get trending prompts')
72
+ .option('-w, --window <window>', 'Time window (1h, 24h, 7d, 30d)', '7d')
73
+ .option('-c, --category <category>', 'Filter by category')
74
+ .option('-l, --limit <number>', 'Number of results', '10')
75
+ .option('--json', 'Output as JSON')
76
+ .action(async (options) => {
77
+ try {
78
+ const baseUrl = config.readIpfsConfig?.().workerBaseUrl || process.env.SAGE_IPFS_WORKER_URL || process.env.SAGE_IPFS_UPLOAD_URL || DEFAULT_WORKER_BASE;
79
+ const token = process.env.SAGE_IPFS_UPLOAD_TOKEN;
80
+ const address = process.env.SAGE_PAY_TO_PIN_ADDRESS;
81
+
82
+ const client = new WorkerClient({ baseUrl, token, address });
83
+
84
+ const params = new URLSearchParams({
85
+ limit: options.limit,
86
+ });
87
+
88
+ if (options.window) params.set('window', options.window);
89
+ if (options.category) params.set('category', options.category);
90
+
91
+ const { body } = await client._fetchJson(`/trends/top?${params}`);
92
+
93
+ if (options.json) {
94
+ console.log(JSON.stringify(body, null, 2));
95
+ return;
96
+ }
97
+
98
+ const results = body.results || [];
99
+ console.log(`Trending prompts (${options.window}):\n`);
100
+
101
+ for (const r of results) {
102
+ const name = r.name || r.cid || 'Untitled';
103
+ console.log(` ${name}`);
104
+ console.log(` CID: ${r.cid}`);
105
+ const trendScore = r.trendScore != null ? r.trendScore.toFixed(2) : 'N/A';
106
+ console.log(` Trend Score: ${trendScore}`);
107
+ if (r.category) {
108
+ console.log(` Category: ${r.category}`);
109
+ }
110
+ if (r.tags && r.tags.length) {
111
+ console.log(` Tags: ${r.tags.join(', ')}`);
112
+ }
113
+ console.log('');
114
+ }
115
+ } catch (error) {
116
+ handleCLIError('discover:trending', error, { exit: true });
117
+ }
118
+ });
119
+
120
+ // Similar subcommand
121
+ discover
122
+ .command('similar <cid>')
123
+ .description('Find prompts similar to a given prompt')
124
+ .option('-l, --limit <number>', 'Number of results', '5')
125
+ .option('-m, --method <method>', 'Search method (vector, keyword, hybrid)', 'hybrid')
126
+ .option('--json', 'Output as JSON')
127
+ .action(async (cid, options) => {
128
+ try {
129
+ const baseUrl = config.readIpfsConfig?.().workerBaseUrl || process.env.SAGE_IPFS_WORKER_URL || process.env.SAGE_IPFS_UPLOAD_URL || DEFAULT_WORKER_BASE;
130
+ const token = process.env.SAGE_IPFS_UPLOAD_TOKEN;
131
+ const address = process.env.SAGE_PAY_TO_PIN_ADDRESS;
132
+
133
+ const client = new WorkerClient({ baseUrl, token, address });
134
+
135
+ const params = new URLSearchParams({
136
+ limit: options.limit,
137
+ method: options.method,
138
+ });
139
+
140
+ const { body } = await client._fetchJson(`/discover/similar/${cid}?${params}`);
141
+
142
+ if (options.json) {
143
+ console.log(JSON.stringify(body, null, 2));
144
+ return;
145
+ }
146
+
147
+ const sourceName = body.source?.name || cid;
148
+ console.log(`Similar to: ${sourceName}\n`);
149
+
150
+ const similar = body.similar || [];
151
+ if (!similar.length) {
152
+ console.log('No similar prompts found.');
153
+ return;
154
+ }
155
+
156
+ for (const r of similar) {
157
+ console.log(` ${r.name || 'Untitled'}`);
158
+ console.log(` CID: ${r.cid}`);
159
+ const similarity = r.similarity != null ? (r.similarity * 100).toFixed(1) : 'N/A';
160
+ console.log(` Similarity: ${similarity}%`);
161
+ if (r.sharedTags && r.sharedTags.length) {
162
+ console.log(` Shared tags: ${r.sharedTags.join(', ')}`);
163
+ }
164
+ if (r.category) {
165
+ console.log(` Category: ${r.category}`);
166
+ }
167
+ console.log('');
168
+ }
169
+ } catch (error) {
170
+ handleCLIError('discover:similar', error, { exit: true });
171
+ }
172
+ });
173
+
174
+ // Match subcommand
175
+ discover
176
+ .command('match')
177
+ .description('Get personalized prompt recommendations')
178
+ .option('-t, --tags <tags>', 'Preferred tags (comma-separated)')
179
+ .option('-c, --categories <categories>', 'Preferred categories (comma-separated)')
180
+ .option('-l, --limit <number>', 'Number of results', '10')
181
+ .option('--diversify', 'Ensure variety in results')
182
+ .option('--json', 'Output as JSON')
183
+ .action(async (options) => {
184
+ try {
185
+ const baseUrl = config.readIpfsConfig?.().workerBaseUrl || process.env.SAGE_IPFS_WORKER_URL || process.env.SAGE_IPFS_UPLOAD_URL || DEFAULT_WORKER_BASE;
186
+ const token = process.env.SAGE_IPFS_UPLOAD_TOKEN;
187
+ const address = process.env.SAGE_PAY_TO_PIN_ADDRESS;
188
+
189
+ const client = new WorkerClient({ baseUrl, token, address });
190
+
191
+ const payload = {
192
+ limit: parseInt(options.limit, 10),
193
+ };
194
+
195
+ if (options.tags) {
196
+ payload.preferredTags = options.tags.split(',').map(t => t.trim()).filter(t => t.length);
197
+ }
198
+
199
+ if (options.categories) {
200
+ payload.preferredCategories = options.categories.split(',').map(c => c.trim()).filter(c => c.length);
201
+ }
202
+
203
+ if (options.diversify) {
204
+ payload.diversify = true;
205
+ }
206
+
207
+ const { body } = await client._fetchJson('/discover/match', {
208
+ method: 'POST',
209
+ headers: client.headers(),
210
+ body: JSON.stringify(payload),
211
+ });
212
+
213
+ if (options.json) {
214
+ console.log(JSON.stringify(body, null, 2));
215
+ return;
216
+ }
217
+
218
+ const matches = body.matches || [];
219
+ console.log('Recommended prompts:\n');
220
+
221
+ if (!matches.length) {
222
+ console.log('No recommendations found. Try adjusting your preferences.');
223
+ return;
224
+ }
225
+
226
+ for (const r of matches) {
227
+ console.log(` ${r.name || 'Untitled'}`);
228
+ console.log(` CID: ${r.cid}`);
229
+ const matchScore = r.matchScore != null ? (r.matchScore * 100).toFixed(1) : 'N/A';
230
+ console.log(` Match Score: ${matchScore}%`);
231
+ if (r.matchReasons && r.matchReasons.length) {
232
+ console.log(` Reasons: ${r.matchReasons.join(', ')}`);
233
+ }
234
+ if (r.category) {
235
+ console.log(` Category: ${r.category}`);
236
+ }
237
+ if (r.tags && r.tags.length) {
238
+ console.log(` Tags: ${r.tags.join(', ')}`);
239
+ }
240
+ console.log('');
241
+ }
242
+ } catch (error) {
243
+ handleCLIError('discover:match', error, { exit: true });
244
+ }
245
+ });
246
+
247
+ program.addCommand(discover);
248
+ }
249
+
250
+ module.exports = { register };
@@ -502,6 +502,217 @@ function register(program) {
502
502
  }
503
503
  })
504
504
  );
505
+ // Migration: Pinata -> Worker
506
+ ipfsCommand.addCommand(
507
+ new Command('migrate')
508
+ .description('Migrate prompts from Pinata to Sage Worker (downloads from Pinata and re-uploads to worker)')
509
+ .option('--dry-run', 'Preview what would be migrated without uploading', false)
510
+ .option('--limit <n>', 'Max number of pins to migrate', (v) => parseInt(v, 10), 100)
511
+ .option('--offset <n>', 'Pagination offset for Pinata list', (v) => parseInt(v, 10), 0)
512
+ .option('--status <status>', 'Filter pins by status (pinned|unpinning|pinning|all)', 'pinned')
513
+ .option('--metadata-name <name>', 'Filter pins by metadata name (partial match)')
514
+ .option('--metadata-type <type>', 'Filter pins by metadata keyvalue type (e.g., sage-prompt)')
515
+ .option('--json', 'Output results as JSON')
516
+ .option('--worker-url <url>', 'Worker base URL', process.env.SAGE_IPFS_WORKER_URL || DEFAULT_WORKER_BASE)
517
+ .option('--worker-token <token>', 'Worker bearer token', process.env.SAGE_IPFS_UPLOAD_TOKEN)
518
+ .option('--gateway <url>', 'IPFS gateway for downloads', process.env.SAGE_IPFS_GATEWAY)
519
+ .action(async (opts) => {
520
+ const axios = require('axios');
521
+ try {
522
+ // Load Pinata credentials from config
523
+ const ipfsConfig = config.readIpfsConfig ? config.readIpfsConfig() : {};
524
+ const pinataJwt = ipfsConfig.pinataJwt || process.env.PINATA_JWT;
525
+ const pinataApiKey = ipfsConfig.pinataApiKey || process.env.PINATA_API_KEY;
526
+ const pinataSecretKey = ipfsConfig.pinataSecretKey || process.env.PINATA_SECRET_API_KEY;
527
+ const pinataApiUrl = ipfsConfig.apiUrl || 'https://api.pinata.cloud';
528
+
529
+ if (!pinataJwt && (!pinataApiKey || !pinataSecretKey)) {
530
+ throw new Error('Pinata credentials not configured. Run `sage ipfs setup --use-pinata` first or set PINATA_JWT env var.');
531
+ }
532
+
533
+ const headers = pinataJwt
534
+ ? { Authorization: `Bearer ${pinataJwt}` }
535
+ : { pinata_api_key: pinataApiKey, pinata_secret_api_key: pinataSecretKey };
536
+
537
+ // Build query params for pinList
538
+ const params = {
539
+ pageLimit: Math.min(opts.limit, 1000),
540
+ pageOffset: opts.offset,
541
+ };
542
+ if (opts.status && opts.status !== 'all') {
543
+ params.status = opts.status;
544
+ }
545
+ if (opts.metadataName) {
546
+ params['metadata[name]'] = opts.metadataName;
547
+ }
548
+ if (opts.metadataType) {
549
+ params['metadata[keyvalues][type]'] = JSON.stringify({ value: opts.metadataType, op: 'eq' });
550
+ }
551
+
552
+ if (!opts.json) {
553
+ console.log('📋 Fetching pins from Pinata...');
554
+ }
555
+
556
+ const listResponse = await axios.get(`${pinataApiUrl}/data/pinList`, {
557
+ headers,
558
+ params,
559
+ timeout: 30000,
560
+ });
561
+
562
+ const pins = listResponse?.data?.rows || [];
563
+ const totalCount = listResponse?.data?.count || pins.length;
564
+
565
+ if (pins.length === 0) {
566
+ if (opts.json) {
567
+ console.log(JSON.stringify({ migrated: 0, failed: 0, skipped: 0, total: 0, errors: [] }));
568
+ } else {
569
+ console.log('📭 No pins found matching criteria.');
570
+ }
571
+ return;
572
+ }
573
+
574
+ if (!opts.json) {
575
+ console.log(`📊 Found ${pins.length} pin(s) (total: ${totalCount})`);
576
+ if (opts.dryRun) {
577
+ console.log('🔍 Dry run mode - previewing only:\n');
578
+ }
579
+ }
580
+
581
+ // Create worker manager for uploads
582
+ const workerManager = opts.dryRun ? null : new IPFSManager({
583
+ provider: 'worker',
584
+ workerBaseUrl: opts.workerUrl,
585
+ uploadToken: opts.workerToken,
586
+ gateway: opts.gateway,
587
+ warm: true,
588
+ });
589
+ if (workerManager) await workerManager.initialize();
590
+
591
+ const results = {
592
+ migrated: 0,
593
+ failed: 0,
594
+ skipped: 0,
595
+ total: pins.length,
596
+ errors: [],
597
+ items: [],
598
+ };
599
+
600
+ for (const pin of pins) {
601
+ const cid = pin.ipfs_pin_hash;
602
+ const name = pin.metadata?.name || 'unknown';
603
+ const type = pin.metadata?.keyvalues?.type || 'unknown';
604
+
605
+ if (opts.dryRun) {
606
+ if (!opts.json) {
607
+ console.log(` - ${cid} (${name}, type: ${type})`);
608
+ }
609
+ results.items.push({ cid, name, type, action: 'would-migrate' });
610
+ results.migrated++;
611
+ continue;
612
+ }
613
+
614
+ try {
615
+ // Download from IPFS gateway
616
+ const gateway = opts.gateway || 'https://ipfs.io';
617
+ const downloadUrl = `${gateway.replace(/\/$/, '')}/ipfs/${cid}`;
618
+
619
+ if (!opts.json) {
620
+ process.stdout.write(` ⏳ Migrating ${cid} (${name})... `);
621
+ }
622
+
623
+ const downloadResponse = await axios.get(downloadUrl, {
624
+ timeout: 30000,
625
+ responseType: 'text',
626
+ headers: { Accept: 'application/json,text/plain,*/*' },
627
+ });
628
+
629
+ let content = downloadResponse.data;
630
+ let promptPayload;
631
+
632
+ // Try to parse as JSON first
633
+ try {
634
+ const parsed = typeof content === 'string' ? JSON.parse(content) : content;
635
+ promptPayload = {
636
+ name: parsed.name || name,
637
+ description: parsed.description || '',
638
+ content: parsed.content || (typeof content === 'string' ? content : JSON.stringify(parsed)),
639
+ timestamp: parsed.timestamp || Date.now(),
640
+ version: parsed.version || '1.0.0',
641
+ };
642
+ } catch {
643
+ // Not JSON, treat as raw content
644
+ promptPayload = {
645
+ name: name,
646
+ description: `Migrated from Pinata (${cid})`,
647
+ content: typeof content === 'string' ? content : JSON.stringify(content),
648
+ timestamp: Date.now(),
649
+ version: '1.0.0',
650
+ };
651
+ }
652
+
653
+ // Upload to worker
654
+ const uploadResult = await workerManager.client.uploadPrompt({
655
+ prompt: promptPayload,
656
+ provider: 'worker',
657
+ warm: true,
658
+ });
659
+
660
+ if (!opts.json) {
661
+ console.log(`✅ → ${uploadResult.cid}`);
662
+ }
663
+
664
+ results.migrated++;
665
+ results.items.push({
666
+ originalCid: cid,
667
+ newCid: uploadResult.cid,
668
+ name,
669
+ status: 'migrated',
670
+ });
671
+
672
+ } catch (err) {
673
+ if (!opts.json) {
674
+ console.log(`❌ Failed: ${err.message}`);
675
+ }
676
+ results.failed++;
677
+ results.errors.push({ cid, name, error: err.message });
678
+ results.items.push({
679
+ originalCid: cid,
680
+ name,
681
+ status: 'failed',
682
+ error: err.message,
683
+ });
684
+ }
685
+ }
686
+
687
+ if (opts.json) {
688
+ console.log(JSON.stringify(results, null, 2));
689
+ } else {
690
+ console.log(`\n📊 Migration complete:`);
691
+ console.log(` Migrated: ${results.migrated}`);
692
+ console.log(` Failed: ${results.failed}`);
693
+ console.log(` Skipped: ${results.skipped}`);
694
+ if (results.errors.length > 0) {
695
+ console.log('\n⚠️ Errors:');
696
+ for (const e of results.errors) {
697
+ console.log(` - ${e.cid}: ${e.error}`);
698
+ }
699
+ }
700
+ }
701
+
702
+ } catch (error) {
703
+ if (opts.json) {
704
+ console.log(JSON.stringify({ error: error.message }));
705
+ } else {
706
+ console.error('❌ Migration failed:', error.message);
707
+ if (error.response?.data) {
708
+ console.error(' Details:', JSON.stringify(error.response.data));
709
+ }
710
+ }
711
+ process.exit(1);
712
+ }
713
+ })
714
+ );
715
+
505
716
  program.addCommand(ipfsCommand);
506
717
  }
507
718
 
package/dist/cli/index.js CHANGED
@@ -138,7 +138,7 @@ program
138
138
 
139
139
  // Reordered to highlight consolidated namespaces ('prompts' and 'gov')
140
140
  const commandGroups = [
141
- { title: 'Content (new)', modules: ['prompts', 'project', 'personal', 'interview', 'creator', 'contributor', 'bounty'] },
141
+ { title: 'Content (new)', modules: ['prompts', 'project', 'personal', 'interview', 'creator', 'contributor', 'bounty', 'discover'] },
142
142
  { title: 'Governance (new)', modules: ['proposals', 'governance', 'dao', 'timelock', 'members', 'gov-config', 'stake-status'] },
143
143
  { title: 'Treasury', modules: ['treasury', 'safe', 'boost'] },
144
144
  { title: 'Ops & Utilities', modules: ['config', 'wallet', 'sxxx', 'ipfs', 'ipns', 'context', 'doctor', 'factory', 'upgrade', 'resolve', 'dry-run-queue', 'mcp', 'start', 'wizard', 'init', 'dao-config', 'pin', 'council', 'sbt', 'completion', 'help', 'hook'] },
@@ -217,6 +217,7 @@ const commandModules = {
217
217
  creator: require('./commands/creator.js'),
218
218
  contributor: require('./commands/contributor.js'),
219
219
  bounty: require('./commands/bounty.js'),
220
+ discover: require('./commands/discover.js'),
220
221
  proposals: require('./commands/proposals.js'),
221
222
  governance: require('./commands/governance.js'),
222
223
  dao: require('./commands/dao.js'),
@@ -170,24 +170,11 @@ class IPFSManager {
170
170
  throw new Error('IPFS SDK adapter unavailable. Update @sage-protocol/sdk to >= 0.1.1.');
171
171
  }
172
172
  const self = this;
173
- // Only require an interactive wallet signer when using the worker provider.
174
- // Pinata/web3 storage flows should not block on wallet connection.
175
- // When not using worker, don't pass worker config at all to prevent SDK from trying to use it.
176
- // Filter out worker-related env vars when using pinata/web3/simulate to prevent SDK from detecting worker config
177
- const filteredEnv = { ...(opts.env || process.env) };
178
- const shouldFilterWorkerEnv = this.ipfsProvider === 'pinata' || this.ipfsProvider === 'web3' || this.ipfsProvider === 'simulate' || this.ipfsProvider === 'w3s';
179
- if (shouldFilterWorkerEnv) {
180
- delete filteredEnv.SAGE_IPFS_WORKER_URL;
181
- delete filteredEnv.SAGE_IPFS_UPLOAD_URL;
182
- delete filteredEnv.SAGE_IPFS_UPLOAD_TOKEN;
183
- delete filteredEnv.SAGE_IPFS_WORKER_CHALLENGE_PATH;
184
- delete filteredEnv.SAGE_IPFS_WORKER_UPLOAD_PATH;
185
- delete filteredEnv.SAGE_IPFS_WORKER_PIN_PATH;
186
- delete filteredEnv.SAGE_IPFS_WORKER_WARM_PATH;
187
- }
188
-
173
+ // Always include worker config if we have a worker base URL configured.
174
+ // This allows per-call provider selection to use worker even if the default is pinata.
175
+ // The SDK's resolveProviderOrder will respect the explicit provider option at call time.
189
176
  const clientConfig = {
190
- env: filteredEnv,
177
+ env: opts.env || process.env,
191
178
  provider: this.ipfsProvider,
192
179
  gateway: this.gateway,
193
180
  timeoutMs: this.timeoutMs,
@@ -201,41 +188,22 @@ class IPFSManager {
201
188
  web3Token: this.w3sToken,
202
189
  };
203
190
 
204
- // Only include worker-related config when actually using worker provider
205
- // When provider is explicitly set to 'pinata', 'web3', or 'simulate', explicitly set worker config to empty/undefined
206
- // This prevents SDK from trying worker first in resolveProviderOrder
207
- if (this.ipfsProvider === 'worker') {
191
+ // Include worker config if we have a base URL, so per-call --provider worker works
192
+ if (this.workerBaseUrl) {
193
+ clientConfig.workerBaseUrl = this.workerBaseUrl;
194
+ clientConfig.workerUploadUrl = this.uploadUrl;
195
+ clientConfig.workerUploadToken = this.uploadToken;
196
+ clientConfig.workerChallengePath = this.workerChallengePath;
197
+ clientConfig.workerUploadPath = this.workerUploadPath;
198
+ clientConfig.workerPinPath = this.workerPinPath;
199
+ clientConfig.workerWarmPath = this.workerWarmPath;
200
+ // Add signer for SIWE auth if no token is set
208
201
  if (!this.uploadToken) {
209
- // Worker with signer auth
210
- clientConfig.workerBaseUrl = this.workerBaseUrl;
211
- clientConfig.workerUploadUrl = this.uploadUrl;
212
- clientConfig.workerUploadToken = this.uploadToken;
213
- clientConfig.workerChallengePath = this.workerChallengePath;
214
- clientConfig.workerUploadPath = this.workerUploadPath;
215
- clientConfig.workerPinPath = this.workerPinPath;
216
- clientConfig.workerWarmPath = this.workerWarmPath;
217
202
  clientConfig.workerSigner = async () => self.#resolveWorkerSigner();
218
203
  if (typeof opts.workerGetAuth === 'function') {
219
204
  clientConfig.workerGetAuth = opts.workerGetAuth;
220
205
  }
221
- } else {
222
- // Worker with token auth
223
- clientConfig.workerBaseUrl = this.workerBaseUrl;
224
- clientConfig.workerUploadUrl = this.uploadUrl;
225
- clientConfig.workerUploadToken = this.uploadToken;
226
- clientConfig.workerChallengePath = this.workerChallengePath;
227
- clientConfig.workerUploadPath = this.workerUploadPath;
228
- clientConfig.workerPinPath = this.workerPinPath;
229
- clientConfig.workerWarmPath = this.workerWarmPath;
230
206
  }
231
- } else {
232
- // Explicitly set worker config to empty/undefined when not using worker
233
- // to prevent SDK from detecting it from env vars
234
- clientConfig.workerBaseUrl = '';
235
- clientConfig.workerUploadUrl = '';
236
- clientConfig.workerUploadToken = '';
237
- clientConfig.workerSigner = undefined;
238
- clientConfig.workerGetAuth = undefined;
239
207
  }
240
208
 
241
209
  return adapter.ipfs.createClient(clientConfig);
@@ -337,11 +305,14 @@ class IPFSManager {
337
305
  filePath,
338
306
  };
339
307
 
308
+ // Use explicit provider from options, or fall back to manager's configured provider
309
+ const effectiveProvider = options.provider || this.ipfsProvider;
310
+
340
311
  const { cid, provider } = await withSpinner('Uploading prompt to IPFS', async () => {
341
312
  return await this.#withBackoff(() => this.client.uploadPrompt({
342
313
  prompt,
343
314
  providers: options.providers,
344
- provider: options.provider,
315
+ provider: effectiveProvider,
345
316
  warm: options.warm ?? this.shouldWarm,
346
317
  gateways: options.gateways,
347
318
  }), { retries: Number(process.env.SAGE_IPFS_UPLOAD_RETRIES || 2), baseMs: Number(process.env.SAGE_IPFS_UPLOAD_BACKOFF_MS || 400) });
@@ -379,10 +350,13 @@ class IPFSManager {
379
350
  async uploadJson(jsonObject, name = 'metadata', options = {}) {
380
351
  try {
381
352
  await this.#refreshClientWithSecretsIfNeeded();
353
+ // Use explicit provider from options, or fall back to manager's configured provider
354
+ const effectiveProvider = options.provider || this.ipfsProvider;
355
+
382
356
  const { cid, provider } = await withSpinner('Uploading JSON to IPFS', async () => {
383
357
  return await this.client.uploadJson(jsonObject, name, {
384
358
  providers: options.providers,
385
- provider: options.provider,
359
+ provider: effectiveProvider,
386
360
  warm: options.warm ?? this.shouldWarm,
387
361
  gateways: options.gateways,
388
362
  });
@@ -20,8 +20,8 @@ Create or update your `.env` file with the required configuration:
20
20
  # Subgraph (preferred) + Blockchain
21
21
  SUBGRAPH_URL=https://api.goldsky.com/api/public/project_cmhxp0fppsbdd01q56xp2gqw9/subgraphs/sxxx-protocol/1.0.5/gn
22
22
  RPC_URL=https://base-sepolia.publicnode.com
23
- LIBRARY_REGISTRY_ADDRESS=0x490CA76839CFF14455C9fD0680c445A10D96e04d
24
- SUBDAO_FACTORY_ADDRESS=0xafE34a581cc64393704D40bE964390119E58654c
23
+ LIBRARY_REGISTRY_ADDRESS=0x93addf18f90894c18F2912dbDB9D5bdCA202430a
24
+ SUBDAO_FACTORY_ADDRESS=0x15523C6232e6e63E50811e37eb63A4ddD1484f12
25
25
 
26
26
  # Optional: Custom IPFS Gateway
27
27
  IPFS_GATEWAY=https://ipfs.io/ipfs
@@ -67,8 +67,8 @@ You should see a JSON response listing the available tools.
67
67
  "env": {
68
68
  "SUBGRAPH_URL": "https://api.goldsky.com/api/public/project_cmhxp0fppsbdd01q56xp2gqw9/subgraphs/sxxx-protocol/1.0.5/gn",
69
69
  "RPC_URL": "https://base-sepolia.publicnode.com",
70
- "LIBRARY_REGISTRY_ADDRESS": "0x490CA76839CFF14455C9fD0680c445A10D96e04d",
71
- "SUBDAO_FACTORY_ADDRESS": "0xafE34a581cc64393704D40bE964390119E58654c"
70
+ "LIBRARY_REGISTRY_ADDRESS": "0x93addf18f90894c18F2912dbDB9D5bdCA202430a",
71
+ "SUBDAO_FACTORY_ADDRESS": "0x15523C6232e6e63E50811e37eb63A4ddD1484f12"
72
72
  }
73
73
  }
74
74
  }
@@ -959,11 +959,19 @@ class PrivySigner {
959
959
  }
960
960
 
961
961
  async estimateGas(transaction) {
962
- return await this.provider.estimateGas(transaction);
962
+ const tx = { ...(transaction || {}) };
963
+ if (!tx.from) {
964
+ tx.from = this.walletManager.account;
965
+ }
966
+ return await this.provider.estimateGas(tx);
963
967
  }
964
968
 
965
969
  async call(transaction) {
966
- return await this.provider.call(transaction);
970
+ const tx = { ...(transaction || {}) };
971
+ if (!tx.from) {
972
+ tx.from = this.walletManager.account;
973
+ }
974
+ return await this.provider.call(tx);
967
975
  }
968
976
 
969
977
  async getTransactionCount(blockTag) {
@@ -2,7 +2,7 @@ const path = require('path');
2
2
 
3
3
  const DEFAULT_GATEWAY = 'https://ipfs.dev.sageprotocol.io/ipfs';
4
4
  const DEFAULT_WORKER_BASE = 'https://api.sageprotocol.io';
5
- const DEFAULT_WORKER_CHALLENGE_PATH = '/auth/challenge';
5
+ const DEFAULT_WORKER_CHALLENGE_PATH = '/ipfs/auth/challenge';
6
6
  const DEFAULT_WORKER_UPLOAD_PATH = '/ipfs/upload';
7
7
  const DEFAULT_WORKER_PIN_PATH = '/ipfs/pin';
8
8
  const DEFAULT_WORKER_WARM_PATH = '/ipfs/warm';
@@ -13,10 +13,10 @@ const TOOL_REGISTRY = {
13
13
  category: 'prompt_workspace',
14
14
  keywords: ['search', 'find', 'prompt', 'query', 'look', 'locate', 'discover'],
15
15
  negativeKeywords: ['create', 'new', 'publish', 'delete'],
16
- description: 'Search prompts across local workspace and on-chain libraries.',
17
- whenToUse: 'Use when you need to find existing prompts by keyword, tag, or content.',
16
+ description: 'Search prompts using hybrid keyword and semantic search with trend ranking.',
17
+ whenToUse: 'Use when you need to find existing prompts by keyword, tag, category, or semantic similarity.',
18
18
  requiredParams: ['query'],
19
- optionalParams: ['source', 'subdao', 'tags', 'includeContent'],
19
+ optionalParams: ['source', 'subdao', 'tags', 'category', 'author', 'limit', 'useVector'],
20
20
  weight: 1.0,
21
21
  },
22
22
  list_prompts: {
@@ -363,14 +363,34 @@ const TOOL_REGISTRY = {
363
363
  },
364
364
  trending_prompts: {
365
365
  category: 'discovery',
366
- keywords: ['trending', 'popular', 'recent', 'discover', 'prompt'],
366
+ keywords: ['trending', 'popular', 'recent', 'discover', 'prompt', 'hot'],
367
367
  negativeKeywords: ['create'],
368
- description: 'List trending prompts from recent LibraryRegistry updates.',
368
+ description: 'Get trending prompts ranked by usage, purchases, forks, and recency.',
369
369
  whenToUse: 'Use when you want to discover popular or recently active prompts.',
370
370
  requiredParams: [],
371
- optionalParams: ['decayMinutes', 'limit'],
371
+ optionalParams: ['window', 'category', 'limit'],
372
372
  weight: 0.9,
373
373
  },
374
+ match_prompts: {
375
+ category: 'discovery',
376
+ keywords: ['match', 'recommend', 'suggest', 'personalize', 'preference', 'similar'],
377
+ negativeKeywords: ['search', 'trending'],
378
+ description: 'Find prompts matching user preferences based on recent usage and preferred tags/categories.',
379
+ whenToUse: 'Use when you want personalized prompt recommendations based on user history or preferences.',
380
+ requiredParams: [],
381
+ optionalParams: ['recentPrompts', 'preferredTags', 'preferredCategories', 'excludeAuthors', 'limit', 'diversify'],
382
+ weight: 1.1,
383
+ },
384
+ find_similar: {
385
+ category: 'discovery',
386
+ keywords: ['similar', 'related', 'like', 'comparable', 'alike'],
387
+ negativeKeywords: ['search', 'trending', 'match'],
388
+ description: 'Find prompts similar to a given prompt using semantic similarity and keyword overlap.',
389
+ whenToUse: 'Use when you have a specific prompt and want to find related alternatives.',
390
+ requiredParams: ['cid'],
391
+ optionalParams: ['limit', 'method'],
392
+ weight: 1.0,
393
+ },
374
394
  help: {
375
395
  category: 'discovery',
376
396
  keywords: ['help', 'usage', 'docs', 'explain'],
@@ -9,7 +9,11 @@ function clamp(value, { min, max }) {
9
9
 
10
10
  function normalizeSources(source) {
11
11
  if (source === 'all' || !source) {
12
- return ['local', 'onchain'];
12
+ // Include worker discovery for hybrid search with trends
13
+ return ['local', 'onchain', 'worker'];
14
+ }
15
+ if (source === 'worker') {
16
+ return ['worker'];
13
17
  }
14
18
  return [source];
15
19
  }
@@ -17,6 +21,7 @@ function normalizeSources(source) {
17
21
  function createUnifiedPromptSearcher({
18
22
  collectLocalPrompts,
19
23
  collectOnchainPrompts,
24
+ collectWorkerPrompts,
20
25
  formatUnifiedResults,
21
26
  } = {}) {
22
27
  if (typeof collectLocalPrompts !== 'function') {
@@ -28,15 +33,18 @@ function createUnifiedPromptSearcher({
28
33
  if (typeof formatUnifiedResults !== 'function') {
29
34
  throw new Error('createUnifiedPromptSearcher requires formatUnifiedResults function');
30
35
  }
36
+ // collectWorkerPrompts is optional - if not provided, worker source is skipped
31
37
 
32
38
  return async function searchPromptsUnified({
33
39
  query = '',
34
40
  source = 'all',
35
41
  subdao = '',
36
42
  tags = [],
43
+ category = '',
37
44
  includeContent = false,
38
45
  page = 1,
39
46
  pageSize = 10,
47
+ useVector = true,
40
48
  } = {}) {
41
49
  try {
42
50
  const sources = normalizeSources(source);
@@ -62,6 +70,51 @@ function createUnifiedPromptSearcher({
62
70
  }
63
71
  }
64
72
 
73
+ // Worker discovery - hybrid search with semantic matching and trend ranking
74
+ if (sources.includes('worker') && typeof collectWorkerPrompts === 'function') {
75
+ const workerLimit = Math.min(MAX_PAGE_SIZE, normalizedPage * normalizedPageSize * 2);
76
+ try {
77
+ const workerResults = await collectWorkerPrompts({
78
+ query,
79
+ tags,
80
+ category,
81
+ includeContent,
82
+ limit: workerLimit,
83
+ useVector,
84
+ });
85
+ if (Array.isArray(workerResults)) {
86
+ merged = merged.concat(workerResults.map((item) => ({
87
+ ...item,
88
+ resultType: 'worker',
89
+ source: 'discovery',
90
+ })));
91
+ }
92
+ } catch (workerError) {
93
+ // Worker search is optional - don't fail if unavailable
94
+ console.warn('Worker discovery search failed:', workerError.message);
95
+ }
96
+ }
97
+
98
+ // Deduplicate by CID (prefer worker results for trend scores)
99
+ const seen = new Map();
100
+ for (const item of merged) {
101
+ const key = item.cid || item.key;
102
+ if (!key) continue;
103
+ const existing = seen.get(key);
104
+ // Prefer worker results (have trend scores) over others
105
+ if (!existing || item.resultType === 'worker') {
106
+ seen.set(key, item);
107
+ }
108
+ }
109
+ merged = Array.from(seen.values());
110
+
111
+ // Sort by combined score if available, otherwise by name
112
+ merged.sort((a, b) => {
113
+ const scoreA = a.combinedScore ?? a.trendScore ?? 0;
114
+ const scoreB = b.combinedScore ?? b.trendScore ?? 0;
115
+ return scoreB - scoreA;
116
+ });
117
+
65
118
  const total = merged.length;
66
119
  const startIndex = (normalizedPage - 1) * normalizedPageSize;
67
120
  const pageResults = merged.slice(startIndex, startIndex + normalizedPageSize);
@@ -172,7 +172,9 @@ async function applyPlan(planOrPath) {
172
172
  // Legacy operator creation signature:
173
173
  // createSubDAOOperator(string name,string description,uint8 accessModel,uint256 minStake,uint256 burnAmount,address executor,address admin)
174
174
  // First, perform a static-call preflight so we can surface custom error reasons before sending.
175
+ const signerAddress = await manager.signer.getAddress();
175
176
  try {
177
+ // Pass explicit from address for Privy wallet compatibility
176
178
  await factory.createSubDAOOperator.staticCall(
177
179
  plan.config.name,
178
180
  plan.config.description,
@@ -180,7 +182,8 @@ async function applyPlan(planOrPath) {
180
182
  minStake,
181
183
  burnAmount,
182
184
  executor,
183
- executor
185
+ executor,
186
+ { from: signerAddress }
184
187
  );
185
188
  } catch (preErr) {
186
189
  const { decodeContractError } = require('../../utils/contract-error-decoder');
@@ -109,6 +109,22 @@ async function planSubDAO(options) {
109
109
  const merged = { ...options, ...answers };
110
110
 
111
111
  // 3. Construct Plan
112
+ // Convert burnAmount from token units to wei if provided as simple number
113
+ let burnAmountWei;
114
+ if (merged.burnAmount) {
115
+ const burnStr = String(merged.burnAmount);
116
+ // If it's a small number (< 1e15), assume it's in token units and convert
117
+ if (burnStr.length < 15 && !burnStr.includes('e')) {
118
+ burnAmountWei = ethers.parseEther(burnStr).toString();
119
+ } else {
120
+ // Already in wei format
121
+ burnAmountWei = burnStr;
122
+ }
123
+ } else {
124
+ // Default: 1000 SXXX = 1000 * 10^18 wei
125
+ burnAmountWei = ethers.parseEther('1000').toString();
126
+ }
127
+
112
128
  const plan = {
113
129
  version: '1.0.0',
114
130
  timestamp: new Date().toISOString(),
@@ -125,8 +141,8 @@ async function planSubDAO(options) {
125
141
  minStake: merged.minStake || '0',
126
142
  owners: merged.owners,
127
143
  threshold: merged.threshold,
128
- // Default to 1000 SXXX (minimum burn) if not provided
129
- burnAmount: merged.burnAmount || '1000000000000000000000',
144
+ // burnAmount in wei (converted above)
145
+ burnAmount: burnAmountWei,
130
146
  proposalThreshold: merged.proposalThreshold || playbook.params?.proposalThreshold || '0',
131
147
  votingPeriod: merged.votingPeriod || playbook.params?.votingPeriod || '3 days',
132
148
  quorumBps: merged.quorumBps != null && merged.quorumBps !== '' ? Number(merged.quorumBps) : (playbook.params?.quorumBps ?? 400),
@@ -1,6 +1,9 @@
1
1
  const { loadSdk } = require('../../utils/sdk-resolver');
2
2
 
3
- const DEFAULT_WALLET_TYPE = 'cast';
3
+ // Prefer Privy as the default interactive wallet when no explicit
4
+ // type is configured. Cast remains available via WALLET_TYPE=cast
5
+ // or CLI profile settings for power users.
6
+ const DEFAULT_WALLET_TYPE = 'privy';
4
7
 
5
8
  function localResolveWalletType(configInstance, env = process.env) {
6
9
  const envType = (env.WALLET_TYPE || '').trim();
@@ -22,12 +22,24 @@ const DEFAULT_TIMELOCK_DELAY_FLOOR =
22
22
  process.env.SAGE_TIMELOCK_DELAY_FLOOR_SECONDS || process.env.SAGE_TIMELOCK_MIN_DELAY_SECONDS || '1';
23
23
 
24
24
  function toBigInt(value) {
25
- if (!value) return 0n;
25
+ if (value == null || value === '') return 0n;
26
26
  if (typeof value === 'bigint') return value;
27
27
  if (typeof value === 'number') return BigInt(value);
28
- if (typeof value === 'string') return BigInt(value);
29
- if (value.toString) return BigInt(value.toString());
30
- throw new Error('Unsupported numeric value');
28
+
29
+ const asString = typeof value === 'string' ? value : value.toString();
30
+ const trimmed = String(asString).trim();
31
+
32
+ // Support shorthand like "100e18" or "1e6"
33
+ const expMatch = /^(\d+)\s*e\s*(\d+)$/i.exec(trimmed);
34
+ if (expMatch) {
35
+ const base = BigInt(expMatch[1]);
36
+ const exp = Number(expMatch[2]);
37
+ let factor = 1n;
38
+ for (let i = 0; i < exp; i++) factor *= 10n;
39
+ return base * factor;
40
+ }
41
+
42
+ return BigInt(trimmed);
31
43
  }
32
44
 
33
45
  function formatFactoryCreationError(err) {
@@ -637,11 +649,37 @@ class SubDAOManager {
637
649
  if (process.env.SAGE_VERBOSE === '1') {
638
650
  console.log('🔍 Static call preflight: createSubDAOWithParams');
639
651
  }
640
- await factory.createSubDAOWithParams.staticCall(...args);
652
+ // Pass explicit from address for Privy wallet compatibility
653
+ await factory.createSubDAOWithParams.staticCall(...args, { from: signerAddress });
641
654
  } catch (preErr) {
642
- const friendlyPre = formatFactoryCreationError(preErr);
643
- console.error('❌ SubDAO creation would revert:', friendlyPre);
644
- throw preErr;
655
+ const friendlyPre = formatFactoryCreationError(preErr) || '';
656
+ const lower = friendlyPre.toLowerCase();
657
+
658
+ // Only treat preflight as fatal for known factory invariants that will
659
+ // definitely revert on-chain. For ambiguous errors (e.g. generic "vote"
660
+ // strings or provider quirks), log a warning and continue to send the tx.
661
+ const fatalPatterns = [
662
+ 'burnoutofbounds',
663
+ 'insufficientsxxxbalance',
664
+ 'insufficientsxxxallowance',
665
+ 'libregrolemissing',
666
+ 'burnerauthorizationfailed',
667
+ 'subdaoburnerauthfailed',
668
+ 'subdaofinalizefailed',
669
+ 'timelockdelayzero'
670
+ ];
671
+
672
+ if (lower && fatalPatterns.some((p) => lower.includes(p))) {
673
+ console.error('❌ SubDAO creation would revert:', friendlyPre);
674
+ throw preErr;
675
+ }
676
+
677
+ console.log(
678
+ colors.yellow(
679
+ `⚠️ Preflight static call for createSubDAOWithParams failed with a non-fatal error (${friendlyPre || 'unknown'}); ` +
680
+ 'continuing to send the transaction.'
681
+ )
682
+ );
645
683
  }
646
684
 
647
685
  try {
@@ -41,6 +41,16 @@ const pairs = [
41
41
  ['VALIDATION_LIB_ADDRESS', 'SubDAOFactoryValidationLib'],
42
42
  ['VOTING_LIB_ADDRESS', 'SubDAOFactoryVotingLib'],
43
43
  ['SAGE_TREASURY_IMPL_ADDRESS', 'SageTreasuryImpl'],
44
+ ['BOOST_MANAGER_ADDRESS', 'GovernanceBoostManagerMerkle'],
45
+ ['BOOST_MANAGER_DIRECT_ADDRESS', 'GovernanceBoostManagerDirect'],
46
+ ['BOOST_VIEW_GLUE_ADDRESS', 'BoostViewGlue'],
47
+ ['BOOST_LOGGER_GLUE_ADDRESS', 'BoostLoggerGlue'],
48
+ ['BOOST_CREATION_GLUE_ADDRESS', 'BoostCreationGlue'],
49
+ ['SXXX_FAUCET_ADDRESS', 'SXXXFaucet'],
50
+ ['SIMPLE_BOUNTY_SYSTEM_ADDRESS', 'SimpleBountySystem'],
51
+ ['SIMPLE_CONTRIBUTOR_SYSTEM_ADDRESS', 'SimpleContributorSystem'],
52
+ ['VOTING_MULTIPLIER_NFT_ADDRESS', 'VotingMultiplierNFT'],
53
+ ['SAGE_AUCTION_HOUSE_ADDRESS', 'SageAuctionHouse'],
44
54
  ];
45
55
  const lines = [];
46
56
  for (const [envKey, contractKey] of pairs) {
@@ -66,13 +76,6 @@ echo "PERSONAL_LICENSE_RECEIPT_ADDRESS: ${PERSONAL_LICENSE_RECEIPT_ADDRESS:-unse
66
76
  echo "PERSONAL_MARKETPLACE_ADDRESS: ${PERSONAL_MARKETPLACE_ADDRESS:-unset}"
67
77
  echo "SUBGRAPH_SLUG: ${SUBGRAPH_SLUG:-unset}"
68
78
  echo "GRAPH_STUDIO_PROJECT_ID: ${GRAPH_STUDIO_PROJECT_ID:-unset}"
69
-
70
- export BOOST_MANAGER_ADDRESS=0xCaF1439bAbAb97C081Eac8aAb7749fE25B0515e7
71
-
72
- export BOOST_MANAGER_DIRECT_ADDRESS=0xf319d4d73bcab900d4e41dF143094EDc0Ad7EfFC
73
-
74
- export BOOST_VIEW_GLUE_ADDRESS=0x544135Fac927650570A8038e3B2439Ff2E2D6010
75
-
76
- export BOOST_LOGGER_GLUE_ADDRESS=0xb7BC1ABa7f033BCf3E01e6381BC3bc3d7d88fB90
77
-
78
- export BOOST_CREATION_GLUE_ADDRESS=0x1D54A742a068bC8Eab5a17d259De148009953546
79
+ echo "BOOST_MANAGER_ADDRESS: ${BOOST_MANAGER_ADDRESS:-unset}"
80
+ echo "SIMPLE_BOUNTY_SYSTEM_ADDRESS: ${SIMPLE_BOUNTY_SYSTEM_ADDRESS:-unset}"
81
+ echo "SXXX_FAUCET_ADDRESS: ${SXXX_FAUCET_ADDRESS:-unset}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-protocol/cli",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Sage Protocol CLI for managing AI prompt libraries",
5
5
  "bin": {
6
6
  "sage": "./bin/sage.js"