@thirdfy/agent-cli 0.4.2 → 0.5.0

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 (35) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.md +4 -6
  3. package/package.json +4 -2
  4. package/src/cli/help.mjs +2 -1
  5. package/src/cli/manifest.mjs +4 -4
  6. package/src/cli/options/index.mjs +2 -0
  7. package/src/cli/options/onboarding/credentials.mjs +0 -4
  8. package/src/cli/options/onboarding/oauthLogin.mjs +8 -0
  9. package/src/commands/account/credits.mjs +9 -7
  10. package/src/commands/discovery.mjs +6 -5
  11. package/src/commands/doctor.mjs +5 -5
  12. package/src/commands/execute.mjs +2 -11
  13. package/src/commands/onboarding/bootstrap.mjs +35 -0
  14. package/src/commands/onboarding/credentials.mjs +2 -25
  15. package/src/commands/onboarding/oauthLogin.mjs +284 -0
  16. package/src/core/chainRegistry.mjs +11 -0
  17. package/src/core/constants.mjs +0 -19
  18. package/src/core/context.mjs +16 -6
  19. package/src/core/errorEnvelope.mjs +6 -0
  20. package/src/core/http.mjs +47 -2
  21. package/src/core/oauth/authServer.mjs +192 -0
  22. package/src/core/oauth/pkce.mjs +17 -0
  23. package/src/core/oauth/session.mjs +117 -0
  24. package/src/lib/creditsUsdSummary.mjs +128 -29
  25. package/src/runtime/actions/selection.mjs +0 -1
  26. package/src/runtime/execution/payloads.mjs +6 -0
  27. package/src/runtime/execution/runners/createExecutionRunners.mjs +0 -3
  28. package/src/runtime/execution/runners/selfLane.mjs +6 -5
  29. package/src/runtime/handlers.mjs +1 -4
  30. package/src/runtime/main.mjs +0 -10
  31. package/src/runtime/owsSigning.mjs +17 -16
  32. package/src/runtime/providerHints/cex.mjs +1 -52
  33. package/src/runtime/bitfinexProbe.mjs +0 -121
  34. package/src/runtime/execution/runners/fallbackHints.mjs +0 -54
  35. package/src/runtime/onboardingHints.mjs +0 -54
package/CHANGELOG.md CHANGED
@@ -2,7 +2,69 @@
2
2
 
3
3
  All notable changes to `@thirdfy/agent-cli` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
