@thirdfy/agent-cli 0.3.0 → 0.4.1

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +59 -19
  3. package/package.json +2 -2
  4. package/scripts/validate-cli-contract-schemas.mjs +6 -0
  5. package/src/cli/errors.mjs +58 -8
  6. package/src/cli/help.mjs +1 -1
  7. package/src/cli/manifest.mjs +62 -6
  8. package/src/cli/options/bootstrap.mjs +15 -3
  9. package/src/cli/options/chains.mjs +5 -0
  10. package/src/cli/options/credits.mjs +9 -0
  11. package/src/cli/options/data.mjs +13 -1
  12. package/src/cli/options/index.mjs +10 -4
  13. package/src/cli/options/walletControl.mjs +8 -0
  14. package/src/commands/bootstrap.mjs +44 -5
  15. package/src/commands/chat.mjs +3 -0
  16. package/src/commands/credits.mjs +109 -16
  17. package/src/commands/delegation.mjs +3 -1
  18. package/src/commands/discovery.mjs +156 -13
  19. package/src/commands/doctor.mjs +5 -2
  20. package/src/commands/execute.mjs +96 -3
  21. package/src/commands/prompt.mjs +5 -3
  22. package/src/commands/wallet.mjs +238 -3
  23. package/src/commands/walletControl.mjs +66 -0
  24. package/src/contracts/cli/schemas/actions.response.schema.json +45 -0
  25. package/src/contracts/cli/schemas/chains-non-evm.response.schema.json +76 -0
  26. package/src/contracts/cli/schemas/chains.response.schema.json +151 -0
  27. package/src/contracts/cli/schemas/error.response.schema.json +61 -0
  28. package/src/contracts/cli/schemas/preview.response.schema.json +179 -0
  29. package/src/contracts/cli/schemas/readiness.response.schema.json +94 -0
  30. package/src/contracts/cli/schemas/wallet-profile.response.schema.json +127 -0
  31. package/src/core/args.mjs +10 -2
  32. package/src/core/chainRegistry.mjs +223 -1
  33. package/src/core/context.mjs +57 -14
  34. package/src/core/errorEnvelope.mjs +209 -0
  35. package/src/core/fundingTokenIn.mjs +24 -1
  36. package/src/core/http.mjs +6 -1
  37. package/src/core/readOnlyActions.mjs +10 -0
  38. package/src/core/walletExecuteChainId.mjs +25 -0
  39. package/src/lib/creditsUsdSummary.mjs +48 -4
  40. package/src/runtime/actions/actionList.mjs +87 -0
  41. package/src/runtime/actions/selection.mjs +17 -22
  42. package/src/runtime/execution/actionPreflight.mjs +50 -14
  43. package/src/runtime/execution/payloads.mjs +36 -28
  44. package/src/runtime/execution/runners.mjs +75 -21
  45. package/src/runtime/execution/svmLane.mjs +106 -0
  46. package/src/runtime/execution/walletControl.mjs +104 -0
  47. package/src/runtime/handlers.mjs +18 -2
  48. package/src/runtime/index.mjs +1 -1
  49. package/src/runtime/main.mjs +21 -5
  50. package/src/runtime/providerHints/apps.mjs +36 -0
  51. package/src/runtime/providerHints/catalogHint.mjs +55 -0
  52. package/src/runtime/providerHints/cex.mjs +130 -0
  53. package/src/runtime/providerHints/data.mjs +144 -0
  54. package/src/runtime/providerHints/dispatch.mjs +177 -0
  55. package/src/runtime/providerHints/dogeos.mjs +38 -0
  56. package/src/runtime/providerHints/earn.mjs +201 -0
  57. package/src/runtime/providerHints/perps.mjs +346 -0
  58. package/src/runtime/providerHints/prediction.mjs +128 -0
  59. package/src/runtime/providerHints/solana.mjs +85 -0
  60. package/src/runtime/providerHints/trading.mjs +78 -0
  61. package/src/runtime/providerHints/walletControl.mjs +39 -0
  62. package/src/runtime/risk/notional.mjs +59 -0
  63. package/src/runtime/risk/orderPreview.mjs +154 -0
  64. package/src/runtime/risk/riskRefusal.mjs +74 -0
  65. package/src/runtime/validation/actionParamsGate.mjs +2 -0
  66. package/src/runtime/providerHints.mjs +0 -1017
@@ -1,7 +1,17 @@
1
- /** Commander 13+ rejects multi-character short flags; keep the historical `-ni` spelling working. */
1
+ /**
2
+ * Commander 13+ rejects multi-character short flags; keep the historical `-ni` spelling working, and
3
+ * `--ni`, the spelling the README and onboarding help use.
4
+ */
5
+ const NOT_INTERACTIVE_ALIASES = new Set(['-ni', '--ni']);
6
+
2
7
  export function normalizeLegacyShortFlags(argv) {
3
8
  const end = argv.indexOf('--');
4
- return argv.map((arg, index) => (arg === '-ni' && (end === -1 || index < end) ? '--not-interactive' : arg));
9
+ return argv.map((arg, index) => (NOT_INTERACTIVE_ALIASES.has(arg) && (end === -1 || index < end) ? '--not-interactive' : arg));
10
+ }
11
+
12
+ /** `help onboarding --path`: one path, as thirdfy-mcp `describeOnboardingPath`. */
13
+ export function attachOnboardingHelpOptions(cmd) {
14
+ return cmd.option('--path <id>', 'one path: email_otp | wallet_sign | existing_credentials | mcp_bootstrap | all');
5
15
  }
6
16
 
