@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 +12 -11
- package/dist/cli/commands/dao.js +1 -1
- package/dist/cli/commands/install.js +178 -36
- package/dist/cli/commands/library.js +275 -267
- package/dist/cli/commands/personal.js +69 -2
- package/dist/cli/commands/wallet.js +5 -10
- package/dist/cli/index.js +5 -0
- package/dist/cli/privy-auth-wallet-manager.js +3 -2
- package/dist/cli/services/config/chain-defaults.js +1 -1
- package/dist/cli/services/config/manager.js +3 -0
- package/dist/cli/services/config/schema.js +1 -0
- package/dist/cli/services/ipfs/onboarding.js +11 -0
- package/dist/cli/utils/aliases.js +1 -0
- package/dist/cli/utils/cli-ui.js +1 -1
- package/dist/cli/utils/provider.js +7 -3
- package/dist/cli/wallet-manager.js +7 -12
- package/dist/prompts/e2e-test-prompt.md +22 -0
- package/dist/prompts/skills/build-web3/plugin.json +11 -0
- package/package.json +1 -1
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
|
|
23
|
+
sage library vault create --name "My Prompts"
|
|
24
24
|
|
|
25
25
|
# 3. Push prompts to your library
|
|
26
|
-
sage library
|
|
26
|
+
sage library vault push <libraryId> --dir ./prompts
|
|
27
27
|
|
|
28
28
|
# 4. List your libraries
|
|
29
|
-
sage library
|
|
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
|
|
148
|
-
sage library
|
|
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
|
|
152
|
+
sage library vault list
|
|
152
153
|
|
|
153
154
|
# Push content from a directory
|
|
154
|
-
sage library
|
|
155
|
+
sage library vault push lib_abc123 --dir ./my-prompts
|
|
155
156
|
|
|
156
157
|
# Get library details
|
|
157
|
-
sage library
|
|
158
|
+
sage library vault info lib_abc123
|
|
158
159
|
|
|
159
160
|
# Delete a library
|
|
160
|
-
sage library
|
|
161
|
+
sage library vault delete lib_abc123
|
|
161
162
|
```
|
|
162
163
|
|
|
163
164
|
### Premium Content
|
package/dist/cli/commands/dao.js
CHANGED
|
@@ -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 },
|
|
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
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
464
|
+
const gatewayConfig = config.readIpfsConfig?.().gateway || process.env.SAGE_IPFS_GATEWAY || '';
|
|
465
|
+
const gatewayList = buildGatewayList(gatewayConfig);
|
|
355
466
|
|
|
356
|
-
|
|
357
|
-
const
|
|
467
|
+
const client = new WorkerClient({ baseUrl });
|
|
468
|
+
const ipfs = new IPFSManager({ gateway: gatewayConfig });
|
|
469
|
+
await ipfs.initialize();
|
|
358
470
|
|
|
359
|
-
//
|
|
471
|
+
// Try to download content (worker first, then gateways)
|
|
360
472
|
let manifest;
|
|
361
473
|
let rawContent;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
}
|
|
369
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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 ||
|
|
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}`);
|