@sage-protocol/cli 0.7.9 → 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,22 +9,24 @@ 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
 
16
16
  ```bash
17
17
  # 1. Connect your wallet
18
+ # Privy is the default for npm installs (no local keystore required)
19
+ # App ID is baked in; no secret is required when using the web relay (default)
18
20
  sage wallet connect
19
21
 
20
22
  # 2. Create a personal library
21
- sage library personal create --name "My Prompts"
23
+ sage library vault create --name "My Prompts"
22
24
 
23
25
  # 3. Push prompts to your library
24
- sage library personal push <libraryId> --dir ./prompts
26
+ sage library vault push <libraryId> --dir ./prompts
25
27
 
26
28
  # 4. List your libraries
27
- sage library personal list
29
+ sage library vault list
28
30
  ```
29
31
 
30
32
  ### DAO Library (With Governance)
@@ -68,6 +70,7 @@ sage install github:user/repo
68
70
  | `sage library personal list` | List your personal libraries |
69
71
  | `sage library personal push` | Push files to a personal library |
70
72
  | `sage library personal delete` | Delete a personal library |
73
+ | `sage library vault ...` | Alias for personal libraries (clearer SIWE vault naming) |
71
74
 
72
75
  ### Prompt Workspace
73
76
 
@@ -103,6 +106,24 @@ sage install github:user/repo
103
106
  | `sage config ipfs onboard` | Configure IPFS/pinning |
104
107
  | `sage secret set <name>` | Store a secret in keychain |
105
108
 
109
+ ### NPM Install Setup (Privy)
110
+
111
+ For npm installs (running outside a repo), put Privy credentials in a global env file:
112
+
113
+ ```bash
114
+ mkdir -p ~/.sage
115
+ cat >> ~/.sage/.env << 'EOF'
116
+ PRIVY_APP_ID=your_app_id
117
+ PRIVY_APP_SECRET=your_app_secret
118
+ EOF
119
+ ```
120
+
121
+ Then:
122
+
123
+ ```bash
124
+ SAGE_WALLET_TYPE=privy sage wallet connect
125
+ ```
126
+
106
127
  ### Governance
107
128
 
108
129
  | Command | Description |
@@ -113,7 +134,7 @@ sage install github:user/repo
113
134
  | `sage gov execute <proposalId>` | Execute a queued proposal |
114
135
  | `sage proposals list` | List proposals for current DAO |
115
136
 
116
- ## Personal Libraries
137
+ ## Personal/Vault Libraries
117
138
 
118
139
  Personal libraries are wallet-owned collections that don't require DAO governance. They're ideal for:
119
140
 
@@ -124,20 +145,20 @@ Personal libraries are wallet-owned collections that don't require DAO governanc
124
145
  ### Create and Manage
125
146
 
126
147
  ```bash
127
- # Create a new personal library
128
- 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"
129
150
 
130
151
  # View your libraries
131
- sage library personal list
152
+ sage library vault list
132
153
 
133
154
  # Push content from a directory
134
- sage library personal push lib_abc123 --dir ./my-prompts
155
+ sage library vault push lib_abc123 --dir ./my-prompts
135
156
 
136
157
  # Get library details
137
- sage library personal info lib_abc123
158
+ sage library vault info lib_abc123
138
159
 
139
160
  # Delete a library
140
- sage library personal delete lib_abc123
161
+ sage library vault delete lib_abc123
141
162
  ```
142
163
 
143
164
  ### Premium Content
@@ -60,7 +60,8 @@ class Auth0PrivyWalletManager {
60
60
  this.email = null;
61
61
 
62
62
  // Privy app credentials
63
- this.appId = process.env.PRIVY_APP_ID || process.env.NEXT_PUBLIC_PRIVY_APP_ID;
63
+ const defaults = require('./defaults');
64
+ this.appId = process.env.PRIVY_APP_ID || process.env.NEXT_PUBLIC_PRIVY_APP_ID || defaults.PRIVY_APP_ID;
64
65
  this.appSecret = process.env.PRIVY_APP_SECRET;
65
66
  }
66
67
 
@@ -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
@@ -29,8 +115,8 @@ function detectSourceType(source) {
29
115
  return { type: 'dao', value: source };
30
116
  }
31
117
 
32
- // IPFS CID (Qm... or bafy...)
33
- if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[a-z0-9]{50,})$/.test(source)) {
118
+ // IPFS CID (Qm... or baf...)
119
+ if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|baf[a-z0-9]{50,})$/.test(source)) {
34
120
  return { type: 'cid', value: source };
35
121
  }
36
122
 
@@ -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}`);