- ## Unreleased
5
+ ## [0.5.0] - 2026-10-15
6
+
7
+ `thirdfy-agent login` signs in with OAuth in the browser (auth.thirdfy.com, the same sign-in the MCP uses), as one agent with read access, plus trade with `--trade`. A minor version because `login` without a token flag now starts that sign-in instead of failing, and `credits balance` / `pricing quote` drop their credit counts for USD. `login email`, token flags and agent API keys work as before.
8
+
9
+ ### Removed
10
+
11
+ - **The ignored Bitfinex flags of `credentials upsert`** (`--permission-profile`, `--verify-permissions`, `--skip-permissions-probe`, `--bitfinex-base-url`), as 0.4.3's warning announced (THI-86). Passing one is now an unknown-option error. Drop them from scripts.
12
+
13
+ ### Added
14
+
15
+ - **`thirdfy-agent login` signs in with OAuth** (THI-107, part of THI-104): auth.thirdfy.com, the same sign-in the MCP uses. With no token flag, `login` does this:
16
+ - opens the browser (`--no-browser` prints the URL);
17
+ - the owner picks one agent and approves read (plus trade with `--trade`);
18
+ - the browser returns to a one-time `127.0.0.1` listener;
19
+ - the CLI is a public client (registered once per server), and uses PKCE S256 and `resource` = the API.
20
+
21
+ How the sign-in is used:
22
+ - **Credential:** it's stored in `auth.oauth` and becomes the agent credential: after `--agent-api-key` and `THIRDFY_AGENT_API_KEY`, before config `agent.apiKey` and before stored owner auth for credits reads.
23
+ - **Agent:** its agent comes from the API (`GET /api/v1/agent/oauth/session`).
24
+ - **Requests:** they send `Authorization: Bearer`, refresh before expiry (rotation), and refresh and retry once after a 401.
25
+ - **Expiry:** a refused refresh is `OAUTH_SESSION_EXPIRED` (`nextAction: reauthenticate`).
26
+ - **`logout`** revokes the sign-in at the auth server.
27
+ - **`whoami`** shows `auth.oauth`.
28
+ - New next actions: `reauthenticate`, `reauthenticate_with_trade`.
29
+
30
+ Needs thirdfy/thirdfy-auth#15 (the API as a resource, token introspection) and the API change THI-106. Until both are live, `login` stops at `OAUTH_RESOURCE_NOT_SUPPORTED` and points to `login email`. `login email` and token flags work as before.
31
+
32
+ ### Changed
33
+
34
+ - **`credits balance` and `pricing quote` show balances in USD, never credits** (THI-123; same rules as thirdfy/thirdfy-mcp#279). Credits are Thirdfy's internal unit, and no price per action or request is stated.
35
+ - **Kept:** `availableUsd`, the free request counts (`freeDailyLimit`, `freeRequestsRemainingToday`, ...), `wouldBePaidBy` and `displayCurrency`.
36
+ - **Owner's fallback:** `ownerCreditsFallback` gets `availableUsd`, at the API's own `retailUsdPerCredit` from the same response, rounded to cents.
37
+ - **Dropped:** every credit count, per-credit rate and per-action price (`balanceCredits`, `balance`, `totalEarned`, `totalSpent`, `creditUsdValue`, `retailUsdPerCredit`, `quote.creditsCost`, `chargedUsd`, ...).
38
+ - **Text output:** without `--json`, `credits balance` reads `Balance loaded: $4.20 left; 8 of 20 free requests left today; then the owner's balance ($86.22) pays; then top up the balance.` The `note` says the same in USD. `pricing quote` reads `Action checked; would be paid by the owner's balance (owner_credits)`.
39
+ - **Unchanged:** the owner reports `credits transactions`, `credits usage`, `models list` and `models estimate` (credit ledger lines), as the MCP left its owner-token credits tools.
40
+ - **Owner ledger:** `totalAvailable` (a credit count) and `currency: 'credits'` are dropped too, and the owner route's free counts (`freeAllowance`, `freeRemainingToday`) are read, so owner auth also prints the free requests left.
41
+ - Before (0.4.3 and earlier) `credits balance` returned only `balanceCredits` and `ownerCreditsFallback.balanceCredits`, so a model answered in credits.
42
+ - **A proper "signed in" page after `thirdfy-agent login`.** The local listener used to answer the browser with a bare, unbranded page. Like Claude Code, GitHub CLI and Google Cloud CLI, it now:
43
+ - holds the browser until the CLI has its tokens and knows its agent;
44
+ - then sends it to the sign-in server's result page, `<issuer>/cli/signed-in`, with the agent, read or trade access, and the API (auth.thirdfy.com 0.5.1+);
45
+ - on a cancelled or failed sign-in, sends it to `<issuer>/cli/error`.
46
+
47
+ A sign-in server without those pages gets a built-in page in the same style.
48
+ - **A sign-in is used only on the API it was issued for**: an api-test sign-in is never sent to production, or the reverse. `whoami` shows `auth.oauth.resource` and `activeForThisApi`.
49
+
50
+ ## [0.4.3] - 2026-10-14
51
+
52
+ Bitfinex support is removed, ethers loads again for local signing and RPC checks, and an unreachable RPC no longer floods `--json` output. Older `credentials upsert` scripts keep working: the removed flags are accepted and ignored.
53
+
54
+ ### Removed
55
+
56
+ - **Bitfinex support.** It was unused and extra code to maintain. Removed:
57
+ - the Bitfinex static provider hint (`actions --provider bitfinex` no longer carries one; thirdfy-mcp removed its hint too, thirdfy/thirdfy-mcp#270);
58
+ - the `credentials upsert` permission probe, and the `bitfinex:funding-primary` / `bitfinex:spot-primary` credential-ref defaults (pass `--credential-ref` yourself);
59
+ - the Bitfinex onboarding hint (`data.onboardingHint` on error envelopes and on `preflight` / `run` results);
60
+ - `src/runtime/bitfinexProbe.mjs`, `src/runtime/onboardingHints.mjs`, `src/runtime/execution/runners/fallbackHints.mjs`, and the Bitfinex docs.
61
+
62
+ `credentials upsert` and `credentials status` stay for the other venues (Hummingbot connectors, Polymarket, Hyperliquid, Lighter, Avantis). The probe's flags (`--permission-profile`, `--verify-permissions`, `--skip-permissions-probe`, `--bitfinex-base-url`) are still accepted, ignored, and named in `meta.warnings`, so older scripts keep working; they go in the next minor version. The API still serves the Bitfinex catalog rows until it removes them.
63
+
64
+ ### Fixed
65
+
66
+ - **`loadEthers` loads ethers again.** `src/runtime/owsSigning.mjs` called `require` in an ES module, where it is undefined, and the `ReferenceError` was swallowed. So ethers never loaded, and these failed: `doctor self`'s `ethers-runtime` and `rpc-connectivity` checks, OWS sign-and-broadcast, the self lane's effect-check balance reads, and `wallet submit`. It now loads through `createRequire(import.meta.url)`.
67
+ - **RPC calls fail once instead of retrying forever.** Every RPC connection (`doctor self`, OWS broadcast, `wallet submit`, effect-check balance reads) is now pinned to its chain id (`pinnedRpcProvider` in `src/core/chainRegistry.mjs`). An unreachable RPC is one error, not an ethers retry line on stdout every second that breaks `--json` output and keeps the process running. `doctor self`'s `rpc-chain-id` check asks the RPC itself (`eth_chainId`), so a wrong RPC is still reported. Tests: `test/runtime/owsSigning.test.cjs`.
6
68
 
7
69
  ## [0.4.2] - 2026-10-14
8
70
 
package/README.md CHANGED
@@ -22,7 +22,6 @@ If you want the full developer docs, start here:
22
22
  - [Action inventory and compatibility](./docs/action-inventory-and-compatibility.md)
23
23
  - [Command reference](./docs/command-reference.md)
24
24
  - [DogeOS quickstart (repo)](./docs/dogeos-quickstart.md)
25
- - [Bitfinex CEX workflow (repo)](./docs/bitfinex-cex-workflow.md)
26
25
  - [Action onboarding standard](./docs/action-onboarding-standard.md)
27
26
  - [Provider docs governance model](./docs/provider-docs-governance.md)
28
27
  - [Skill integration RFC](./docs/skill-integration-rfc.md)
@@ -40,11 +39,11 @@ Run without global install:
40
39
  npx @thirdfy/agent-cli --help
41
40
  ```
42
41
 
43
- ## What's new in v0.4.2
42
+ ## What's new in v0.5.0
44
43
 
45
- - **One spelling per agent:** `wallet profile` and `readiness` report the agent key in lowercase, whether it came from `--agent-key` or from your config.
46
- - **`pricing quote` on an older API** (no agent quote route) names the agent only from `--agent-key`.
47
- - **Reorganized source** (no behavior change): source files moved into domain folders. If your code imports files from inside the package, see [CHANGELOG.md](./CHANGELOG.md) for the new paths; the `thirdfy-agent` command is unchanged.
44
+ - **Sign in with your Thirdfy account:** `thirdfy-agent login` opens your browser at auth.thirdfy.com. You pick one agent and approve read access, and trade access with `thirdfy-agent login --trade`. The CLI then acts as that agent, refreshes the sign-in by itself, and `thirdfy-agent logout` revokes it. Disconnect it any time in Connected apps at auth.thirdfy.com.
45
+ - **Same sign-in as the MCP:** the same consent page and the same Connected apps list as Claude, ChatGPT and Cursor.
46
+ - **Nothing else changes:** `login email`, `--auth-token`, `--owner-session-token` and agent API keys work as before, and an explicit `--agent-api-key` always wins over the sign-in.
48
47
  - Older versions: see [CHANGELOG.md](./CHANGELOG.md).
49
48
 
50
49
  ## Quick start
@@ -346,7 +345,6 @@ thirdfy-agent run --agent-api-key "$THIRDFY_AGENT_API_KEY" --action swap \
346
345
  Provider and venue guides are kept outside `README` so this page stays stable as integrations grow.
347
346
 
348
347
  - Provider index: [`docs/providers/index.md`](./docs/providers/index.md)
349
- - Bitfinex runbook: [`docs/providers/bitfinex.md`](./docs/providers/bitfinex.md)
350
348
  - DogeOS runbook: [`docs/providers/dogeos.md`](./docs/providers/dogeos.md)
351
349
 
352
350
  ## Command groups
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "scripts": {
25
25
  "test": "node --test test/*/*.test.cjs test/*/*/*.test.cjs",
26
- "lint": "node --check ./bin/thirdfy-agent.mjs && node --check ./src/core/*.mjs && node --check ./src/core/execution/*.mjs && node --check ./src/commands/*.mjs && node --check ./src/commands/venues/*.mjs && node --check ./src/commands/onboarding/*.mjs && node --check ./src/commands/account/*.mjs && node --check ./src/commands/signals/*.mjs && node --check ./src/cli/*.mjs && node --check ./src/cli/options/*.mjs && node --check ./src/cli/options/venues/*.mjs && node --check ./src/cli/options/onboarding/*.mjs && node --check ./src/cli/options/account/*.mjs && node --check ./src/runtime/*.mjs && node --check ./src/runtime/execution/*.mjs && node --check ./src/runtime/execution/runners/*.mjs && node --check ./src/runtime/actions/*.mjs && node --check ./src/runtime/validation/*.mjs && node --check ./src/runtime/certification/*.mjs && node --check ./src/runtime/providerHints/*.mjs && node --check ./src/runtime/risk/*.mjs",
26
+ "lint": "node --check ./bin/thirdfy-agent.mjs && node --check ./src/core/*.mjs && node --check ./src/core/execution/*.mjs && node --check ./src/core/oauth/*.mjs && node --check ./src/commands/*.mjs && node --check ./src/commands/venues/*.mjs && node --check ./src/commands/onboarding/*.mjs && node --check ./src/commands/account/*.mjs && node --check ./src/commands/signals/*.mjs && node --check ./src/cli/*.mjs && node --check ./src/cli/options/*.mjs && node --check ./src/cli/options/venues/*.mjs && node --check ./src/cli/options/onboarding/*.mjs && node --check ./src/cli/options/account/*.mjs && node --check ./src/runtime/*.mjs && node --check ./src/runtime/execution/*.mjs && node --check ./src/runtime/execution/runners/*.mjs && node --check ./src/runtime/actions/*.mjs && node --check ./src/runtime/validation/*.mjs && node --check ./src/runtime/certification/*.mjs && node --check ./src/runtime/providerHints/*.mjs && node --check ./src/runtime/risk/*.mjs",
27
27
  "validate:cli-manifest-handlers": "node ./scripts/validate-cli-manifest-handlers.mjs",
28
28
  "validate:runtime-boundaries": "node ./scripts/validate-runtime-boundaries.mjs",
29
29
  "validate:npm-pack": "node ./scripts/validate-npm-pack.mjs",
@@ -36,6 +36,8 @@
36
36
  "e2e:local-api:smoke": "node ./scripts/e2e/local-api-smoke.mjs",
37
37
  "e2e:base:simulate": "node ./scripts/e2e/base-simulate.mjs",
38
38
  "e2e:base:real": "node ./scripts/e2e/base-realtx.mjs",
39
+ "e2e:agent-wallet:simulate": "node ./scripts/e2e/agent-wallet/run.mjs",
40
+ "e2e:agent-wallet:real": "node ./scripts/e2e/agent-wallet/run.mjs --real",
39
41
  "e2e:delegation:discovery": "node ./scripts/e2e/delegation-discovery.mjs",
40
42
  "e2e:delegation:lifecycle": "node ./scripts/e2e/delegation-lifecycle.mjs",
41
43
  "e2e:delegation:commands": "node ./scripts/e2e/delegation-command-parity.mjs",
package/src/cli/help.mjs CHANGED
@@ -7,6 +7,7 @@ Core commands:
7
7
  thirdfy-agent profile use --profile <name> [--run-mode thirdfy|self|hybrid|agent_wallet] [--custody-mode managed|external] [--json]
8
8
  thirdfy-agent profile show [--json]
9
9
  thirdfy-agent config set --key <path> --value <value> [--json]
10
+ thirdfy-agent login [--trade] [--no-browser] [--issuer <url>] [--json] (OAuth in the browser: one agent)
10
11
  thirdfy-agent login [--auth-token <token> | --owner-session-token <token>] [--agent-api-key <key>] [--agent-key <key>] [--run-mode <mode>] [--custody-mode managed|external] [--wallet-address <addr>] [--json]
11
12
  thirdfy-agent login email <address> [--code <otp>] [--accept-terms] [--key-name <name>] [--agent-key <key>] [--not-interactive|--ni] [--json]
12
13
  thirdfy-agent logout [--all] [--clear-agent-key] [--json]
@@ -54,7 +55,7 @@ Core commands:
54
55
  thirdfy-agent wallet sign --agent-api-key <key> --action <id> --params <json> [--chain-id <id>] [--json]
55
56
  thirdfy-agent wallet submit --signed-tx-hex <hex> [--chain-id <id>] [--rpc-url <url>] [--json]
56
57
  thirdfy-agent agent run --agent-api-key <key> --action <id> --params <json> [--signer-method managed_wallet_server|byow|fanout_intent] [--strategy-intent single_wallet|fanout] [--run-mode <mode>] [--user-did <did>] [--json]
57
- thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--permission-profile funding|spot] [--credential-ref <ref>] [--verify-permissions] [--skip-permissions-probe] [--bitfinex-base-url <url>] [--json]
58
+ thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--credential-ref <ref>] [--json]
58
59
  thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
59
60
  thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
60
61
  thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
@@ -47,8 +47,8 @@ export const CLI_MANIFEST = [
47
47
  {
48
48
  path: ['login'],
49
49
  handler: 'commandLogin',
50
- options: ['auth', 'bootstrap'],
51
- description: 'Login with auth token, owner session, or agent API key',
50
+ options: ['auth', 'bootstrap', 'oauthLogin'],
51
+ description: 'Sign in with OAuth in the browser (one agent; --trade for writes), or with an auth token, owner session or agent API key',
52
52
  },
53
53
  {
54
54
  path: ['login', 'email'],
@@ -212,7 +212,7 @@ export const CLI_MANIFEST = [
212
212
  handler: 'commandCreditsBalance',
213
213
  tier: 'online',
214
214
  options: ['auth'],
215
- description: 'Credits balance',
215
+ description: 'Balance in USD, free requests today and who pays next',
216
216
  },
217
217
  {
218
218
  path: ['credits', 'buy'],
@@ -254,7 +254,7 @@ export const CLI_MANIFEST = [
254
254
  handler: 'commandPricingQuote',
255
255
  tier: 'online',
256
256
  options: ['auth', 'execution'],
257
- description: 'Quote action-tier credits for a catalog action',
257
+ description: 'Check who would pay for a catalog action now (never a price)',
258
258
  },
259
259
  {
260
260
  path: ['chat', 'send'],
@@ -6,6 +6,7 @@ import { attachTrackingOptions } from './tracking.mjs';
6
6
  import { attachWalletOptions } from './account/wallet.mjs';
7
7
  import { attachBootstrapOptions, attachOnboardingHelpOptions } from './onboarding/bootstrap.mjs';
8
8
  import { attachCredentialsOptions } from './onboarding/credentials.mjs';
9
+ import { attachOAuthLoginOptions } from './onboarding/oauthLogin.mjs';
9
10
  import { attachAnalyticsOptions } from './analytics.mjs';
10
11
  import { attachPolymarketOptions } from './venues/polymarket.mjs';
11
12
  import { attachHummingbotOptions } from './venues/hummingbot.mjs';
@@ -27,6 +28,7 @@ const BUNDLES = {
27
28
  tracking: attachTrackingOptions,
28
29
  wallet: attachWalletOptions,
29
30
  bootstrap: attachBootstrapOptions,
31
+ oauthLogin: attachOAuthLoginOptions,
30
32
  onboardingHelp: attachOnboardingHelpOptions,
31
33
  credentials: attachCredentialsOptions,
32
34
  analytics: attachAnalyticsOptions,
@@ -3,12 +3,8 @@ export function attachCredentialsOptions(cmd) {
3
3
  .option('--venue <id>', 'credential venue')
4
4
  .option('--api-key <key>', 'exchange API key')
5
5
  .option('--api-secret <secret>', 'exchange API secret')
6
- .option('--permission-profile <profile>', 'funding|spot')
7
6
  .option('--credential-ref <ref>', 'credential reference')
8
7
  .option('--credential-type <type>', 'credential type')
9
- .option('--verify-permissions', 'verify exchange permissions')
10
- .option('--skip-permissions-probe', 'skip permissions probe')
11
- .option('--bitfinex-base-url <url>', 'Bitfinex API base URL')
12
8
  .option('--passphrase <value>', 'exchange passphrase')
13
9
  .option('--metadata <json>', 'credential metadata JSON')
14
10
  .option('--policy <json>', 'credential policy JSON');
@@ -0,0 +1,8 @@
1
+ /** `thirdfy-agent login` with OAuth (auth.thirdfy.com), the default when no token flag is passed. */
2
+ export function attachOAuthLoginOptions(cmd) {
3
+ return cmd
4
+ .option('--trade', 'also ask for trade access (writes that move funds); read-only otherwise')
5
+ .option('--no-browser', 'print the sign-in URL instead of opening the browser')
6
+ .option('--issuer <url>', 'sign-in server (default: paired with --api-base, https://auth.thirdfy.com)')
7
+ .option('--login-timeout <seconds>', 'how long to wait for the browser sign-in (default 300)');
8
+ }
@@ -6,7 +6,8 @@ import { toAvantisSpelling } from '../../core/venueBrand.mjs';
6
6
  import {
7
7
  formatCreditsEstimateSummary,
8
8
  formatCreditsModelsSummary,
9
- stripUsdFields,
9
+ describeWouldBePaidBy,
10
+ toUsdFirst,
10
11
  } from '../../lib/creditsUsdSummary.mjs';
11
12
 
12
13
  function resolveToolCallsEstimate(flags) {
@@ -146,12 +147,13 @@ export function createCreditsCommands({ printEnvelope, getAuthToken, buildOwnerA
146
147
  }
147
148
 
148
149
  /**
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
150
+ * `pricing quote --action <id>`: whether one catalog action can run now and who pays for it:
151
+ * `wouldBePaidBy` (agent_credits | free_request | owner_credits | none) when the API can read the balances
151
152
  * (thirdfy-api-v2#887). The ledger follows `resolveCreditsLedger`. Agent: `POST /agent/credits/quote`
152
153
  * with the agent's key. Owner (session token or auth token): `/credits/pricing/quote`, for one of the
153
154
  * 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 --agent-key names the key's agent. Credits only.
155
+ * through the owner route when owner auth is set and --agent-key names the key's agent. Never a price:
156
+ * the quote's credit cost and USD figures are dropped (thirdfy-mcp quoteActionPricing, thirdfy-mcp#279).
155
157
  */
156
158
  async function commandPricingQuote(ctx, flags) {
157
159
  const action = requireFlag(flags, 'action', 'Missing --action for pricing quote');
@@ -210,9 +212,9 @@ export function createCreditsCommands({ printEnvelope, getAuthToken, buildOwnerA
210
212
  success: true,
211
213
  code: 'OK',
212
214
  // 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),
215
+ message: response?.wouldBePaidBy ? `Action checked; would be paid by ${describeWouldBePaidBy(response.wouldBePaidBy)}` : 'Action checked',
216
+ // No price per action or request: no credit cost, rate or USD figure (thirdfy-mcp quoteActionPricing).
217
+ data: toUsdFirst(response),
216
218
  meta: {
217
219
  apiBase: ctx.apiBase,
218
220
  action,
@@ -2,7 +2,7 @@ import { requireFlag, parseJsonFlag } from '../core/args.mjs';
2
2
  import { apiGet, apiPost } from '../core/http.mjs';
3
3
  import { extractActions, getCachedActionsCatalogSnapshot, resolveCreditsLedger } from '../core/context.mjs';
4
4
  import { getEffectiveRunMode } from '../core/runMode.mjs';
5
- import { creditsFreeQuotaNote, formatCreditsBalanceSummary, stripUsdFields } from '../lib/creditsUsdSummary.mjs';
5
+ import { creditsFreeQuotaNote, formatCreditsBalanceSummary, toUsdFirst } from '../lib/creditsUsdSummary.mjs';
6
6
  import { filterActionsByChain } from '../runtime/actions/chainFilter.mjs';
7
7
  import { pageActionList, readActionListPaging } from '../runtime/actions/actionList.mjs';
8
8
  import {
@@ -186,7 +186,8 @@ async function commandChains(ctx, flags, capabilities) {
186
186
  * `credits balance`. Owner auth reads the owner's balance (`/credits/balance`). An explicit
187
187
  * --agent-api-key, or no owner auth but an agent key in env or config, reads the agent's own ledger
188
188
  * (`/agent/credits/balance`: its credits, free requests today and the owner-credits fallback), as
189
- * thirdfy-mcp getCreditsBalance. Credits only, never dollars.
189
+ * thirdfy-mcp getCreditsBalance. In USD, never credits: `availableUsd` (the owner's fallback too, at the
190
+ * API's `retailUsdPerCredit`), free requests and who pays; no credit count, rate or price (thirdfy-mcp#279).
190
191
  */
191
192
  async function commandCreditsBalance(ctx, flags, capabilities) {
192
193
  const { ledger, agentApiKey } = resolveCreditsLedger(flags);
@@ -203,9 +204,9 @@ async function commandCreditsBalance(ctx, flags, capabilities) {
203
204
  printEnvelope({
204
205
  success: true,
205
206
  code: 'OK',
206
- message: flags.json ? 'Credits balance loaded' : `Credits balance loaded: ${summary}`,
207
- // Credits only: no USD price per credit (thirdfy-mcp getCreditsBalance).
208
- data: { ...stripUsdFields(response), ...(note ? { note } : {}) },
207
+ message: flags.json ? 'Balance loaded' : `Balance loaded: ${summary}`,
208
+ // USD-first: balances in USD, no credit count, per-credit rate or price (thirdfy-mcp getCreditsBalance).
209
+ data: { ...toUsdFirst(response), ...(note ? { note } : {}) },
209
210
  meta: {
210
211
  apiBase: ctx.apiBase,
211
212
  ledger: agentLedger ? 'agent' : 'owner',
@@ -3,7 +3,7 @@ import { requireFlag } from '../core/args.mjs';
3
3
  import { apiGet, requestWithRouteFallback } from '../core/http.mjs';
4
4
  import { loadProfileConfig, getProfileConfigPath, getCachedActionsCatalog } from '../core/context.mjs';
5
5
  import { DEFAULT_API_BASE, PROFILE_DEFAULTS } from '../core/constants.mjs';
6
- import { describeChainGas, parseChainIdFlag, resolveChainRpcUrl } from '../core/chainRegistry.mjs';
6
+ import { DEFAULT_CHAIN_ID, describeChainGas, parseChainIdFlag, pinnedRpcProvider, resolveChainRpcUrl } from '../core/chainRegistry.mjs';
7
7
  import { exitAfterOutput } from '../core/envelope.mjs';
8
8
  import { normalizeRunMode, normalizeManagedRunMode } from '../core/runMode.mjs';
9
9
  import { buildExecutionCertificationReport } from '../runtime/certification/executionCertify.mjs';
@@ -63,13 +63,13 @@ async function commandDoctorSelf(ctx, flags) {
63
63
  try {
64
64
  if (!rpcUrl) throw new Error(`no known RPC for chain ${flags.chainId}; pass --rpc-url <url>`);
65
65
  const ethers = await loadEthers();
66
- const provider = new ethers.JsonRpcProvider(rpcUrl);
66
+ const expectedChainId = parseChainIdFlag(flags.chainId);
67
+ const provider = pinnedRpcProvider(ethers, { rpcUrl, chainId: expectedChainId || DEFAULT_CHAIN_ID });
67
68
  const block = await provider.getBlockNumber();
68
69
  checks.push({ name: 'rpc-connectivity', ok: Number.isFinite(block), detail: `latestBlock=${block}` });
69
- const expectedChainId = parseChainIdFlag(flags.chainId);
70
70
  if (expectedChainId) {
71
- const network = await provider.getNetwork();
72
- const rpcChainId = Number(network.chainId);
71
+ // The provider's network is pinned, so ask the RPC itself which chain it serves.
72
+ const rpcChainId = Number(await provider.send('eth_chainId', []));
73
73
  checks.push({
74
74
  name: 'rpc-chain-id',
75
75
  ok: rpcChainId === expectedChainId,
@@ -33,7 +33,6 @@ export function createExecuteCommands(deps) {
33
33
  runPreflightByMode,
34
34
  runExecutionByMode,
35
35
  resolveManagedExecutionPreflight,
36
- applyExecutionFallbackHints,
37
36
  performSelfBroadcast,
38
37
  executeManagedWalletRun,
39
38
  executeSelfRun,
@@ -57,11 +56,7 @@ async function commandPreflight(ctx, flags, capabilities) {
57
56
  || (await applyLighterAgentWalletAddressDefault(ctx, flags, resolved, runMode))
58
57
  || (await applyAvantisAgentWalletAddressDefault(ctx, flags, resolved, runMode));
59
58
  const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved, { hybridWalletMode, managedPreflight });
60
- const normalized = applyExecutionFallbackHints(backendResult.normalized, {
61
- flags,
62
- runMode,
63
- resolvedAction: resolved.resolvedAction,
64
- });
59
+ const normalized = backendResult.normalized;
65
60
  const data = flags.__polymarketSetupGuide
66
61
  ? {
67
62
  ...normalized,
@@ -195,11 +190,7 @@ async function commandRun(ctx, flags, capabilities) {
195
190
  hybridWalletMode,
196
191
  managedPreflight,
197
192
  });
198
- const normalized = applyExecutionFallbackHints(backendResult.normalized, {
199
- flags,
200
- runMode,
201
- resolvedAction: resolved.resolvedAction,
202
- });
193
+ const normalized = backendResult.normalized;
203
194
  if (runMode === 'self' && parseBooleanFlag(flags.broadcast, false) && normalized?.success) {
204
195
  const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, normalized);
205
196
  printEnvelope({
@@ -17,6 +17,9 @@ import {
17
17
  } from '../../core/context.mjs';
18
18
  import { apiGet, apiPost, requestWithRouteFallback } from '../../core/http.mjs';
19
19
  import { isEvmAddress, isSolanaAddress } from '../../core/chainRegistry.mjs';
20
+ import { commandOAuthLogin } from './oauthLogin.mjs';
21
+ import { readOAuthSession } from '../../core/oauth/session.mjs';
22
+ import { revokeToken } from '../../core/oauth/authServer.mjs';
20
23
  import { createRequire } from 'module';
21
24
 
22
25
  const require = createRequire(import.meta.url);
@@ -35,6 +38,7 @@ export function createBootstrapCommands(deps) {
35
38
  verifyOwnerWalletChallenge,
36
39
  resolveEffectiveUserDid,
37
40
  negotiateCapabilities,
41
+ CLI_VERSION,
38
42
  commandAgentRegister,
39
43
  } = deps;
40
44
 
@@ -232,6 +236,19 @@ async function commandWhoami(ctx, flags) {
232
236
  hasOwnerSessionToken: Boolean(auth.ownerSessionToken),
233
237
  ownerSessionToken: maskSecret(auth.ownerSessionToken),
234
238
  ownerSessionExpiresAt: auth.ownerSessionExpiresAt || null,
239
+ oauth: readOAuthSession(config)
240
+ ? {
241
+ issuer: auth.oauth.issuer || null,
242
+ // The API this sign-in is for; commands use it only on that API (--api-base).
243
+ resource: auth.oauth.resource || null,
244
+ activeForThisApi: String(auth.oauth.resource || '').replace(/\/+$/, '') === String(ctx.apiBase || '').replace(/\/+$/, ''),
245
+ agentKey: auth.oauth.agentKey || null,
246
+ scope: auth.oauth.scope || '',
247
+ canTrade: auth.oauth.canTrade === true,
248
+ expiresAt: auth.oauth.expiresAt || null,
249
+ accessToken: maskSecret(auth.oauth.accessToken),
250
+ }
251
+ : null,
235
252
  },
236
253
  agent: {
237
254
  hasAgentApiKey: Boolean(agent.apiKey),
@@ -272,6 +289,12 @@ async function commandLogin(ctx, flags) {
272
289
  );
273
290
  let loginSource = 'token';
274
291
 
292
+ // No token and no wallet-signature inputs: sign in with OAuth in the browser (THI-104).
293
+ const hasChallengeInputs = Boolean(String(flags.challengeId || '').trim() && String(flags.signature || '').trim());
294
+ if (!authToken && !ownerSessionToken && !hasChallengeInputs) {
295
+ return commandOAuthLogin(ctx, flags, { printEnvelope, version: CLI_VERSION });
296
+ }
297
+
275
298
  if (!authToken && !ownerSessionToken) {
276
299
  const challengeId = String(flags.challengeId || '').trim();
277
300
  const signature = String(flags.signature || '').trim();
@@ -611,6 +634,17 @@ async function commandLogout(ctx, flags) {
611
634
  }
612
635
  }
613
636
 
637
+ // An OAuth sign-in is revoked at the auth server (RFC 7009), so its tokens stop working
638
+ // everywhere, not only on this machine. Best effort: local credentials are cleared either way.
639
+ const oauthSession = readOAuthSession(current);
640
+ let oauthRevoked = null;
641
+ if (oauthSession) {
642
+ const revoke = (token) =>
643
+ revokeToken({ revocationEndpoint: oauthSession.revocationEndpoint, clientId: oauthSession.clientId, token }, ctx.timeoutMs || 30_000);
644
+ const [refresh, access] = await Promise.all([revoke(oauthSession.refreshToken), revoke(oauthSession.accessToken)]);
645
+ oauthRevoked = Boolean(refresh || access);
646
+ }
647
+
614
648
  const next = { ...current };
615
649
  delete next.auth;
616
650
  if (flags.clearAgentKey) {
@@ -626,6 +660,7 @@ async function commandLogout(ctx, flags) {
626
660
  revokeAttempted,
627
661
  revokeSuccess,
628
662
  revokeError,
663
+ ...(oauthSession ? { oauthRevoked, oauthAgentKey: oauthSession.agentKey || null } : {}),
629
664
  configPath: getProfileConfigPath(),
630
665
  },
631
666
  meta: { apiBase: ctx.apiBase },
@@ -2,47 +2,25 @@ import { createRequire } from 'module';
2
2
  import { requireFlag, parseJsonFlag } from '../../core/args.mjs';
3
3
  import { requestWithRouteFallback } from '../../core/http.mjs';
4
4
  import { createCliError } from '../../core/envelope.mjs';
5
- import { BITFINEX_PERMISSION_PROFILES, DEFAULT_BITFINEX_BASE_URL } from '../../core/constants.mjs';
6
- import { parseBooleanFlag } from '../../core/runMode.mjs';
7
5
 
8
6
  const require = createRequire(import.meta.url);
9
7
  const { normalizeCredentialStatus } = require('../../utils/cli/delegationNormalization.cjs');
10
- export function createCredentialsCommands({ printEnvelope, getAuthToken, buildOwnerAuthHeaders, probeBitfinexPermissions }) {
8
+ export function createCredentialsCommands({ printEnvelope, getAuthToken, buildOwnerAuthHeaders }) {
11
9
  async function commandCredentialsUpsert(ctx, flags, capabilities) {
12
10
  const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials upsert');
13
11
  const venue = requireFlag(flags, 'venue', 'Missing --venue');
14
- const permissionProfile = String(flags.permissionProfile || 'funding').trim().toLowerCase();
15
- const bitfinexProfile =
16
- String(venue).trim().toLowerCase() === 'bitfinex'
17
- ? BITFINEX_PERMISSION_PROFILES[permissionProfile] || BITFINEX_PERMISSION_PROFILES.funding
18
- : null;
19
12
  const payload = {
20
13
  venue,
21
14
  apiKey: requireFlag(flags, 'apiKey', 'Missing --api-key'),
22
15
  apiSecret: requireFlag(flags, 'apiSecret', 'Missing --api-secret'),
23
16
  credentialType: String(flags.credentialType || 'api_key'),
24
- credentialRef: String(flags.credentialRef || bitfinexProfile?.credentialRef || ''),
17
+ credentialRef: String(flags.credentialRef || ''),
25
18
  };
26
19
  if (flags.passphrase) payload.passphrase = String(flags.passphrase);
27
20
  if (flags.metadata) payload.metadata = parseJsonFlag(flags, 'metadata');
28
21
  if (flags.policy) payload.policy = parseJsonFlag(flags, 'policy');
29
22
  if (flags.agentKey) payload.agentKey = String(flags.agentKey).trim();
30
23
  if (flags.chainId) payload.chainId = Number(flags.chainId);
31
- const probeEnabled =
32
- String(venue).trim().toLowerCase() === 'bitfinex' &&
33
- !parseBooleanFlag(flags.skipPermissionsProbe, false) &&
34
- parseBooleanFlag(flags.verifyPermissions, true);
35
- let permissionProbe = null;
36
- if (probeEnabled) {
37
- permissionProbe = await probeBitfinexPermissions({
38
- apiKey: payload.apiKey,
39
- apiSecret: payload.apiSecret,
40
- permissionProfile: bitfinexProfile?.id || 'funding',
41
- timeoutMs: ctx.timeoutMs,
42
- baseUrl: String(flags.bitfinexBaseUrl || process.env.BITFINEX_BASE_URL || DEFAULT_BITFINEX_BASE_URL),
43
- });
44
- }
45
-
46
24
  const response = await requestWithRouteFallback(ctx, {
47
25
  method: 'POST',
48
26
  routes: ['/api/v1/agent/delegation/credentials/upsert', '/api/v1/agent/agent/delegation/credentials/upsert'],
@@ -58,7 +36,6 @@ async function commandCredentialsUpsert(ctx, flags, capabilities) {
58
36
  data: response,
59
37
  meta: {
60
38
  apiBase: ctx.apiBase,
61
- permissionProbe,
62
39
  capabilitiesVersion: capabilities?.contractVersion || null,
63
40
  },
64
41
  });