@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.
@@ -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
- return getAddress(registries[0].id);
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
- // Only --local mode requires appSecret for direct Privy SDK calls
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/device code flow - uses relay, no secret needed
304
- // Force device code mode when secret is not available
305
- if (!appSecret) {
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 preferSessionFlow = relayEnabled && !hasAppSecret;
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;
@@ -101,6 +101,7 @@ const COMMAND_CATALOG = {
101
101
  'info',
102
102
  'fork',
103
103
  'personal',
104
+ 'vault',
104
105
  'delete'
105
106
  ],
106
107
  subcommandAliases: {
@@ -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
- if (!session.signer.provider && provider) {
18
- session.signer.connect(provider);
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 session.signer;
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
- // Device code flow via relay works with just appId (no secret needed)
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
- // Force device code mode when secret is not available
140
- if (!appSecret) {
141
- process.env.SAGE_PRIVY_LOGIN_MODE = 'device';
142
- if (!process.env.SAGE_QUIET_JSON && process.env.SAGE_VERBOSE === '1') {
143
- console.log('🔐 Using device code authentication via web app relay...');
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-protocol/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Sage Protocol CLI for managing AI prompt libraries",
5
5
  "bin": {
6
6
  "sage": "./bin/sage.js"