@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const axios = require('axios');
|
|
4
|
-
const { getAddress, id, parseUnits, formatUnits, Contract, MaxUint256 } = require('ethers');
|
|
4
|
+
const { getAddress, id, parseUnits, formatUnits, Contract, MaxUint256, ZeroAddress } = require('ethers');
|
|
5
5
|
const { decryptAesGcm, fromB64 } = require('../utils/aes');
|
|
6
6
|
const sdk = require('@sage-protocol/sdk');
|
|
7
7
|
const { formatJson } = require('../utils/format');
|
|
@@ -244,6 +244,42 @@ const PERSONAL_REGISTRY_QUERY = `
|
|
|
244
244
|
async function resolveOrCreatePersonalRegistry(wallet, opts = {}) {
|
|
245
245
|
const ownerAddress = getAddress(await wallet.getAddress());
|
|
246
246
|
const subgraphUrl = resolveSubgraphUrl(opts.subgraph);
|
|
247
|
+
const provider = wallet.provider || await getProvider();
|
|
248
|
+
|
|
249
|
+
const { resolveArtifact } = require('../utils/artifacts');
|
|
250
|
+
const PromptRegistryABI = resolveArtifact('contracts/PromptRegistry.sol/PromptRegistry.json').abi;
|
|
251
|
+
|
|
252
|
+
async function validateRegistryCandidate(registryAddr) {
|
|
253
|
+
try {
|
|
254
|
+
const code = await provider.getCode(registryAddr);
|
|
255
|
+
if (!code || code === '0x') {
|
|
256
|
+
return { ok: false, reason: 'no_code' };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const read = new Contract(registryAddr, PromptRegistryABI, provider);
|
|
260
|
+
|
|
261
|
+
// Ensure this registry is actually usable by the current wallet.
|
|
262
|
+
const govRole = await read.GOVERNANCE_ROLE().catch(() => null);
|
|
263
|
+
if (!govRole) return { ok: false, reason: 'missing_governance_role' };
|
|
264
|
+
|
|
265
|
+
const hasGovRole = await read.hasRole(govRole, ownerAddress).catch(() => null);
|
|
266
|
+
if (!hasGovRole) return { ok: false, reason: 'missing_governance_role_for_owner' };
|
|
267
|
+
|
|
268
|
+
// Personal registries should not be linked to a SubDAO.
|
|
269
|
+
const subdao = await read.subDAO().catch(() => null);
|
|
270
|
+
if (subdao && getAddress(subdao) !== getAddress(ZeroAddress)) {
|
|
271
|
+
return { ok: false, reason: 'linked_to_subdao', subdao: getAddress(subdao) };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// If paused, addPrompt will revert.
|
|
275
|
+
const paused = await read.paused().catch(() => false);
|
|
276
|
+
if (paused) return { ok: false, reason: 'paused' };
|
|
277
|
+
|
|
278
|
+
return { ok: true };
|
|
279
|
+
} catch (e) {
|
|
280
|
+
return { ok: false, reason: 'validation_error', error: e };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
247
283
|
|
|
248
284
|
// Try subgraph lookup first
|
|
249
285
|
if (subgraphUrl) {
|
|
@@ -254,7 +290,14 @@ async function resolveOrCreatePersonalRegistry(wallet, opts = {}) {
|
|
|
254
290
|
});
|
|
255
291
|
const registries = resp.data?.data?.personalRegistries || [];
|
|
256
292
|
if (registries.length > 0) {
|
|
257
|
-
|
|
293
|
+
const candidate = getAddress(registries[0].id);
|
|
294
|
+
const validation = await validateRegistryCandidate(candidate);
|
|
295
|
+
if (validation.ok) return candidate;
|
|
296
|
+
|
|
297
|
+
if (!opts.json) {
|
|
298
|
+
const extra = validation.subdao ? ` (subDAO=${validation.subdao})` : '';
|
|
299
|
+
ui.warn(`Resolved personal registry ${candidate} is not usable (${validation.reason}${extra}). Creating a fresh registry...`);
|
|
300
|
+
}
|
|
258
301
|
}
|
|
259
302
|
} catch (_) {
|
|
260
303
|
// Subgraph unavailable, fall through to create
|
|
@@ -309,6 +352,30 @@ async function addPromptToRegistry(wallet, registryAddr, promptData, opts = {})
|
|
|
309
352
|
const PromptRegistryABI = resolveArtifact('contracts/PromptRegistry.sol/PromptRegistry.json').abi;
|
|
310
353
|
const registry = new Contract(registryAddr, PromptRegistryABI, wallet);
|
|
311
354
|
|
|
355
|
+
// Proactive checks to surface actionable errors before spending gas.
|
|
356
|
+
try {
|
|
357
|
+
const ownerAddress = getAddress(await wallet.getAddress());
|
|
358
|
+
const govRole = await registry.GOVERNANCE_ROLE().catch(() => null);
|
|
359
|
+
if (!govRole) {
|
|
360
|
+
throw new Error(`Resolved registry ${registryAddr} does not expose GOVERNANCE_ROLE(); is this a valid PromptRegistry?`);
|
|
361
|
+
}
|
|
362
|
+
const hasGov = await registry.hasRole(govRole, ownerAddress).catch(() => null);
|
|
363
|
+
if (!hasGov) {
|
|
364
|
+
throw new Error(`Wallet ${ownerAddress} lacks GOVERNANCE_ROLE on registry ${registryAddr}. This usually means the registry is not your personal registry (or it was not initialized as personal).`);
|
|
365
|
+
}
|
|
366
|
+
const subdao = await registry.subDAO().catch(() => null);
|
|
367
|
+
if (subdao && getAddress(subdao) !== getAddress(ZeroAddress)) {
|
|
368
|
+
throw new Error(`Registry ${registryAddr} is linked to SubDAO ${getAddress(subdao)}; personal publish requires a personal registry (subDAO=0x0).`);
|
|
369
|
+
}
|
|
370
|
+
const paused = await registry.paused().catch(() => false);
|
|
371
|
+
if (paused) {
|
|
372
|
+
throw new Error(`Registry ${registryAddr} is paused; unpause it (DEFAULT_ADMIN_ROLE) or create a fresh personal registry.`);
|
|
373
|
+
}
|
|
374
|
+
} catch (e) {
|
|
375
|
+
// Re-throw with a cleaner message for JSON/non-JSON callers.
|
|
376
|
+
throw e;
|
|
377
|
+
}
|
|
378
|
+
|
|
312
379
|
const title = promptData.title || promptData.name || promptData.key || 'Untitled';
|
|
313
380
|
const tags = promptData.tags || [];
|
|
314
381
|
const contentCID = promptData.contentCID || '';
|
|
@@ -289,9 +289,10 @@ const Web3AuthWalletManager = require('../web3auth-wallet-manager');
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// Device code flow via relay works with just appId (no secret needed)
|
|
292
|
-
//
|
|
292
|
+
// Always prefer relay unless --local is explicitly set
|
|
293
293
|
if (appId) {
|
|
294
294
|
if (options?.local) {
|
|
295
|
+
process.env.SAGE_PRIVY_LOGIN_MODE = 'local';
|
|
295
296
|
// Local OAuth requires appSecret for direct Privy SDK authentication
|
|
296
297
|
if (!appSecret) {
|
|
297
298
|
ui.error('Local OAuth mode requires PRIVY_APP_SECRET.');
|
|
@@ -300,15 +301,9 @@ const Web3AuthWalletManager = require('../web3auth-wallet-manager');
|
|
|
300
301
|
}
|
|
301
302
|
ui.info('Using local OAuth authentication...');
|
|
302
303
|
} else {
|
|
303
|
-
// Web
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
process.env.SAGE_PRIVY_LOGIN_MODE = 'device';
|
|
307
|
-
ui.info('Using device code authentication via web app relay...');
|
|
308
|
-
} else {
|
|
309
|
-
process.env.SAGE_PRIVY_LOGIN_MODE = 'web';
|
|
310
|
-
ui.info('Using web-based authentication...');
|
|
311
|
-
}
|
|
304
|
+
// Web relay flow - uses web app, no localhost callback
|
|
305
|
+
process.env.SAGE_PRIVY_LOGIN_MODE = 'web';
|
|
306
|
+
ui.info('Using web-based authentication via relay...');
|
|
312
307
|
}
|
|
313
308
|
|
|
314
309
|
const walletManager = new PrivyAuthWalletManager();
|
package/dist/cli/index.js
CHANGED
|
@@ -19,6 +19,11 @@ if (!process.env.CONTRACT_ARTIFACTS_ROOT) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Always trust SAGE_RPC_URL over RPC_URL when provided
|
|
23
|
+
if (process.env.SAGE_RPC_URL) {
|
|
24
|
+
process.env.RPC_URL = process.env.SAGE_RPC_URL;
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
// Filter out deprecated Lit SDK warning immediately
|
|
23
28
|
const realStderrWrite = process.stderr.write.bind(process.stderr);
|
|
24
29
|
process.stderr.write = (chunk, encoding, callback) => {
|
|
@@ -82,7 +82,9 @@ class PrivyAuthWalletManager {
|
|
|
82
82
|
try {
|
|
83
83
|
const relayEnabled = process.env.SAGE_DISABLE_PRIVY_RELAY !== '1';
|
|
84
84
|
const hasAppSecret = !!this.appSecret;
|
|
85
|
-
const
|
|
85
|
+
const mode = String(process.env.SAGE_PRIVY_LOGIN_MODE || '').trim().toLowerCase();
|
|
86
|
+
const forceRelay = relayEnabled && mode !== 'local' && mode !== 'oauth';
|
|
87
|
+
const preferSessionFlow = forceRelay;
|
|
86
88
|
|
|
87
89
|
// Try cached credentials first
|
|
88
90
|
const cached = readPrivyCredentials();
|
|
@@ -140,7 +142,6 @@ class PrivyAuthWalletManager {
|
|
|
140
142
|
|
|
141
143
|
// No valid cached credentials - need OAuth login
|
|
142
144
|
console.log('\nNo cached Privy credentials found.');
|
|
143
|
-
const mode = String(process.env.SAGE_PRIVY_LOGIN_MODE || '').trim().toLowerCase();
|
|
144
145
|
const useSessionFlow = preferSessionFlow || mode === 'web' || mode === 'session';
|
|
145
146
|
const useDeviceCodeFlow = !preferSessionFlow && (mode === 'device' || mode === 'device_code');
|
|
146
147
|
|
|
@@ -58,7 +58,7 @@ const CHAIN_CONFIGS = {
|
|
|
58
58
|
* Used for resolution precedence.
|
|
59
59
|
*/
|
|
60
60
|
const ENV_VARS = {
|
|
61
|
-
rpcUrl: ['RPC_URL', 'BASE_SEPOLIA_RPC', 'BASE_RPC_URL'],
|
|
61
|
+
rpcUrl: ['SAGE_RPC_URL', 'RPC_URL', 'BASE_SEPOLIA_RPC', 'BASE_RPC_URL'],
|
|
62
62
|
chainId: ['CHAIN_ID', 'BASE_CHAIN_ID'],
|
|
63
63
|
subgraphUrl: ['SUBGRAPH_URL', 'SAGE_SUBGRAPH_URL', 'NEXT_PUBLIC_GRAPH_ENDPOINT', 'NEXT_PUBLIC_SUBGRAPH_URL']
|
|
64
64
|
};
|
|
@@ -121,6 +121,9 @@ class ConfigManager {
|
|
|
121
121
|
if (fs.existsSync(sageEnv)) {
|
|
122
122
|
dotenv.config({ path: sageEnv });
|
|
123
123
|
}
|
|
124
|
+
if (process.env.SAGE_RPC_URL) {
|
|
125
|
+
process.env.RPC_URL = process.env.SAGE_RPC_URL;
|
|
126
|
+
}
|
|
124
127
|
} catch (_) {
|
|
125
128
|
// dotenv not available, skip
|
|
126
129
|
}
|
|
@@ -105,6 +105,7 @@ const addressMap = z.record(
|
|
|
105
105
|
*/
|
|
106
106
|
const ipfsConfig = z.object({
|
|
107
107
|
gateway: z.string().url().optional(),
|
|
108
|
+
gateways: z.array(z.string()).optional(),
|
|
108
109
|
pinataApiKey: z.string().optional(),
|
|
109
110
|
pinataSecretKey: z.string().optional(),
|
|
110
111
|
pinataJwt: z.string().optional(),
|
|
@@ -2,6 +2,12 @@ const path = require('path');
|
|
|
2
2
|
const ui = require('../../utils/cli-ui');
|
|
3
3
|
|
|
4
4
|
const DEFAULT_GATEWAY = 'https://ipfs.dev.sageprotocol.io/ipfs';
|
|
5
|
+
const FALLBACK_GATEWAYS = [
|
|
6
|
+
'https://dweb.link/ipfs',
|
|
7
|
+
'https://nftstorage.link/ipfs',
|
|
8
|
+
'https://ipfs.io/ipfs',
|
|
9
|
+
'https://gateway.pinata.cloud/ipfs'
|
|
10
|
+
];
|
|
5
11
|
const DEFAULT_WORKER_BASE = 'https://api.sageprotocol.io';
|
|
6
12
|
const DEFAULT_WORKER_CHALLENGE_PATH = '/ipfs/auth/challenge';
|
|
7
13
|
const DEFAULT_WORKER_UPLOAD_PATH = '/ipfs/upload';
|
|
@@ -87,6 +93,7 @@ function createIpfsOnboarding({
|
|
|
87
93
|
|
|
88
94
|
if (!quiet) {
|
|
89
95
|
ui.header('Sage IPFS pinning setup', 'Configure your gateway and pinning providers so uploads work out of the box.');
|
|
96
|
+
ui.output(`Gateway fallbacks: ${FALLBACK_GATEWAYS.join(', ')}`);
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
let scope = initialScope && ['project', 'global'].includes(initialScope) ? initialScope : null;
|
|
@@ -332,6 +339,10 @@ function createIpfsOnboarding({
|
|
|
332
339
|
const payload = {};
|
|
333
340
|
if (gateway !== undefined) {
|
|
334
341
|
payload.gateway = ensureString(gateway) || null;
|
|
342
|
+
if (payload.gateway) {
|
|
343
|
+
const fallback = FALLBACK_GATEWAYS.filter((g) => g !== payload.gateway);
|
|
344
|
+
payload.gateways = fallback;
|
|
345
|
+
}
|
|
335
346
|
}
|
|
336
347
|
payload.warmGateway = !!warmGateway;
|
|
337
348
|
if (provider) payload.provider = provider;
|
package/dist/cli/utils/cli-ui.js
CHANGED
|
@@ -402,7 +402,7 @@ function keyValue(data) {
|
|
|
402
402
|
* @param {boolean} [options.pretty=true] - Pretty print
|
|
403
403
|
*/
|
|
404
404
|
function json(data, options = {}) {
|
|
405
|
-
const { pretty = true } = options;
|
|
405
|
+
const { pretty = true } = options || {};
|
|
406
406
|
console.log(pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
|
|
407
407
|
}
|
|
408
408
|
|
|
@@ -14,11 +14,15 @@ async function getWallet(provider, options = {}) {
|
|
|
14
14
|
throw new Error('Signer unavailable. Connect a wallet capable of signing transactions.');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
let signer = session.signer;
|
|
18
|
+
|
|
19
|
+
// ethers v6 Signer#connect returns a NEW signer; it does not mutate.
|
|
20
|
+
// Ensure the returned signer is connected so read calls (eth_call) work.
|
|
21
|
+
if (!signer.provider && provider && typeof signer.connect === 'function') {
|
|
22
|
+
signer = signer.connect(provider);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
return
|
|
25
|
+
return signer;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
module.exports = {
|
|
@@ -133,19 +133,14 @@ class WalletManager {
|
|
|
133
133
|
process.env.SAGE_PRIVY_RELAY_URL = defaults.SAGE_PRIVY_RELAY_URL;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
//
|
|
137
|
-
// Use PrivyAuthWalletManager when appId is available
|
|
136
|
+
// Prefer relay-based web login by default (no localhost callback).
|
|
137
|
+
// Use PrivyAuthWalletManager when appId is available.
|
|
138
138
|
if (appId) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
if (!process.env.SAGE_QUIET_JSON && process.env.SAGE_VERBOSE === '1') {
|
|
147
|
-
console.log('🔐 Using Privy OAuth authentication (same wallet as web app)');
|
|
148
|
-
}
|
|
139
|
+
if (!process.env.SAGE_PRIVY_LOGIN_MODE) {
|
|
140
|
+
process.env.SAGE_PRIVY_LOGIN_MODE = 'web';
|
|
141
|
+
}
|
|
142
|
+
if (!process.env.SAGE_QUIET_JSON && process.env.SAGE_VERBOSE === '1') {
|
|
143
|
+
console.log('🔐 Using web relay authentication (same wallet as web app)');
|
|
149
144
|
}
|
|
150
145
|
this.wallet = new PrivyAuthWalletManager();
|
|
151
146
|
const ok = await this.wallet.initialize();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: E2E Test Prompt
|
|
3
|
+
description: A test prompt for validating the /prompt/:cid endpoint
|
|
4
|
+
tags: [test, e2e, validation]
|
|
5
|
+
kind: "prompt"
|
|
6
|
+
publishable: true
|
|
7
|
+
targets: []
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# E2E Test Prompt
|
|
11
|
+
|
|
12
|
+
This is a test prompt created to validate the prompt allowlist flow.
|
|
13
|
+
|
|
14
|
+
## Purpose
|
|
15
|
+
- Test that prompts published to a DAO get added to the allowlist
|
|
16
|
+
- Verify that `/prompt/:cid` endpoint serves allowlisted prompts
|
|
17
|
+
- Confirm library sync properly indexes prompt CIDs
|
|
18
|
+
|
|
19
|
+
## Instructions
|
|
20
|
+
When this prompt is accessible via the API, the allowlist flow is working correctly.
|
|
21
|
+
|
|
22
|
+
Created: 2025-12-26
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "build-web3",
|
|
3
|
+
"description": "Build Web3 dApps and smart contracts from scratch through shipping. Full lifecycle for EVM/Base development with Solidity, Foundry, Next.js, and viem/wagmi. Covers contracts, frontend, testing, security analysis, and deployment.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": "Sage Protocol",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": ["web3", "solidity", "foundry", "next.js", "viem", "wagmi", "smart-contracts", "dapp"],
|
|
8
|
+
"category": "development",
|
|
9
|
+
"homepage": "https://github.com/sage-protocol",
|
|
10
|
+
"repository": "https://github.com/sage-protocol/sage-protocol"
|
|
11
|
+
}
|