@sage-protocol/cli 0.8.0 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (34) hide show
  1. package/README.md +12 -11
  2. package/dist/cli/commands/boost.js +339 -62
  3. package/dist/cli/commands/bounty.js +28 -4
  4. package/dist/cli/commands/config.js +10 -1
  5. package/dist/cli/commands/contributor.js +16 -6
  6. package/dist/cli/commands/dao.js +1 -1
  7. package/dist/cli/commands/discover.js +3 -3
  8. package/dist/cli/commands/governance.js +141 -58
  9. package/dist/cli/commands/install.js +178 -36
  10. package/dist/cli/commands/ipfs.js +12 -2
  11. package/dist/cli/commands/library.js +277 -268
  12. package/dist/cli/commands/members.js +132 -18
  13. package/dist/cli/commands/multiplier.js +101 -13
  14. package/dist/cli/commands/nft.js +16 -3
  15. package/dist/cli/commands/personal.js +69 -2
  16. package/dist/cli/commands/prompt.js +1 -1
  17. package/dist/cli/commands/proposals.js +153 -3
  18. package/dist/cli/commands/stake-status.js +130 -56
  19. package/dist/cli/commands/sxxx.js +37 -4
  20. package/dist/cli/commands/wallet.js +5 -10
  21. package/dist/cli/contracts/index.js +2 -1
  22. package/dist/cli/index.js +5 -0
  23. package/dist/cli/privy-auth-wallet-manager.js +3 -2
  24. package/dist/cli/services/config/chain-defaults.js +1 -1
  25. package/dist/cli/services/config/manager.js +3 -0
  26. package/dist/cli/services/config/schema.js +1 -0
  27. package/dist/cli/services/ipfs/onboarding.js +11 -0
  28. package/dist/cli/utils/aliases.js +62 -3
  29. package/dist/cli/utils/cli-ui.js +1 -1
  30. package/dist/cli/utils/provider.js +7 -3
  31. package/dist/cli/wallet-manager.js +7 -12
  32. package/dist/prompts/e2e-test-prompt.md +22 -0
  33. package/dist/prompts/skills/build-web3/plugin.json +11 -0
  34. package/package.json +1 -1
@@ -18,6 +18,92 @@ const { handleCLIError } = require('../utils/error-handler');
18
18
  const { withSpinner } = require('../utils/progress');
19
19
  const { readWorkspace, addDependency, initWorkspace } = require('../services/prompts/workspace');
20
20
 
