@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 +32 -11
- package/dist/cli/auth0-privy-wallet-manager.js +2 -1
- package/dist/cli/commands/dao.js +1 -1
- package/dist/cli/commands/install.js +180 -38
- 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 +47 -13
- package/dist/cli/services/config/chain-defaults.js +1 -1
- package/dist/cli/services/config/manager.js +8 -0
- package/dist/cli/services/config/schema.js +1 -0
- package/dist/cli/services/ipfs/onboarding.js +11 -0
- package/dist/cli/services/wallet/context.js +15 -8
- package/dist/cli/subdao-manager.js +1 -1
- 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/utils/wallet-shared.js +2 -1
- package/dist/cli/wallet-manager.js +15 -18
- 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,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
|
|
23
|
+
sage library vault create --name "My Prompts"
|
|
22
24
|
|
|
23
25
|
# 3. Push prompts to your library
|
|
24
|
-
sage library
|
|
26
|
+
sage library vault push <libraryId> --dir ./prompts
|
|
25
27
|
|
|
26
28
|
# 4. List your libraries
|
|
27
|
-
sage library
|
|
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
|
|
128
|
-
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"
|
|
129
150
|
|
|
130
151
|
# View your libraries
|
|
131
|
-
sage library
|
|
152
|
+
sage library vault list
|
|
132
153
|
|
|
133
154
|
# Push content from a directory
|
|
134
|
-
sage library
|
|
155
|
+
sage library vault push lib_abc123 --dir ./my-prompts
|
|
135
156
|
|
|
136
157
|
# Get library details
|
|
137
|
-
sage library
|
|
158
|
+
sage library vault info lib_abc123
|
|
138
159
|
|
|
139
160
|
# Delete a library
|
|
140
|
-
sage library
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -29,8 +115,8 @@ function detectSourceType(source) {
|
|
|
29
115
|
return { type: 'dao', value: source };
|
|
30
116
|
}
|
|
31
117
|
|
|
32
|
-
// IPFS CID (Qm... or
|
|
33
|
-
if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|
|
|
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
|
|
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}`);
|