7
17
  export function attachBootstrapOptions(cmd) {
@@ -16,9 +26,11 @@ export function attachBootstrapOptions(cmd) {
16
26
  .option('--otp <otp>', 'email OTP alias')
17
27
  .option('--accept-terms', 'accept terms for email login')
18
28
  .option('--key-name <name>', 'Privy key name')
19
- .option('--not-interactive', 'fail fast when required login input is missing (alias: -ni)')
29
+ .option('--not-interactive', 'fail fast when required login input is missing (aliases: --ni, -ni)')
20
30
  .option('--no-bootstrap-agent', 'skip agent bootstrap on email login')
21
31
  .option('--rotate-agent-key', 'rotate agent key')
32
+ .option('--owner-payout-address <addr>', 'owner payout address for exits from a wallet under wallet control (API 4.0+)')
33
+ .option('--allowed-actions <list>', "comma-separated catalog action names for the agent's action template (API 4.0+)")
22
34
  .option('--login-attempt-id <id>', 'email login attempt id')
23
35
  .option('--all', 'logout all sessions')
24
36
  .option('--clear-agent-key', 'clear stored agent key on logout');
@@ -1,3 +1,8 @@
1
1
  export function attachChainOptions(cmd) {
2
2
  return cmd.option('--chain-id <id>', 'chain id');
3
3
  }
4
+
5
+ /** Options for `chains` (thirdfy-mcp getChainCapabilities / getNonEvmCapabilities). */
6
+ export function attachChainListOptions(cmd) {
7
+ return cmd.option('--non-evm', 'only the non-EVM (Solana) execution contract');
8
+ }
@@ -13,3 +13,12 @@ export function attachCreditsOptions(cmd) {
13
13
  )
14
14
  .option('--surface <surface>', 'billing surface (console, automation, cli, agent_api)', 'cli');
15
15
  }
16
+
17
+ /** `credits buy` only: no catalog or pricing flags, and no --action / --params to override. */
18
+ export function attachCreditsBuyOptions(cmd) {
19
+ return cmd
20
+ .option('--usd <dollars>', 'whole US dollars of credits to buy with USDC from the agent wallet')
21
+ .option('--tx-hash <hash>', 'resume a purchase whose USDC payment was already sent (after WRITE_OUTCOME_UNKNOWN)')
22
+ .option('--chain-id <id>', 'chain the USDC is paid on (default 8453 Base; 84532 Base Sepolia on test APIs)')
23
+ .option('--idempotency-key <key>', 'idempotency key for this purchase');
24
+ }
@@ -1,4 +1,4 @@
1
- /** Options of the data commands: `signals`, and the data-category filter of `actions`. */
1
+ /** Options of the discovery and data commands: `actions` filters and `signals`. */
2
2
 
3
3
  export function attachSignalsOptions(cmd) {
4
4
  return cmd
@@ -12,3 +12,15 @@ export function attachDataCategoryOptions(cmd) {
12
12
  'only data reads of this category (market_data, signal, research, wallet); comma-separated for several',
13
13
  );
14
14
  }
15
+
16
+ /** `actions` filters. It lists actions only, so it takes none of the `run` / `preflight` options. */
17
+ export function attachActionListOptions(cmd) {
18
+ return cmd
19
+ .option('--provider <id>', 'only this provider (hyperliquid, polymarket, earn, ...)')
20
+ .option('--chain-id <id>', 'only actions on this chain')
21
+ .option('--catalog <id>', 'only this actions catalog')
22
+ .option('--search <text>', 'only actions whose name, description or aliases contain this text (one page)')
23
+ .option('--limit <n>', 'page size (concise: 50 by default, at most 100; detailed: 10, at most 25)')
24
+ .option('--offset <n>', 'skip this many matching actions (default 0)')
25
+ .option('--view <view>', 'concise (default with paging) or detailed (full entries with paramsSchema)');
26
+ }
@@ -4,20 +4,21 @@ import { attachExecutionOptions } from './execution.mjs';
4
4
  import { attachDelegationOptions } from './delegation.mjs';
5
5
  import { attachTrackingOptions } from './tracking.mjs';
6
6
  import { attachWalletOptions } from './wallet.mjs';
7
- import { attachBootstrapOptions } from './bootstrap.mjs';
7
+ import { attachBootstrapOptions, attachOnboardingHelpOptions } from './bootstrap.mjs';
8
8
  import { attachCredentialsOptions } from './credentials.mjs';
9
9
  import { attachAnalyticsOptions } from './analytics.mjs';
10
10
  import { attachPolymarketOptions } from './polymarket.mjs';
11
11
  import { attachHummingbotOptions } from './hummingbot.mjs';
12
12
  import { attachCondorOptions } from './condor.mjs';
13
13
  import { attachAgentRegisterOptions } from './agent.mjs';
14
- import { attachChainOptions } from './chains.mjs';
15
- import { attachCreditsOptions } from './credits.mjs';
14
+ import { attachChainListOptions, attachChainOptions } from './chains.mjs';
15
+ import { attachCreditsBuyOptions, attachCreditsOptions } from './credits.mjs';
16
16
  import { attachChatOptions } from './chat.mjs';
17
17
  import { attachVenueOptions } from './venue.mjs';
18
18
  import { attachBrainOptions } from './brain.mjs';
19
19
  import { attachCertificationOptions } from './certification.mjs';
20
- import { attachDataCategoryOptions, attachSignalsOptions } from './data.mjs';
20
+ import { attachActionListOptions, attachDataCategoryOptions, attachSignalsOptions } from './data.mjs';
21
+ import { attachWalletControlOptions } from './walletControl.mjs';
21
22
 
22
23
  const BUNDLES = {
23
24
  auth: attachAuthOptions,
@@ -26,6 +27,7 @@ const BUNDLES = {
26
27
  tracking: attachTrackingOptions,
27
28
  wallet: attachWalletOptions,
28
29
  bootstrap: attachBootstrapOptions,
30
+ onboardingHelp: attachOnboardingHelpOptions,
29
31
  credentials: attachCredentialsOptions,
30
32
  analytics: attachAnalyticsOptions,
31
33
  polymarket: attachPolymarketOptions,
@@ -33,13 +35,17 @@ const BUNDLES = {
33
35
  condor: attachCondorOptions,
34
36
  agent: attachAgentRegisterOptions,
35
37
  chains: attachChainOptions,
38
+ chainList: attachChainListOptions,
36
39
  credits: attachCreditsOptions,
40
+ creditsBuy: attachCreditsBuyOptions,
37
41
  chat: attachChatOptions,
38
42
  venue: attachVenueOptions,
39
43
  brain: attachBrainOptions,
40
44
  certification: attachCertificationOptions,
41
45
  signals: attachSignalsOptions,
42
46
  dataCategory: attachDataCategoryOptions,
47
+ actionList: attachActionListOptions,
48
+ walletControl: attachWalletControlOptions,
43
49
  };
44
50
 
45
51
  /** @param {import('commander').Command} cmd */
@@ -0,0 +1,8 @@
1
+ /** Options of `wallet-control request|submit` (owner actions on a user-owned agent wallet). */
2
+ export function attachWalletControlOptions(cmd) {
3
+ return cmd
4
+ .option('--action <action>', 'owner action: update_policy, stop_agent, resume_agent, change_payout, export')
5
+ .option('--payout-address <addr>', 'new owner payout address (change_payout)')
6
+ .option('--recipient-public-key <key>', 'HPKE recipient public key (export)')
7
+ .option('--signature-input <json>', 'the signatureInput of the request the owner signed (submit)');
8
+ }
@@ -1,4 +1,4 @@
1
- import { requireFlag, isNonInteractive } from '../core/args.mjs';
1
+ import { requireFlag, isNonInteractive, usageError } from '../core/args.mjs';
2
2
  import { createCliError } from '../core/envelope.mjs';
3
3
  import { PROFILE_DEFAULTS } from '../core/constants.mjs';
4
4
  import {
@@ -16,6 +16,7 @@ import {
16
16
  getProfileConfigPath,
17
17
  } from '../core/context.mjs';
18
18
  import { apiGet, apiPost, requestWithRouteFallback } from '../core/http.mjs';
19
+ import { isEvmAddress, isSolanaAddress } from '../core/chainRegistry.mjs';
19
20
  import { createRequire } from 'module';
20
21
 
21
22
  const require = createRequire(import.meta.url);
@@ -153,12 +154,24 @@ async function commandConfigSet(ctx, flags) {
153
154
  }
154
155
 
155
156
 
157
+ const ONBOARDING_PATHS = ['email_otp', 'wallet_sign', 'existing_credentials', 'mcp_bootstrap'];
158
+
159
+ /** `help onboarding [--path <id>]`: every path, or one with `selectedPath` (thirdfy-mcp `describeOnboardingPath`). */
156
160
  async function commandHelpOnboarding(ctx, flags) {
161
+ const help = buildOnboardingHelp(ctx, flags);
162
+ const selectedPath = String(flags.path || 'all').trim().toLowerCase();
163
+ if (selectedPath !== 'all' && !ONBOARDING_PATHS.includes(selectedPath)) {
164
+ throw usageError(`--path must be one of ${ONBOARDING_PATHS.join(', ')} or all`, 'path');
165
+ }
166
+ const data =
167
+ selectedPath === 'all'
168
+ ? { selectedPath, ...help }
169
+ : { selectedPath, ...help.paths.find((entry) => entry.id === selectedPath), nonInteractive: help.nonInteractive, gas: help.gas, apiBase: help.apiBase };
157
170
  printEnvelope({
158
171
  success: true,
159
172
  code: 'ONBOARDING_HELP_OK',
160
- message: 'Thirdfy onboarding help',
161
- data: buildOnboardingHelp(ctx, flags),
173
+ message: selectedPath === 'all' ? 'Thirdfy onboarding help' : `Thirdfy onboarding help: ${selectedPath}`,
174
+ data,
162
175
  meta: { apiBase: ctx.apiBase },
163
176
  });
164
177
  }
@@ -289,6 +302,16 @@ async function commandLogin(ctx, flags) {
289
302
 
290
303
  let bootstrapAgentApiKey = '';
291
304
  let bootstrapPerformed = false;
305
+ // API 4.0: where exits from a wallet under wallet control may pay, and the agent's action
306
+ // template. The API validates both and answers ownerPayout / actionTemplate (thirdfy-mcp
307
+ // bootstrapSelfIdentity parity).
308
+ const ownerPayoutAddress = String(flags.ownerPayoutAddress || '').trim();
309
+ const allowedActions = String(flags.allowedActions || '')
310
+ .split(',')
311
+ .map((entry) => entry.trim())
312
+ .filter(Boolean);
313
+ let ownerPayout;
314
+ let actionTemplate;
292
315
  const requestedAgentApiKey = String(flags.agentApiKey || '').trim();
293
316
  const bootstrapAgentKey = String(flags.agentKey || '').trim();
294
317
  if (!requestedAgentApiKey && bootstrapAgentKey) {
@@ -308,11 +331,15 @@ async function commandLogin(ctx, flags) {
308
331
  custodyMode,
309
332
  ...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
310
333
  rotate: parseBooleanFlag(flags.rotateAgentKey, false),
334
+ ...(ownerPayoutAddress ? { ownerPayoutAddress } : {}),
335
+ ...(allowedActions.length ? { allowedActions } : {}),
311
336
  },
312
337
  });
313
338
  bootstrapAgentApiKey = String(
314
339
  bootstrapResponse?.agentApiKey || bootstrapResponse?.data?.agentApiKey || ''
315
340
  ).trim();
341
+ ownerPayout = bootstrapResponse?.ownerPayout || bootstrapResponse?.data?.ownerPayout;
342
+ actionTemplate = bootstrapResponse?.actionTemplate || bootstrapResponse?.data?.actionTemplate;
316
343
  bootstrapPerformed = true;
317
344
  }
318
345
  }
@@ -355,6 +382,8 @@ async function commandLogin(ctx, flags) {
355
382
  hasOwnerSessionToken: Boolean(ownerSessionToken),
356
383
  hasAgentApiKey: Boolean(requestedAgentApiKey || bootstrapAgentApiKey),
357
384
  bootstrapPerformed,
385
+ ...(ownerPayout ? { ownerPayout } : {}),
386
+ ...(actionTemplate ? { actionTemplate } : {}),
358
387
  runMode: loginRunMode,
359
388
  custodyMode,
360
389
  configPath: getProfileConfigPath(),
@@ -499,12 +528,15 @@ async function commandLoginEmail(ctx, flags) {
499
528
  // Persist first so operators keep session tokens even when wallet provisioning failed.
500
529
  persistProfileConfig(next);
501
530
 
531
+ // An EVM (0x) or Solana (base58) execution wallet counts; an agent may have only a Solana one.
502
532
  const executionWalletAddresses = Object.values(executionWallets)
503
533
  .map((row) => (row && typeof row === 'object' ? String(row.walletAddress || '').trim() : ''))
504
- .filter((addr) => /^0x[a-fA-F0-9]{40}$/.test(addr));
534
+ .filter((addr) => isEvmAddress(addr) || isSolanaAddress(addr));
505
535
  const hasExecutionWallet = executionWalletAddresses.length > 0;
506
536
  if (runMode === 'agent_wallet' && (executionWalletsError || !hasExecutionWallet)) {
507
- const fundHint = executionWalletAddresses[0] || 'the managed execution wallet address for each chain';
537
+ const fundHint = executionWalletAddresses.length
538
+ ? executionWalletAddresses.join(', ')
539
+ : 'the managed execution wallet address for each chain';
508
540
  throw createCliError(
509
541
  'LOGIN_EMAIL_INCOMPLETE',
510
542
  executionWalletsError
@@ -538,6 +570,13 @@ async function commandLoginEmail(ctx, flags) {
538
570
  ? {
539
571
  fundingReminder:
540
572
  'Fund executionWallets addresses for agent_wallet. Do not fund the owner embedded wallet for Path A trading.',
573
+ ...(executionWalletAddresses.some((addr) => isSolanaAddress(addr))
574
+ ? {
575
+ solanaFundingReminder: `Fund the Solana execution wallet ${executionWalletAddresses
576
+ .filter((addr) => isSolanaAddress(addr))
577
+ .join(', ')} (base58; give the user this address, not the EVM one) with the SPL tokens (such as USDC) the agent trades. Thirdfy sponsors Solana network fees; keep a little SOL only for rent on new token accounts (about 0.002 SOL each).`,
578
+ }
579
+ : {}),
541
580
  }
542
581
  : {}),
543
582
  },
@@ -16,6 +16,9 @@ const NETWORK_CHAIN_IDS = {
16
16
  'arc-testnet': 5042002,
17
17
  'arc-mainnet': 5042,
18
18
  'arc': 5042,
19
+ // Solana (svm). Without these, --network sol-mainnet fell through to the Base default.
20
+ 'sol-mainnet': 101,
21
+ 'sol-devnet': 103,
19
22
  };
20
23
 
21
24
  const DEFAULT_CHAT_CHAIN_ID = 8453;
@@ -1,9 +1,12 @@
1
- import { requireFlag } from '../core/args.mjs';
1
+ import { requireFlag, usageError } from '../core/args.mjs';
2
2
  import { apiGet, apiPost } from '../core/http.mjs';
3
+ import { loadProfileConfig, resolveCreditsLedger } from '../core/context.mjs';
4
+ import { createCliError } from '../core/envelope.mjs';
3
5
  import { toAvantisSpelling } from '../core/venueBrand.mjs';
4
6
  import {
5
7
  formatCreditsEstimateSummary,
6
8
  formatCreditsModelsSummary,
9
+ stripUsdFields,
7
10
  } from '../lib/creditsUsdSummary.mjs';
8
11
 
9
12
  function resolveToolCallsEstimate(flags) {
@@ -32,7 +35,38 @@ function resolveToolCallsEstimate(flags) {
32
35
  };
33
36
  }
34
37
 
35
- export function createCreditsCommands({ printEnvelope, getAuthToken }) {
38
+ /** Thirdfy credits are bought on Base with USDC from the agent's wallet (thirdfy-api-v2 `buy-agent-credits`). */
39
+ const CREDITS_PURCHASE_CHAIN_ID = 8453;
40
+
41
+ export function createCreditsCommands({ printEnvelope, getAuthToken, buildOwnerAuthHeaders, commandWalletExecute }) {
42
+ /**
43
+ * `credits buy --usd <n>`: buy the agent's Thirdfy credits with USDC from its execution wallet on
44
+ * Base (gas sponsored), as thirdfy-mcp's `walletExecute buy-agent-credits` does. A shortcut for
45
+ * `wallet execute --action buy-agent-credits --params '{"usdAmount":n}' --chain-id 8453`; the API
46
+ * sets the allowed range (whole dollars, $2 to $1,000 today). A purchase whose claim did not
47
+ * finish answers WRITE_OUTCOME_UNKNOWN: check `credits balance`, do not buy again; `--tx-hash`
48
+ * resumes that purchase without paying twice. `--chain-id` defaults to Base (84532 on test APIs).
49
+ */
50
+ async function commandCreditsBuy(ctx, flags, capabilities) {
51
+ const raw = String(requireFlag(flags, 'usd', 'Missing --usd (whole US dollars, for example --usd 10)')).trim();
52
+ if (!/^\d+$/.test(raw) || Number(raw) <= 0) throw usageError('--usd must be a whole number of US dollars, for example 10', 'usd');
53
+ const usdAmount = Number(raw);
54
+ const txHash = String(flags.txHash || '').trim();
55
+ await commandWalletExecute(
56
+ ctx,
57
+ {
58
+ ...flags,
59
+ action: 'buy-agent-credits',
60
+ // txHash resumes a purchase whose payment was already sent (the catalog row takes it).
61
+ params: JSON.stringify({ usdAmount, ...(txHash ? { txHash } : {}) }),
62
+ chainId: String(flags.chainId || CREDITS_PURCHASE_CHAIN_ID),
63
+ estimatedAmountUsd: String(usdAmount),
64
+ runMode: 'agent_wallet',
65
+ },
66
+ capabilities,
67
+ );
68
+ }
69
+
36
70
  async function commandCreditsTransactions(ctx, flags) {
37
71
  const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credits transactions');
38
72
  const page = Number(flags.page || 1);
@@ -111,29 +145,88 @@ export function createCreditsCommands({ printEnvelope, getAuthToken }) {
111
145
  });
112
146
  }
113
147
 
148
+ /**
149
+ * `pricing quote --action <id>`: the credits one catalog action costs, and `wouldBePaidBy`
150
+ * (agent_credits | free_request | owner_credits | none) when the API can read the balances
151
+ * (thirdfy-api-v2#887). The ledger follows `resolveCreditsLedger`. Agent: `POST /agent/credits/quote`
152
+ * with the agent's key. Owner (session token or auth token): `/credits/pricing/quote`, for one of the
153
+ * owner's agents with --agent-key. An API without the agent route (404, before #887) is quoted
154
+ * through the owner route when owner auth is set and the key's agent is known: --agent-key, or the
155
+ * configured agent when the key also comes from env or config. Credits only.
156
+ */
114
157
  async function commandPricingQuote(ctx, flags) {
115
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for pricing quote');
116
158
  const action = requireFlag(flags, 'action', 'Missing --action for pricing quote');
117
- const response = await apiPost(
118
- ctx,
119
- '/api/v1/credits/pricing/quote',
120
- {
121
- // Veranta names price as their Avantis spelling, which every API deployment knows.
122
- actionKey: toAvantisSpelling(action),
123
- chainId: Number(flags.chainId || 8453),
124
- },
125
- { Authorization: `Bearer ${authToken}` },
126
- );
159
+ // Veranta names price as their Avantis spelling, which every API deployment knows.
160
+ const body = { actionKey: toAvantisSpelling(action), chainId: Number(flags.chainId || 8453) };
161
+ const { ledger: requestedLedger, agentApiKey, agentApiKeyExplicit } = resolveCreditsLedger(flags);
162
+ const flagAgentKey = String(flags.agentKey || '').trim();
163
+ const ownerHeaders = () =>
164
+ buildOwnerAuthHeaders(flags, 'Missing --owner-session-token or --auth-token (or THIRDFY_OWNER_SESSION_TOKEN / THIRDFY_AUTH_TOKEN), or an agent key, for pricing quote');
165
+ const ownerQuote = (agentKey) =>
166
+ apiPost(ctx, '/api/v1/credits/pricing/quote', { ...body, ...(agentKey ? { agentKey } : {}) }, ownerHeaders());
167
+ let ledger = requestedLedger;
168
+ let quotedAgentKey = flagAgentKey || null;
169
+ const warnings = [];
170
+ let response;
171
+ if (ledger === 'agent') {
172
+ try {
173
+ response = await apiPost(ctx, '/api/v1/agent/credits/quote', body, { 'x-agent-api-key': agentApiKey });
174
+ quotedAgentKey = response?.agentKey || null;
175
+ if (flagAgentKey && quotedAgentKey && flagAgentKey.toLowerCase() !== String(quotedAgentKey).toLowerCase()) {
176
+ warnings.push(`--agent-key ${flagAgentKey} is not the agent of this API key; the quote is for ${quotedAgentKey}.`);
177
+ }
178
+ } catch (error) {
179
+ if (Number(error?.statusCode) !== 404) throw error;
180
+ // Owner auth is needed for the older owner route, and the agent must be the key's own.
181
+ let hasOwner = true;
182
+ try {
183
+ ownerHeaders();
184
+ } catch {
185
+ hasOwner = false;
186
+ }
187
+ if (!hasOwner) {
188
+ throw createCliError(
189
+ 'OWNER_AUTH_REQUIRED',
190
+ 'This Thirdfy API has no agent quote route yet: pass --owner-session-token or --auth-token for an owner quote.',
191
+ { blockedStage: 'auth' },
192
+ );
193
+ }
194
+ const pairedAgentKey = flagAgentKey || (agentApiKeyExplicit ? '' : String(process.env.THIRDFY_AGENT_KEY || loadProfileConfig()?.agent?.agentKey || '').trim());
195
+ if (!pairedAgentKey) {
196
+ throw createCliError(
197
+ 'AGENT_IDENTITY_UNAVAILABLE',
198
+ 'This Thirdfy API has no agent quote route yet; to quote through the owner route, pass --agent-key (the agent of this API key).',
199
+ { blockedStage: 'auth' },
200
+ );
201
+ }
202
+ ledger = 'owner';
203
+ quotedAgentKey = pairedAgentKey;
204
+ response = await ownerQuote(pairedAgentKey);
205
+ }
206
+ } else {
207
+ response = await ownerQuote(flagAgentKey);
208
+ }
127
209
  printEnvelope({
128
210
  success: true,
129
211
  code: 'OK',
130
- message: 'Action pricing quote generated',
131
- data: response,
132
- meta: { apiBase: ctx.apiBase, action },
212
+ // wouldBePaidBy (agent_credits | free_request | owner_credits | none), when the API sends it.
213
+ message: response?.wouldBePaidBy ? `Action pricing quote generated; would be paid by ${response.wouldBePaidBy}` : 'Action pricing quote generated',
214
+ // An action's cost is in credits only, never dollars (thirdfy-mcp quoteActionPricing).
215
+ data: stripUsdFields(response),
216
+ meta: {
217
+ apiBase: ctx.apiBase,
218
+ action,
219
+ ledger,
220
+ ...(quotedAgentKey ? { agentKey: quotedAgentKey } : {}),
221
+ ...(warnings.length ? { warnings } : {}),
222
+ },
133
223
  });
134
224
  }
135
225
 
226
+
227
+
136
228
  return {
229
+ commandCreditsBuy,
137
230
  commandCreditsTransactions,
138
231
  commandCreditsUsage,
139
232
  commandModelsList,
@@ -426,9 +426,11 @@ async function commandDelegationRedeem(ctx, flags, capabilities) {
426
426
  if (!['thirdfy', 'hybrid', 'agent_wallet'].includes(selectedRunMode)) {
427
427
  throw createCliError('DELEGATION_REDEEM_MODE_INVALID', 'delegation redeem supports only --run-mode thirdfy|hybrid|agent_wallet');
428
428
  }
429
+ // Pass the run mode on only when it was given: a profile / config default must still be able to
430
+ // switch to agent_wallet on a Solana action (commandRun resolves the effective mode itself).
429
431
  const delegatedFlags = {
430
432
  ...flags,
431
- runMode: selectedRunMode,
433
+ ...(flags.runMode ? { runMode: selectedRunMode } : {}),
432
434
  };
433
435
  if (!delegatedFlags.estimatedAmountUsd) {
434
436
  delegatedFlags.estimatedAmountUsd = '5';
@@ -1,10 +1,58 @@
1
1
  import { requireFlag, parseJsonFlag } from '../core/args.mjs';
2
2
  import { apiGet, apiPost } from '../core/http.mjs';
3
- import { extractActions, getCachedActionsCatalog } from '../core/context.mjs';
3
+ import { extractActions, getCachedActionsCatalogSnapshot, resolveCreditsLedger } from '../core/context.mjs';
4
4
  import { getEffectiveRunMode } from '../core/runMode.mjs';
5
- import { formatCreditsBalanceSummary } from '../lib/creditsUsdSummary.mjs';
5
+ import { creditsFreeQuotaNote, formatCreditsBalanceSummary, stripUsdFields } from '../lib/creditsUsdSummary.mjs';
6
6
  import { filterActionsByChain } from '../runtime/actions/chainFilter.mjs';
7
- export function createDiscoveryCommands({ printEnvelope, applyActionFilters, getTradingProviderHint, resolveChainSupport, getAuthToken, resolveAgentApiKey }) {
7
+ import { pageActionList, readActionListPaging } from '../runtime/actions/actionList.mjs';
8
+ import {
9
+ caip2ForChainId,
10
+ chainRegistrySource,
11
+ configuredEvmChainIds,
12
+ configuredSvmChainIds,
13
+ getApiChainRegistry,
14
+ isSvmChainId,
15
+ } from '../core/chainRegistry.mjs';
16
+ import { SOLANA_PROVIDER_HINTS } from '../runtime/providerHints/solana.mjs';
17
+
18
+ /** Execution lanes when the API's capabilities name none (thirdfy-mcp SUPPORTED_EXECUTION_LANES). */
19
+ const DEFAULT_EXECUTION_LANES = Object.freeze(['thirdfy', 'hybrid', 'self', 'agent_wallet', 'validation_only']);
20
+
21
+ /** Chain ids the catalog rows name (`supportedChains` and the like), EVM only. Same as thirdfy-mcp `inferCatalogEvmChainIds`. */
22
+ function catalogEvmChainIds(rows) {
23
+ const found = new Set();
24
+ const collect = (value) => {
25
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) found.add(value);
26
+ else if (typeof value === 'string' && /^\d+$/.test(value)) found.add(Number(value));
27
+ else if (Array.isArray(value)) value.forEach(collect);
28
+ };
29
+ for (const row of Array.isArray(rows) ? rows : []) {
30
+ for (const key of ['chainId', 'chainIds', 'supportedChainIds', 'supportedChains', 'chains', 'networks']) collect(row?.[key]);
31
+ }
32
+ return [...found].filter((chainId) => !isSvmChainId(chainId)).sort((a, b) => a - b);
33
+ }
34
+
35
+ /** The Solana execution contract, as thirdfy-mcp `solanaExecutionContract` with CLI commands. */
36
+ function solanaExecutionContract() {
37
+ const chainIds = configuredSvmChainIds();
38
+ return {
39
+ enabled: chainIds.length > 0,
40
+ chainFamily: 'svm',
41
+ chainIds,
42
+ caip2: Object.fromEntries(chainIds.map((id) => [id, caip2ForChainId(id)]).filter(([, caip2]) => caip2)),
43
+ requiredRunMode: 'agent_wallet',
44
+ supportedExecutionLanes: ['agent_wallet'],
45
+ wallet: "The agent's managed Privy Solana wallet. executionWalletAddress is base58 and case-sensitive.",
46
+ commands: ['preview', 'run', 'wallet execute', 'agent run'],
47
+ evmOnlyCommands: ['self-exec', 'wallet sign', 'wallet submit'],
48
+ providers: Object.keys(SOLANA_PROVIDER_HINTS),
49
+ flow: '`thirdfy-agent login email` (executionWallets) or `preflight --chain-id 101 --run-mode agent_wallet` gives the base58 executionWalletAddress -> give the user that address (not the EVM one) and fund it with the token to spend -> `run --chain-id 101 --run-mode agent_wallet` with base58 params (for example jupiter_swap {inputMint, outputMint, amount}).',
50
+ gas: 'Thirdfy sponsors Solana network fees (the execution-wallet reply says gasSponsored): never ask the user to buy SOL for gas. The wallet pays only rent for new token accounts (about 0.002 SOL each) and SOL the action spends; a write short of it is refused with INSUFFICIENT_NATIVE_GAS.',
51
+ notes:
52
+ 'Solana execution runs only with --run-mode agent_wallet on chain 101 (103 devnet where a provider supports it). self, hybrid and thirdfy stay EVM-only. Check `actions --chain-id 101` (supportedChains) for each action\'s chains.',
53
+ };
54
+ }
55
+ export function createDiscoveryCommands({ printEnvelope, applyActionFilters, getProviderHintForActions, resolveChainSupport, getAuthToken, buildOwnerAuthHeaders, resolveAgentApiKey }) {
8
56
  async function commandCatalogsList(ctx, flags, capabilities) {
9
57
  const response = await apiGet(ctx, '/api/v1/agent/actions/catalog');
10
58
  const actions = extractActions(response);
@@ -32,20 +80,32 @@ async function commandCatalogsList(ctx, flags, capabilities) {
32
80
  }
33
81
 
34
82
  async function commandActions(ctx, flags, capabilities) {
35
- const policyAware = Boolean(flags.agentApiKey);
83
+ // The agent's own list whenever execution would run with a key (flag, env or config), so `actions`
84
+ // lists what `run` / `preflight` accept, as thirdfy-mcp `getAgentActions` does.
85
+ const paging = readActionListPaging(flags);
86
+ const snapshot = await getCachedActionsCatalogSnapshot(ctx, flags);
36
87
  const chainIdFilter = Number(flags.chainId || 0) || undefined;
37
- const actions = filterActionsByChain(applyActionFilters(await getCachedActionsCatalog(ctx, flags), flags), chainIdFilter);
38
- const providerHint = getTradingProviderHint(flags.provider);
88
+ const actions = filterActionsByChain(applyActionFilters(snapshot.actions, flags), chainIdFilter);
89
+ const providerHint = getProviderHintForActions(flags.provider, actions);
90
+ // --search / --limit / --offset / --view: one page, as thirdfy-mcp getAgentActions answers.
91
+ const page = paging ? pageActionList(actions, paging) : null;
39
92
  const effectiveRunMode = getEffectiveRunMode(ctx, flags);
40
93
  const chainSupport = resolveChainSupport(capabilities, chainIdFilter, effectiveRunMode);
41
94
  printEnvelope({
42
95
  success: true,
43
96
  code: 'OK',
44
97
  message: 'Actions loaded',
45
- data: { actions, ...(providerHint ? { providerHint } : {}) },
98
+ data: {
99
+ ...(page
100
+ ? { actions: page.actions, pagination: page.pagination, view: page.view, ...(page.hint ? { hint: page.hint } : {}) }
101
+ : { actions }),
102
+ ...(providerHint ? { providerHint } : {}),
103
+ },
46
104
  meta: {
47
105
  apiBase: ctx.apiBase,
48
- policyAware,
106
+ // A key was sent; policyScoped: the API answered with the agent policy's allowlist.
107
+ policyAware: snapshot.keyed,
108
+ policyScoped: snapshot.policyScoped,
49
109
  runMode: effectiveRunMode,
50
110
  profile: ctx.profile || null,
51
111
  chainCompatibility: chainSupport,
@@ -54,19 +114,101 @@ async function commandActions(ctx, flags, capabilities) {
54
114
  });
55
115
  }
56
116
 
57
- async function commandCreditsBalance(ctx, flags, capabilities) {
58
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credits balance');
59
- const response = await apiGet(ctx, '/api/v1/credits/balance', {
60
- Authorization: `Bearer ${authToken}`,
117
+ /**
118
+ * `chains`: which chains Thirdfy acts on, from the API's capabilities and catalog (thirdfy-mcp
119
+ * `getChainCapabilities`). `--non-evm`: the Solana execution contract (`getNonEvmCapabilities`).
120
+ */
121
+ async function commandChains(ctx, flags, capabilities) {
122
+ const svmChainIds = configuredSvmChainIds();
123
+ const nonEvmChains = svmChainIds.length > 0 ? ['solana'] : [];
124
+ const meta = { apiBase: ctx.apiBase, capabilitiesVersion: capabilities?.contractVersion || null };
125
+ if (flags.nonEvm) {
126
+ const executionEnabled = svmChainIds.length > 0;
127
+ printEnvelope({
128
+ success: true,
129
+ code: 'OK',
130
+ message: executionEnabled ? 'Solana execution is enabled' : 'Solana execution is not enabled on this API',
131
+ data: {
132
+ supportedChains: nonEvmChains,
133
+ executionEnabled,
134
+ contractVersion: 'v2',
135
+ chainRegistrySource: chainRegistrySource(),
136
+ solana: solanaExecutionContract(),
137
+ notes: executionEnabled
138
+ ? 'Solana execution is enabled through `run` / `wallet execute` / `agent run` with --run-mode agent_wallet on chain 101 (the agent\'s managed Privy Solana wallet). self-exec, wallet sign and wallet submit stay EVM-only.'
139
+ : 'Solana execution is not enabled on this Thirdfy API: its capabilities list no svm chain. Chains 101/103 are refused with SOLANA_NOT_ENABLED.',
140
+ },
141
+ meta,
142
+ });
143
+ return;
144
+ }
145
+ const configured = configuredEvmChainIds();
146
+ let catalogChainIds = [];
147
+ let catalogSource = 'live';
148
+ let catalogError;
149
+ try {
150
+ catalogChainIds = catalogEvmChainIds((await getCachedActionsCatalogSnapshot(ctx, flags)).actions);
151
+ } catch (error) {
152
+ catalogSource = 'unavailable';
153
+ catalogError = error?.message || String(error);
154
+ }
155
+ const apiChains = getApiChainRegistry()?.chains;
156
+ printEnvelope({
157
+ success: true,
158
+ code: 'OK',
159
+ message: 'Chain capabilities loaded',
160
+ data: {
161
+ supportedEvmChainIds: [...new Set([...configured, ...catalogChainIds])].sort((a, b) => a - b),
162
+ configuredEvmChainIds: configured,
163
+ catalogEvmChainIds: catalogChainIds,
164
+ catalogSource,
165
+ ...(catalogError ? { catalogError } : {}),
166
+ supportedNonEvmChains: nonEvmChains,
167
+ supportedSvmChainIds: svmChainIds,
168
+ chainRegistrySource: chainRegistrySource(),
169
+ caip2: Object.fromEntries(
170
+ [...configured, ...svmChainIds].map((id) => [id, caip2ForChainId(id)]).filter(([, caip2]) => caip2),
171
+ ),
172
+ supportedExecutionLanes: Array.isArray(capabilities?.runModes) && capabilities.runModes.length
173
+ ? capabilities.runModes
174
+ : [...DEFAULT_EXECUTION_LANES],
175
+ defaultChainId: 8453,
176
+ defaultExecutionLane: getEffectiveRunMode(ctx, flags),
177
+ nonEvmExecutionContract: solanaExecutionContract(),
178
+ // Each chain the API lists, with its execution lanes (CLI only; the MCP has no per-chain lanes).
179
+ ...(apiChains ? { chains: apiChains } : {}),
180
+ },
181
+ meta,
61
182
  });
183
+ }
184
+
185
+ /**
186
+ * `credits balance`. Owner auth reads the owner's balance (`/credits/balance`). An explicit
187
+ * --agent-api-key, or no owner auth but an agent key in env or config, reads the agent's own ledger
188
+ * (`/agent/credits/balance`: its credits, free requests today and the owner-credits fallback), as
189
+ * thirdfy-mcp getCreditsBalance. Credits only, never dollars.
190
+ */
191
+ async function commandCreditsBalance(ctx, flags, capabilities) {
192
+ const { ledger, agentApiKey } = resolveCreditsLedger(flags);
193
+ const agentLedger = ledger === 'agent';
194
+ const response = agentLedger
195
+ ? await apiGet(ctx, '/api/v1/agent/credits/balance', { 'x-agent-api-key': agentApiKey })
196
+ : await apiGet(
197
+ ctx,
198
+ '/api/v1/credits/balance',
199
+ buildOwnerAuthHeaders(flags, 'Missing --owner-session-token or --auth-token (or THIRDFY_OWNER_SESSION_TOKEN / THIRDFY_AUTH_TOKEN), or an agent key for the agent\'s own credits, for credits balance'),
200
+ );
62
201
  const summary = formatCreditsBalanceSummary(response);
202
+ const note = creditsFreeQuotaNote(response);
63
203
  printEnvelope({
64
204
  success: true,
65
205
  code: 'OK',
66
206
  message: flags.json ? 'Credits balance loaded' : `Credits balance loaded: ${summary}`,
67
- data: response,
207
+ // Credits only: no USD price per credit (thirdfy-mcp getCreditsBalance).
208
+ data: { ...stripUsdFields(response), ...(note ? { note } : {}) },
68
209
  meta: {
69
210
  apiBase: ctx.apiBase,
211
+ ledger: agentLedger ? 'agent' : 'owner',
70
212
  capabilitiesVersion: capabilities?.contractVersion || null,
71
213
  ...(flags.json ? {} : { summary }),
72
214
  },
@@ -76,6 +218,7 @@ async function commandCreditsBalance(ctx, flags, capabilities) {
76
218
  return {
77
219
  commandCatalogsList,
78
220
  commandActions,
221
+ commandChains,
79
222
  commandCreditsBalance,
80
223
  };
81
224
  }