21
+ function uniqStrings(list) {
22
+ const seen = new Set();
23
+ const out = [];
24
+ for (const item of list || []) {
25
+ const value = String(item || '').trim();
26
+ if (!value || seen.has(value)) continue;
27
+ seen.add(value);
28
+ out.push(value);
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function buildGatewayUrl(base, cid) {
34
+ const trimmed = String(base || '').trim().replace(/\/$/, '');
35
+ if (!trimmed) return '';
36
+ if (trimmed.endsWith('/ipfs')) return `${trimmed}/${cid}`;
37
+ if (trimmed.includes('/ipfs/')) return `${trimmed}/${cid}`;
38
+ return `${trimmed}/ipfs/${cid}`;
39
+ }
40
+
41
+ function buildGatewayList(preferred) {
42
+ const envList = process.env.SAGE_IPFS_GATEWAYS
43
+ ? process.env.SAGE_IPFS_GATEWAYS.split(',').map((v) => v.trim())
44
+ : [];
45
+ const defaults = [
46
+ preferred,
47
+ 'https://dweb.link/ipfs/',
48
+ 'https://nftstorage.link/ipfs/',
49
+ 'https://ipfs.io/ipfs/',
50
+ 'https://gateway.pinata.cloud/ipfs/',
51
+ ];
52
+ return uniqStrings([...envList, ...defaults]);
53
+ }
54
+
55
+ function parseIpfsBody(body) {
56
+ if (body && typeof body.content === 'string') return { raw: body.content };
57
+ if (body && body.library) return { json: body };
58
+ if (typeof body === 'string') return { raw: body };
59
+ if (body && typeof body === 'object') return { json: body };
60
+ return {};
61
+ }
62
+
63
+ async function fetchIpfsContent({ cid, workerClient, gateways, logger }) {
64
+ if (workerClient) {
65
+ try {
66
+ const { res, body } = await workerClient._fetchJson(`/ipfs/content/${cid}`);
67
+ const parsed = parseIpfsBody(body);
68
+ if (res?.ok && (parsed.raw || parsed.json)) {
69
+ return { source: 'worker', ...parsed };
70
+ }
71
+ } catch (err) {
72
+ logger?.debug?.('install_worker_fetch_failed', { cid, err: err.message || String(err) });
73
+ }
74
+ }
75
+
76
+ const axios = require('axios');
77
+ let lastError;
78
+ for (const gateway of gateways || []) {
79
+ const url = buildGatewayUrl(gateway, cid);
80
+ if (!url) continue;
81
+ try {
82
+ const response = await axios.get(url, {
83
+ timeout: 12000,
84
+ maxRedirects: 5,
85
+ validateStatus: (status) => status < 400,
86
+ });
87
+ const parsed = parseIpfsBody(response.data);
88
+ if (parsed.raw || parsed.json) {
89
+ return { source: 'gateway', url, ...parsed };
90
+ }
91
+ if (typeof response.data === 'string') {
92
+ return { source: 'gateway', url, raw: response.data };
93
+ }
94
+ // If object, treat as json
95
+ if (response.data && typeof response.data === 'object') {
96
+ return { source: 'gateway', url, json: response.data };
97
+ }
98
+ } catch (err) {
99
+ logger?.debug?.('install_gateway_fetch_failed', { cid, gateway: url, err: err.message || String(err) });
100
+ lastError = err;
101
+ }
102
+ }
103
+ const message = lastError ? lastError.message || String(lastError) : `Failed to download content from ${cid}`;
104
+ throw new Error(message);
105
+ }
106
+
21
107
  /**
22
108
  * Detect source type from input string
23
109
  * @param {string} source - Input source string
@@ -161,6 +247,7 @@ async function installFromDao(daoAddress, projectDir, opts) {
161
247
  const { resolveRegistryAddress } = require('../utils/address-resolution');
162
248
  const { WorkerClient } = require('../services/ipfs/worker-client');
163
249
  const { DEFAULT_WORKER_BASE } = require('../services/ipfs/onboarding');
250
+ const IPFSManager = require('../ipfs-manager');
164
251
  const config = require('../config');
165
252
 
166
253
  const provider = new ethers.JsonRpcProvider(
@@ -196,17 +283,28 @@ async function installFromDao(daoAddress, projectDir, opts) {
196
283
  process.env.SAGE_IPFS_WORKER_URL ||
197
284
  DEFAULT_WORKER_BASE;
198
285
 
286
+ const gatewayConfig = config.readIpfsConfig?.().gateway || process.env.SAGE_IPFS_GATEWAY || '';
287
+ const gatewayList = buildGatewayList(gatewayConfig);
288
+
199
289
  const client = new WorkerClient({ baseUrl });
200
- const { body } = await client._fetchJson(`/ipfs/content/${manifestCID}`);
290
+ const ipfs = new IPFSManager({ gateway: gatewayConfig });
291
+ await ipfs.initialize();
201
292
 
202
- // Worker returns wrapped content - parse if needed
203
293
  let manifest;
204
- if (body && typeof body.content === 'string') {
205
- manifest = JSON.parse(body.content);
206
- } else if (body && body.library) {
207
- manifest = body;
208
- } else {
209
- throw new Error(`Failed to download manifest from ${manifestCID}`);
294
+ try {
295
+ const result = await fetchIpfsContent({
296
+ cid: manifestCID,
297
+ workerClient: client,
298
+ gateways: gatewayList,
299
+ logger: ui,
300
+ });
301
+ if (result.json) {
302
+ manifest = result.json;
303
+ } else if (result.raw) {
304
+ manifest = JSON.parse(result.raw);
305
+ }
306
+ } catch (err) {
307
+ throw new Error(`Failed to download manifest from ${manifestCID}: ${err.message}`);
210
308
  }
211
309
 
212
310
  const libraryName = manifest.library?.name || 'library';
@@ -234,15 +332,26 @@ async function installFromDao(daoAddress, projectDir, opts) {
234
332
  if (!prompt.cid) continue;
235
333
 
236
334
  try {
237
- const { body: contentBody } = await client._fetchJson(`/ipfs/content/${prompt.cid}`);
238
-
239
- // Worker returns wrapped content
240
335
  let content;
241
- if (contentBody && typeof contentBody.content === 'string') {
242
- content = contentBody.content;
243
- } else if (typeof contentBody === 'string') {
244
- content = contentBody;
245
- } else {
336
+ try {
337
+ const result = await fetchIpfsContent({
338
+ cid: prompt.cid,
339
+ workerClient: client,
340
+ gateways: gatewayList,
341
+ logger: ui,
342
+ });
343
+ if (result.raw) {
344
+ content = result.raw;
345
+ } else if (result.json) {
346
+ content = JSON.stringify(result.json, null, 2);
347
+ }
348
+ } catch (err) {
349
+ if (verbose) ui.warn(`Worker fetch failed for ${prompt.cid}: ${err.message}. Trying gateway download...`);
350
+ const data = await ipfs.downloadJson(prompt.cid);
351
+ content = data?.content || JSON.stringify(data, null, 2);
352
+ }
353
+
354
+ if (!content) {
246
355
  throw new Error('Unexpected content format');
247
356
  }
248
357
 
@@ -345,30 +454,45 @@ async function installFromCid(cid, projectDir, opts) {
345
454
  const { verbose = false, name } = opts;
346
455
  const { WorkerClient } = require('../services/ipfs/worker-client');
347
456
  const { DEFAULT_WORKER_BASE } = require('../services/ipfs/onboarding');
457
+ const IPFSManager = require('../ipfs-manager');
348
458
  const config = require('../config');
349
459
 
350
460
  const baseUrl = config.readIpfsConfig?.().workerBaseUrl ||
351
461
  process.env.SAGE_IPFS_WORKER_URL ||
352
462
  DEFAULT_WORKER_BASE;
353
463
 
354
- const client = new WorkerClient({ baseUrl });
464
+ const gatewayConfig = config.readIpfsConfig?.().gateway || process.env.SAGE_IPFS_GATEWAY || '';
465
+ const gatewayList = buildGatewayList(gatewayConfig);
355
466
 
356
- // Try to download content
357
- const { body } = await client._fetchJson(`/ipfs/content/${cid}`);
467
+ const client = new WorkerClient({ baseUrl });
468
+ const ipfs = new IPFSManager({ gateway: gatewayConfig });
469
+ await ipfs.initialize();
358
470
 
359
- // Check if it's a manifest
471
+ // Try to download content (worker first, then gateways)
360
472
  let manifest;
361
473
  let rawContent;
362
- if (body && typeof body.content === 'string') {
363
- try {
364
- manifest = JSON.parse(body.content);
365
- } catch (_) {
366
- rawContent = body.content;
474
+ try {
475
+ const result = await fetchIpfsContent({
476
+ cid,
477
+ workerClient: client,
478
+ gateways: gatewayList,
479
+ logger: ui,
480
+ });
481
+ if (result.json) {
482
+ if (result.json.library) {
483
+ manifest = result.json;
484
+ } else {
485
+ rawContent = result.json.content || JSON.stringify(result.json, null, 2);
486
+ }
487
+ } else if (result.raw) {
488
+ try {
489
+ manifest = JSON.parse(result.raw);
490
+ } catch (_) {
491
+ rawContent = result.raw;
492
+ }
367
493
  }
368
- } else if (body && body.library) {
369
- manifest = body;
370
- } else if (typeof body === 'string') {
371
- rawContent = body;
494
+ } catch (err) {
495
+ throw new Error(`Failed to download content from ${cid}: ${err.message}`);
372
496
  }
373
497
 
374
498
  if (manifest && manifest.library) {
@@ -388,13 +512,26 @@ async function installFromCid(cid, projectDir, opts) {
388
512
  for (const prompt of manifest.prompts || []) {
389
513
  if (!prompt.cid) continue;
390
514
  try {
391
- const { body: contentBody } = await client._fetchJson(`/ipfs/content/${prompt.cid}`);
392
515
  let content;
393
- if (contentBody && typeof contentBody.content === 'string') {
394
- content = contentBody.content;
395
- } else if (typeof contentBody === 'string') {
396
- content = contentBody;
397
- } else {
516
+ try {
517
+ const result = await fetchIpfsContent({
518
+ cid: prompt.cid,
519
+ workerClient: client,
520
+ gateways: gatewayList,
521
+ logger: ui,
522
+ });
523
+ if (result.raw) {
524
+ content = result.raw;
525
+ } else if (result.json) {
526
+ content = JSON.stringify(result.json, null, 2);
527
+ }
528
+ } catch (err) {
529
+ if (verbose) ui.warn(`Worker fetch failed for ${prompt.cid}: ${err.message}. Trying gateway download...`);
530
+ const data = await ipfs.downloadJson(prompt.cid);
531
+ content = data?.content || JSON.stringify(data, null, 2);
532
+ }
533
+
534
+ if (!content) {
398
535
  throw new Error('Unexpected content format');
399
536
  }
400
537
 
@@ -419,7 +556,7 @@ async function installFromCid(cid, projectDir, opts) {
419
556
  };
420
557
  } else {
421
558
  // Single file - use raw content
422
- const content = rawContent || (body && body.content) || '';
559
+ const content = rawContent || '';
423
560
  if (!content) {
424
561
  throw new Error(`Failed to download content from ${cid}`);
425
562
  }
@@ -458,6 +595,7 @@ function resolveAlias(alias) {
458
595
  */
459
596
  function copyDirRecursive(src, dest, copied, verbose, label) {
460
597
  for (const file of fs.readdirSync(src)) {
598
+ if (file === '.git') continue;
461
599
  const srcPath = path.join(src, file);
462
600
  const destPath = path.join(dest, file);
463
601
  const stat = fs.statSync(srcPath);
@@ -582,6 +720,7 @@ function register(program) {
582
720
  .option('--to <dir>', 'Installation directory (default: auto-detect)')
583
721
  .option('--as-prompt', 'Force install as prompt (to prompts/)')
584
722
  .option('--branch <name>', 'Git branch for GitHub sources', 'main')
723
+ .option('--subpath <path>', 'Subpath within a GitHub repo (overrides path in github:owner/repo/path)')
585
724
  .option('--name <name>', 'Override the installed name')
586
725
  .option('-y, --yes', 'Skip confirmations')
587
726
  .option('-v, --verbose', 'Show detailed output')
@@ -592,6 +731,9 @@ function register(program) {
592
731
 
593
732
  const projectDir = opts.to || process.cwd();
594
733
  const sourceInfo = detectSourceType(source);
734
+ if (sourceInfo.type === 'github' && opts.subpath) {
735
+ sourceInfo.subpath = opts.subpath;
736
+ }
595
737
 
596
738
  if (!opts.json) {
597
739
  ui.info(`Installing from ${sourceInfo.type}: ${source}`);
@@ -369,9 +369,19 @@ function register(program) {
369
369
  ui.configure({ verbose: opts.verbose, json: opts.json });
370
370
 
371
371
  const baseUrl = opts.workerUrl || (config.readIpfsConfig && (config.readIpfsConfig().workerBaseUrl || '')) || DEFAULT_WORKER_BASE;
372
- const wc = new WorkerClient({ baseUrl, token: opts.workerToken, address: opts.workerAddress });
372
+ let workerAddress = opts.workerAddress;
373
+ if (!workerAddress) {
374
+ try {
375
+ const wallet = await getConnectedWallet();
376
+ workerAddress = wallet?.account || (wallet?.signer && await wallet.signer.getAddress()) || null;
377
+ } catch (_) { /* ignore */ }
378
+ }
379
+ if (!workerAddress) {
380
+ throw new Error('address required (set --worker-address or configure a wallet)');
381
+ }
382
+ const wc = new WorkerClient({ baseUrl, token: opts.workerToken, address: workerAddress });
373
383
  const result = await wc.listPins({
374
- address: opts.workerAddress,
384
+ address: workerAddress,
375
385
  status: opts.status,
376
386
  limit: opts.limit,
377
387
  });