@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.
- package/dist/cli/cast-wallet-manager.js +142 -12
- package/dist/cli/claude-desktop-config.json +2 -2
- package/dist/cli/commands/dao.js +1 -0
- package/dist/cli/commands/discover.js +250 -0
- package/dist/cli/commands/ipfs.js +211 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/ipfs-manager.js +22 -48
- package/dist/cli/mcp-setup.md +4 -4
- package/dist/cli/privy-auth-wallet-manager.js +10 -2
- package/dist/cli/services/ipfs/onboarding.js +1 -1
- package/dist/cli/services/mcp/sage-tool-registry.js +26 -6
- package/dist/cli/services/mcp/unified-prompt-search.js +54 -1
- package/dist/cli/services/subdao/applier.js +4 -1
- package/dist/cli/services/subdao/planner.js +18 -2
- package/dist/cli/services/wallet/context.js +4 -1
- package/dist/cli/subdao-manager.js +46 -8
- package/dist/ops/config/env/load-env.sepolia.sh +13 -10
- package/package.json +1 -1
|
@@ -376,19 +376,27 @@ class CastWalletManager {
|
|
|
376
376
|
let walletData = this.loadCastWallet();
|
|
377
377
|
|
|
378
378
|
if (!walletData) {
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
walletData =
|
|
383
|
-
this.saveCastWallet(walletData);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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": "
|
|
9
|
-
"SUBDAO_FACTORY_ADDRESS": "
|
|
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
|
}
|
package/dist/cli/commands/dao.js
CHANGED
|
@@ -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'),
|
package/dist/cli/ipfs-manager.js
CHANGED
|
@@ -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
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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:
|
|
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:
|
|
359
|
+
provider: effectiveProvider,
|
|
386
360
|
warm: options.warm ?? this.shouldWarm,
|
|
387
361
|
gateways: options.gateways,
|
|
388
362
|
});
|
package/dist/cli/mcp-setup.md
CHANGED
|
@@ -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=
|
|
24
|
-
SUBDAO_FACTORY_ADDRESS=
|
|
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": "
|
|
71
|
-
"SUBDAO_FACTORY_ADDRESS": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17
|
-
whenToUse: 'Use when you need to find existing prompts by keyword, tag, or
|
|
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', '
|
|
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: '
|
|
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: ['
|
|
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
|
-
|
|
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
|
-
//
|
|
129
|
-
burnAmount:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
644
|
-
|
|
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
|
-
|
|
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}"
|