@sage-protocol/cli 0.8.0 → 0.8.2

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/README.md CHANGED
@@ -9,7 +9,7 @@ sage --help
9
9
 
10
10
  ## Quick Start
11
11
 
12
- ### Personal Library (No Governance)
12
+ ### Personal/Vault Library (No Governance)
13
13
 
14
14
  Create a wallet-owned library for your prompts without DAO overhead:
15
15
 
@@ -20,13 +20,13 @@ Create a wallet-owned library for your prompts without DAO overhead:
20
20
  sage wallet connect
21
21
 
22
22
  # 2. Create a personal library
23
- sage library personal create --name "My Prompts"
23
+ sage library vault create --name "My Prompts"
24
24
 
25
25
  # 3. Push prompts to your library
26
- sage library personal push <libraryId> --dir ./prompts
26
+ sage library vault push <libraryId> --dir ./prompts
27
27
 
28
28
  # 4. List your libraries
29
- sage library personal list
29
+ sage library vault list
30
30
  ```
31
31
 
32
32
  ### DAO Library (With Governance)
@@ -70,6 +70,7 @@ sage install github:user/repo
70
70
  | `sage library personal list` | List your personal libraries |
71
71
  | `sage library personal push` | Push files to a personal library |
72
72
  | `sage library personal delete` | Delete a personal library |
73
+ | `sage library vault ...` | Alias for personal libraries (clearer SIWE vault naming) |
73
74
 
74
75
  ### Prompt Workspace
75
76
 
@@ -133,7 +134,7 @@ SAGE_WALLET_TYPE=privy sage wallet connect
133
134
  | `sage gov execute <proposalId>` | Execute a queued proposal |
134
135
  | `sage proposals list` | List proposals for current DAO |
135
136
 
136
- ## Personal Libraries
137
+ ## Personal/Vault Libraries
137
138
 
138
139
  Personal libraries are wallet-owned collections that don't require DAO governance. They're ideal for:
139
140
 
@@ -144,20 +145,20 @@ Personal libraries are wallet-owned collections that don't require DAO governanc
144
145
  ### Create and Manage
145
146
 
146
147
  ```bash
147
- # Create a new personal library
148
- sage library personal create --name "My AI Prompts" --description "Curated prompts for development"
148
+ # Create a new vault library (alias of personal)
149
+ sage library vault create --name "My AI Prompts" --description "Curated prompts for development"
149
150
 
150
151
  # View your libraries
151
- sage library personal list
152
+ sage library vault list
152
153
 
153
154
  # Push content from a directory
154
- sage library personal push lib_abc123 --dir ./my-prompts
155
+ sage library vault push lib_abc123 --dir ./my-prompts
155
156
 
156
157
  # Get library details
157
- sage library personal info lib_abc123
158
+ sage library vault info lib_abc123
158
159
 
159
160
  # Delete a library
160
- sage library personal delete lib_abc123
161
+ sage library vault delete lib_abc123
161
162
  ```
162
163
 
163
164
  ### Premium Content
@@ -847,7 +847,7 @@ function register(program) {
847
847
  const favorites = subdaoStore.listFavorites();
848
848
  const recent = subdaoStore.listRecent();
849
849
  if (opts.json) {
850
- ui.json({ favorites, recent }, null, 2, { pretty: false });
850
+ ui.json({ favorites, recent }, { pretty: false });
851
851
  return;
852
852
  }
853
853
  if (!favorites.length && !recent.length) return ui.info('No saved DAOs. Use `sage dao save <address> --alias <name>`.');
@@ -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}`);