@thirdfy/agent-cli 0.2.76 → 0.2.78
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/CHANGELOG.md +43 -0
- package/README.md +5 -4
- package/package.json +4 -4
- package/src/cli/manifest.mjs +4 -4
- package/src/cli/options/bootstrap.mjs +7 -1
- package/src/cli/program.mjs +1 -0
- package/src/cli/register.mjs +3 -0
- package/src/commands/avantis.mjs +7 -4
- package/src/commands/credits.mjs +3 -1
- package/src/commands/telemetry.mjs +5 -1
- package/src/commands/venue.mjs +5 -2
- package/src/core/actionTimeouts.mjs +4 -5
- package/src/core/context.mjs +5 -5
- package/src/core/hostedRuntimeIdempotency.mjs +28 -0
- package/src/core/http.mjs +58 -7
- package/src/core/venueBrand.mjs +76 -0
- package/src/runtime/actions/selection.mjs +66 -19
- package/src/runtime/certification/executionCertify.mjs +3 -1
- package/src/runtime/certification/venueActionSchemas.mjs +5 -3
- package/src/runtime/execution/amounts.mjs +63 -1
- package/src/runtime/execution/lanes.mjs +4 -2
- package/src/runtime/execution/payloads.mjs +3 -4
- package/src/runtime/execution/runners.mjs +94 -5
- package/src/runtime/main.mjs +3 -1
- package/src/runtime/providerHints.mjs +92 -65
- package/src/runtime/validation/actionParamsGate.mjs +56 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
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
|
+
## [0.2.78] - 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Avantis is now Veranta.** The Base perps venue rebranded (ticker AVNT unchanged). Help, errors, provider hints and docs say Veranta. Provider hints for `avantis` / `avantis-vault` list Veranta action names (`place_veranta_perps_order`, `deposit_veranta_vault`, ...) and carry `displayName: "Veranta"` and `providerAliases`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Veranta names work against every API deployment.** A Veranta action name (`*_veranta_*`) resolves to the catalog row the API in hand has: the Veranta row on an API with Veranta names, the Avantis row on one without. Avantis names keep working, including against a catalog that lists them only as aliases of Veranta rows. With no catalog to check, a Veranta name is sent in the Avantis spelling, which every deployment accepts. `pricing quote` does the same.
|
|
14
|
+
- **`veranta` / `veranta-vault` provider aliases.** `--provider veranta` lists the same actions as `--provider avantis` (and `veranta-vault` as `avantis-vault`). `venue readiness|setup|fund veranta` calls the API with the `avantis` venue id. `avantis` is still accepted everywhere.
|
|
15
|
+
- **Hardcoded venue lists recognise both spellings:** the offchain-venue lane guard (`OFFCHAIN_VENUE_ORDER_SELF_UNSUPPORTED`), the 180s long timeout for onboarding and market open/close, the `agent_wallet` `traderAddress` default, the `complete_*_onboarding` `estimatedAmountUsd` default, and `certify` venue fixtures (a live Veranta row replaces the Avantis fixture; the schema digest is spelling-independent).
|
|
16
|
+
- Error codes and internal identifiers are unchanged (`avantis_delegate`, `AVANTIS_*` setup blockers, provider ids `avantis` / `avantis-vault`). Pin: `test/veranta-rebrand.test.cjs`.
|
|
17
|
+
|
|
18
|
+
## [0.2.77] - 2026-09-24
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Dependencies on current releases:**
|
|
23
|
+
- `commander` 14.0.3: 12 → 14 is two majors.
|
|
24
|
+
- `ethers` 6.17.0 and `@open-wallet-standard/core` 1.4.2.
|
|
25
|
+
- Commander 15 is held back because it needs Node ≥22.12. agent-cli still supports Node ≥20, and Hermes runtimes install `@latest` on the Node their image already has.
|
|
26
|
+
- **Extra positional arguments are still accepted.** Commander 13+ rejects them by default; the CLI opts back in (`allowExcessArguments`), so a command like `intent-status <id>` behaves as in 0.2.76. Pin: `test/commander-routing.test.cjs`.
|
|
27
|
+
- **`-ni` still works.** Commander 13+ rejects two-letter short flags. The option is `--not-interactive`, and a bare `-ni` is rewritten to it before parsing. Pin: `test/commander-routing.test.cjs`.
|
|
28
|
+
- **`venue readiness|setup|fund` declare `<venue>`.** The manifest passed `true` as the argument name, which commander 12 accepted as one required positional. The behavior is unchanged: `thirdfy-agent venue readiness avantis`.
|
|
29
|
+
- **CI and release:**
|
|
30
|
+
- CI tests Node 20, 22 and 24.
|
|
31
|
+
- Release and live e2e run on Node 24.
|
|
32
|
+
- `actions/checkout@v7`, `actions/setup-node@v7`.
|
|
33
|
+
- Dependabot with weekly updates.
|
|
34
|
+
- **agent_wallet reads make one API call.** A read in `agent_wallet` mode skips the `/execution-wallet` preflight, because the API resolves the execution wallet itself. The preflight still runs when the read needs its answer: `--wallet-address` set to a wallet other than the agent key, chain 4663 (Robinhood delegated lane), or a Hyperliquid/Lighter/Avantis address default. With `--wallet-address` equal to the agent key, the wallet the API reports is checked after the read (same `EXECUTION_WALLET_MISMATCH` rule). The result shows `executionWalletPreflight: { skipped: "read_fast_path" }`. Matches `thirdfy-mcp` `walletExecute`.
|
|
35
|
+
- **`WRITE_OUTCOME_UNKNOWN` advice matches `thirdfy-mcp`.** The error names the action and the idempotency key: re-read state first; if the write landed, do not resend; if unsure and retrying, reuse the exact same key; if the re-read shows it did not land, send it with a new key. The API holds an unknown-outcome claim for 24 h.
|
|
36
|
+
- **Unknown actions.** An action that is not in the catalog, or not allowed for the agent key, still fails with `error: PARAMS_SCHEMA_INVALID`, now with `blockedReason: UNKNOWN_ACTION`, `resolvedAction: null`, and `suggestions` (closest catalog actions). Same shape as `thirdfy-mcp` 0.1.6.
|
|
37
|
+
- **Write timeout floor (pin only).** The CLI picks write timeouts from its own read/write classifier, not the catalog `behavior.latencyClass`, so a write never gets the read timeout. `test/mcp-api-parity.test.cjs` pins it.
|
|
38
|
+
|
|
39
|
+
### Security
|
|
40
|
+
|
|
41
|
+
- **The agent API key never goes in a URL.** The policy-aware actions listing (`thirdfy-agent actions --agent-api-key …`, and action resolution before `run` / `preflight` / `wallet execute`) called `GET /api/v1/agent/actions?agentApiKey=…`. It now sends the key in the `x-agent-api-key` header, which every agent route reads since Thirdfy API 3.15; a key in the query string gets `Deprecation` headers. Pin: `test/mcp-api-parity.test.cjs`.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **Replayed writes are labeled.** When the API answers a value-moving write from an earlier identical one (`Idempotent-Replayed: true` header or `idempotentReplay: true` in the body), the result carries `idempotentReplay: true` and a note: nothing new was executed; send a new `executionIdempotencyKey` (`idempotencyKey` on execute-intent) to place another. Both come from `--idempotency-key`. Matches `thirdfy-mcp` `idempotentReplayFields`.
|
|
46
|
+
- **`deposit_earn_position` amount aliases.** `amountHuman` and `amountInHuman` are accepted in place of `amount` (a decimal token-unit string). Two amounts, or raw base units (`amountInRaw`, `amountIn`), fail with `EARN_AMOUNT_*` before any request. **New refusal:** earn deposits that sent `amountIn` / `amountInRaw` used to reach the API, which reads `amount` as token units; they now fail locally. `execute_optimal_swap` takes the same `amountInRaw` / `amountInHuman` + `tokenInDecimals` contract as `swap`. Matches the `thirdfy-mcp` params gate (`earn_deposit_decimal_amount`, `evm_erc20_amount_in`).
|
|
47
|
+
|
|
5
48
|
## [0.2.76] - 2026-09-23
|
|
6
49
|
|
|
7
50
|
### Fixed
|
package/README.md
CHANGED
|
@@ -40,10 +40,11 @@ Run without global install:
|
|
|
40
40
|
npx @thirdfy/agent-cli --help
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
## What's new in v0.2.
|
|
43
|
+
## What's new in v0.2.78
|
|
44
44
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
45
|
+
- Avantis, the Base perps venue, is now **Veranta**. Use `--provider veranta` / `veranta-vault` and Veranta action names such as `place_veranta_perps_order` and `get_veranta_positions`.
|
|
46
|
+
- Avantis names (`--provider avantis`, `place_avantis_perps_order`, `venue readiness avantis`) keep working. A Veranta name finds the matching action whether the Thirdfy API catalog lists Veranta or Avantis names.
|
|
47
|
+
- `venue readiness|setup|fund veranta` works; `avantis` is still accepted.
|
|
47
48
|
- See [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases for older versions.
|
|
48
49
|
|
|
49
50
|
## Quick start
|
|
@@ -323,7 +324,7 @@ Provider and venue guides are kept outside `README` so this page stays stable as
|
|
|
323
324
|
- Discovery: `catalogs list`, `actions` (optional `--provider`, `--chain-id`; see [`docs/command-reference.md`](./docs/command-reference.md) for trading vs earn vs prediction providers and case-insensitive matching)
|
|
324
325
|
- Execution: `preflight`, `run`, `intent-status`, `jeff preflight`, `jeff trade`, `jeff status`
|
|
325
326
|
- Polymarket: `polymarket status`, `polymarket setup`, `polymarket dry-run`
|
|
326
|
-
- Venue tooling: `venue readiness`, `venue setup`, `venue fund` (`polymarket`, `hyperliquid`, `lighter`, `
|
|
327
|
+
- Venue tooling: `venue readiness`, `venue setup`, `venue fund` (`polymarket`, `hyperliquid`, `lighter`, `veranta`)
|
|
327
328
|
- Hummingbot: `hummingbot status`, `connectors`, `executor-schema`, `executor-create`, `executor-stop`, `gateway-networks`, `gateway-connectors`
|
|
328
329
|
- Condor Pattern A: `condor policy-status`, `condor preflight`
|
|
329
330
|
- Managed signer execution: `wallet execute`, `wallet sign`, `wallet submit`, `agent run`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thirdfy/agent-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.78",
|
|
4
4
|
"description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
},
|
|
72
72
|
"homepage": "https://github.com/thirdfy/agent-cli#readme",
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@open-wallet-standard/core": "^1.
|
|
75
|
-
"commander": "^
|
|
76
|
-
"ethers": "^6.
|
|
74
|
+
"@open-wallet-standard/core": "^1.4.2",
|
|
75
|
+
"commander": "^14.0.3",
|
|
76
|
+
"ethers": "^6.17.0"
|
|
77
77
|
}
|
|
78
78
|
}
|
package/src/cli/manifest.mjs
CHANGED
|
@@ -103,8 +103,8 @@ export const CLI_MANIFEST = [
|
|
|
103
103
|
path: ['venue', 'readiness'],
|
|
104
104
|
handler: 'commandVenueReadiness',
|
|
105
105
|
options: ['auth', 'venue'],
|
|
106
|
-
description: 'Normalized venue readiness (polymarket, hyperliquid, lighter,
|
|
107
|
-
args: { query:
|
|
106
|
+
description: 'Normalized venue readiness (polymarket, hyperliquid, lighter, veranta)',
|
|
107
|
+
args: { query: '<venue>' },
|
|
108
108
|
tier: 'online',
|
|
109
109
|
},
|
|
110
110
|
{
|
|
@@ -112,7 +112,7 @@ export const CLI_MANIFEST = [
|
|
|
112
112
|
handler: 'commandVenueSetup',
|
|
113
113
|
options: ['auth', 'venue'],
|
|
114
114
|
description: 'Prepare or complete venue onboarding via shared API contract',
|
|
115
|
-
args: { query:
|
|
115
|
+
args: { query: '<venue>' },
|
|
116
116
|
tier: 'online',
|
|
117
117
|
},
|
|
118
118
|
{
|
|
@@ -120,7 +120,7 @@ export const CLI_MANIFEST = [
|
|
|
120
120
|
handler: 'commandVenueFund',
|
|
121
121
|
options: ['auth', 'venue'],
|
|
122
122
|
description: 'Return venue funding plan (does not move funds implicitly)',
|
|
123
|
-
args: { query:
|
|
123
|
+
args: { query: '<venue>' },
|
|
124
124
|
tier: 'online',
|
|
125
125
|
},
|
|
126
126
|
{
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** Commander 13+ rejects multi-character short flags; keep the historical `-ni` spelling working. */
|
|
2
|
+
export function normalizeLegacyShortFlags(argv) {
|
|
3
|
+
const end = argv.indexOf('--');
|
|
4
|
+
return argv.map((arg, index) => (arg === '-ni' && (end === -1 || index < end) ? '--not-interactive' : arg));
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
export function attachBootstrapOptions(cmd) {
|
|
2
8
|
return cmd
|
|
3
9
|
.option('--proof-type <type>', 'session_token|wallet_signature')
|
|
@@ -10,7 +16,7 @@ export function attachBootstrapOptions(cmd) {
|
|
|
10
16
|
.option('--otp <otp>', 'email OTP alias')
|
|
11
17
|
.option('--accept-terms', 'accept terms for email login')
|
|
12
18
|
.option('--key-name <name>', 'Privy key name')
|
|
13
|
-
.option('
|
|
19
|
+
.option('--not-interactive', 'fail fast when required login input is missing (alias: -ni)')
|
|
14
20
|
.option('--no-bootstrap-agent', 'skip agent bootstrap on email login')
|
|
15
21
|
.option('--rotate-agent-key', 'rotate agent key')
|
|
16
22
|
.option('--login-attempt-id <id>', 'email login attempt id')
|
package/src/cli/program.mjs
CHANGED
|
@@ -3,3 +3,4 @@ import { buildCliProgram } from './register.mjs';
|
|
|
3
3
|
export { buildCliProgram };
|
|
4
4
|
export { collectRootFlags, collectLegacyFlags } from './flags.mjs';
|
|
5
5
|
export { configureCommanderErrors, handleRootOnlyFlows, collectVersionFlagsFromArgv } from './errors.mjs';
|
|
6
|
+
export { normalizeLegacyShortFlags } from './options/bootstrap.mjs';
|
package/src/cli/register.mjs
CHANGED
|
@@ -107,6 +107,9 @@ function registerManifestEntry(program, entry, nodeByPath, deps) {
|
|
|
107
107
|
export function buildCliProgram(deps) {
|
|
108
108
|
const program = new Command('thirdfy-agent');
|
|
109
109
|
program.name('thirdfy-agent');
|
|
110
|
+
// Commander 13+ rejects extra positional arguments by default. 0.2.76 (commander 12) ignored them,
|
|
111
|
+
// and agents that pass an id positionally (`intent-status <id>`) must keep working.
|
|
112
|
+
program.allowExcessArguments(true);
|
|
110
113
|
attachGlobalOptions(program);
|
|
111
114
|
program.addHelpCommand(false);
|
|
112
115
|
program.showHelpAfterError(false);
|
package/src/commands/avantis.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { toLegacyVenueActionKey } from '../core/venueBrand.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Veranta (formerly Avantis) helpers. Names keep the Avantis spelling (internal identifiers); every
|
|
5
|
+
* check accepts both the `*_avantis_*` and `*_veranta_*` action spellings.
|
|
6
|
+
*/
|
|
1
7
|
export function createAvantisHelpers(deps) {
|
|
2
8
|
const { getPreparedParams, resolveManagedExecutionPreflight } = deps;
|
|
3
9
|
|
|
4
10
|
function avantisDefaultAddressParamForAction(action) {
|
|
5
|
-
const canonical =
|
|
6
|
-
.trim()
|
|
7
|
-
.toLowerCase()
|
|
8
|
-
.replace(/_/g, '-');
|
|
11
|
+
const canonical = toLegacyVenueActionKey(action).replace(/_/g, '-');
|
|
9
12
|
if (
|
|
10
13
|
canonical === 'get-avantis-open-orders' ||
|
|
11
14
|
canonical === 'get-avantis-positions' ||
|
package/src/commands/credits.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { requireFlag } from '../core/args.mjs';
|
|
2
2
|
import { apiGet, apiPost } from '../core/http.mjs';
|
|
3
|
+
import { toAvantisSpelling } from '../core/venueBrand.mjs';
|
|
3
4
|
import {
|
|
4
5
|
formatCreditsEstimateSummary,
|
|
5
6
|
formatCreditsModelsSummary,
|
|
@@ -117,7 +118,8 @@ export function createCreditsCommands({ printEnvelope, getAuthToken }) {
|
|
|
117
118
|
ctx,
|
|
118
119
|
'/api/v1/credits/pricing/quote',
|
|
119
120
|
{
|
|
120
|
-
|
|
121
|
+
// Veranta names price as their Avantis spelling, which every API deployment knows.
|
|
122
|
+
actionKey: toAvantisSpelling(action),
|
|
121
123
|
chainId: Number(flags.chainId || 8453),
|
|
122
124
|
},
|
|
123
125
|
{ Authorization: `Bearer ${authToken}` },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { requireFlag, parseJsonFlag } from '../core/args.mjs';
|
|
2
2
|
import { apiGet, apiPost } from '../core/http.mjs';
|
|
3
|
+
import { hasVerantaToken, normalizeVenueProviderId } from '../core/venueBrand.mjs';
|
|
3
4
|
export function createTelemetryCommands({ printEnvelope, resolveAgentApiKey }) {
|
|
4
5
|
function applySharedPortfolioListingFiltersToQuery(query, flags) {
|
|
5
6
|
if (flags.chainId) query.set('chainId', String(flags.chainId));
|
|
@@ -110,7 +111,10 @@ async function commandTrackAction(ctx, flags, capabilities) {
|
|
|
110
111
|
if (flags.idempotencyKey) payload.idempotencyKey = String(flags.idempotencyKey);
|
|
111
112
|
if (flags.category) payload.category = String(flags.category);
|
|
112
113
|
if (flags.source) payload.source = String(flags.source);
|
|
113
|
-
if (flags.provider)
|
|
114
|
+
if (flags.provider) {
|
|
115
|
+
// `veranta` / `veranta-vault` report under the internal `avantis` / `avantis-vault` ids.
|
|
116
|
+
payload.provider = hasVerantaToken(flags.provider) ? normalizeVenueProviderId(flags.provider) : String(flags.provider);
|
|
117
|
+
}
|
|
114
118
|
if (flags.protocol) payload.protocol = String(flags.protocol);
|
|
115
119
|
if (flags.namespace) payload.namespace = String(flags.namespace);
|
|
116
120
|
if (flags.actionType) payload.actionType = String(flags.actionType);
|
package/src/commands/venue.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { apiGet, apiPost } from '../core/http.mjs';
|
|
2
2
|
import { loadProfileConfig } from '../core/context.mjs';
|
|
3
|
+
import { normalizeVenueProviderId } from '../core/venueBrand.mjs';
|
|
3
4
|
|
|
5
|
+
// `avantis` is the internal venue id the API expects; `veranta` (the venue's new name) maps onto it.
|
|
4
6
|
const VENUES = new Set(['polymarket', 'hyperliquid', 'lighter', 'avantis']);
|
|
5
7
|
|
|
6
8
|
function resolveVenue(flags) {
|
|
7
9
|
const fromQuery = Array.isArray(flags.query) ? flags.query[0] : flags.query;
|
|
8
|
-
const
|
|
10
|
+
const requested = String(flags.venue || fromQuery || '').trim().toLowerCase();
|
|
11
|
+
const venue = normalizeVenueProviderId(requested);
|
|
9
12
|
if (!VENUES.has(venue)) {
|
|
10
|
-
throw new Error(`Unsupported venue "${
|
|
13
|
+
throw new Error(`Unsupported venue "${requested}". Use polymarket, hyperliquid, lighter, or veranta.`);
|
|
11
14
|
}
|
|
12
15
|
return venue;
|
|
13
16
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_TIMEOUT_MS } from './constants.mjs';
|
|
2
2
|
import { isReadOnlyActionName, isReadOnlyResolvedAction } from './readOnlyActions.mjs';
|
|
3
|
+
import { toLegacyVenueActionKey } from './venueBrand.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Default HTTP timeout for managed writes. A perps order or TP/SL update signs, broadcasts, and waits
|
|
@@ -8,6 +9,7 @@ import { isReadOnlyActionName, isReadOnlyResolvedAction } from './readOnlyAction
|
|
|
8
9
|
*/
|
|
9
10
|
const DEFAULT_WRITE_TIMEOUT_MS = 90_000;
|
|
10
11
|
|
|
12
|
+
// Keyed by the Avantis spelling; `normalizeActionId` maps Veranta names onto it.
|
|
11
13
|
const LONG_RUNNING_ACTIONS = new Set([
|
|
12
14
|
'complete_avantis_onboarding',
|
|
13
15
|
'get_avantis_setup_status',
|
|
@@ -29,17 +31,14 @@ const LONG_RUNNING_ACTIONS = new Set([
|
|
|
29
31
|
'swap',
|
|
30
32
|
'execute_optimal_swap',
|
|
31
33
|
'deposit_hyperliquid_cctp',
|
|
32
|
-
// Avantis market open/close: the API streams the batched order for up to 90s, then polls its
|
|
34
|
+
// Veranta (formerly Avantis) market open/close: the API streams the batched order for up to 90s, then polls its
|
|
33
35
|
// tracking status for up to 90s more before it answers.
|
|
34
36
|
'place_avantis_perps_order',
|
|
35
37
|
'close_avantis_position',
|
|
36
38
|
]);
|
|
37
39
|
|
|
38
40
|
function normalizeActionId(action) {
|
|
39
|
-
return
|
|
40
|
-
.trim()
|
|
41
|
-
.replace(/-/g, '_')
|
|
42
|
-
.toLowerCase();
|
|
41
|
+
return toLegacyVenueActionKey(action).replace(/-/g, '_');
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
function positiveEnvMs(...names) {
|
package/src/core/context.mjs
CHANGED
|
@@ -131,11 +131,11 @@ export async function getCachedActionsCatalog(ctx, flags) {
|
|
|
131
131
|
if (hit && hit.expiresAt > now) {
|
|
132
132
|
return hit.actions;
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
// The agent key goes in the `x-agent-api-key` header, never the URL: URLs land in access logs and
|
|
135
|
+
// proxies, and API 3.15 answers a URL key with Deprecation headers.
|
|
136
|
+
const response = policyAware
|
|
137
|
+
? await apiGet(ctx, '/api/v1/agent/actions', { 'x-agent-api-key': String(flags.agentApiKey) })
|
|
138
|
+
: await apiGet(ctx, '/api/v1/agent/actions/catalog');
|
|
139
139
|
const actions = extractActions(response);
|
|
140
140
|
catalogCache.set(cacheKey, {
|
|
141
141
|
actions,
|
|
@@ -42,3 +42,31 @@ export function wrapWriteIdempotencyKeyForHostedRuntime(existingKey, runtimeId,
|
|
|
42
42
|
}
|
|
43
43
|
return existing;
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/** Told to the caller when the API answered a write from an earlier identical one. */
|
|
47
|
+
export const idempotentReplayNote = (keyArg = 'executionIdempotencyKey') =>
|
|
48
|
+
`This result is from an earlier identical write; nothing new was executed. Send a new ${keyArg} to place another.`;
|
|
49
|
+
|
|
50
|
+
export const IDEMPOTENT_REPLAY_NOTE = idempotentReplayNote();
|
|
51
|
+
|
|
52
|
+
function hasReplayFlag(value, depth) {
|
|
53
|
+
if (!value || typeof value !== 'object' || depth > 4) return false;
|
|
54
|
+
if (Array.isArray(value)) return value.some((item) => hasReplayFlag(item, depth + 1));
|
|
55
|
+
if (value.idempotentReplay === true) return true;
|
|
56
|
+
return Object.values(value).some((item) => hasReplayFlag(item, depth + 1));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `{ idempotentReplay: true, note }` when the API replayed an earlier write with the same key:
|
|
61
|
+
* `idempotentReplay: true` anywhere in the body, or an `Idempotent-Replayed: true` header (API
|
|
62
|
+
* 3.15). The note names the key field to change (`executionIdempotencyKey` on /execute,
|
|
63
|
+
* `idempotencyKey` on /execute-intent; both come from `--idempotency-key`). Mirrors thirdfy-mcp
|
|
64
|
+
* `idempotentReplayFields`.
|
|
65
|
+
*/
|
|
66
|
+
export function idempotentReplayFields(data, headers, keyArg) {
|
|
67
|
+
const header = headers
|
|
68
|
+
? Object.entries(headers).find(([name]) => name.toLowerCase() === 'idempotent-replayed')?.[1]
|
|
69
|
+
: undefined;
|
|
70
|
+
const replayed = String(header ?? '').trim().toLowerCase() === 'true' || hasReplayFlag(data, 0);
|
|
71
|
+
return replayed ? { idempotentReplay: true, note: idempotentReplayNote(keyArg) } : {};
|
|
72
|
+
}
|
package/src/core/http.mjs
CHANGED
|
@@ -17,21 +17,71 @@ function isTimeoutError(error) {
|
|
|
17
17
|
return error?.name === 'AbortError' || message.includes('abort') || message.includes('timeout');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** The idempotency key a write body carries, and the field that carries it. */
|
|
21
|
+
function writeIdempotencyKey(init) {
|
|
22
|
+
try {
|
|
23
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
24
|
+
const executionKey = String(body?.executionIdempotencyKey || '').trim();
|
|
25
|
+
if (executionKey) return { key: executionKey, keyArg: 'executionIdempotencyKey', action: body?.action };
|
|
26
|
+
const intentKey = String(body?.idempotencyKey || '').trim();
|
|
27
|
+
if (intentKey) return { key: intentKey, keyArg: 'idempotencyKey', action: body?.action };
|
|
28
|
+
return { key: '', keyArg: 'executionIdempotencyKey', action: body?.action };
|
|
29
|
+
} catch {
|
|
30
|
+
return { key: '', keyArg: 'executionIdempotencyKey', action: undefined };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
/**
|
|
21
35
|
* A write whose request timed out may already be on chain. Resending it can double a trade, so the
|
|
22
|
-
* caller gets `WRITE_OUTCOME_UNKNOWN` and re-reads state before any retry.
|
|
36
|
+
* caller gets `WRITE_OUTCOME_UNKNOWN` and re-reads state before any retry. The advice matches
|
|
37
|
+
* thirdfy-mcp `thirdfyWriteOutcomeUnknownPayload`: reusing the same key lets the API answer with
|
|
38
|
+
* the first write's result if it landed (the API holds an unknown-outcome claim for 24 h).
|
|
23
39
|
*/
|
|
24
|
-
function writeOutcomeUnknownError(route, ctx, cause) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
function writeOutcomeUnknownError(route, ctx, cause, init) {
|
|
41
|
+
const { key, keyArg, action } = writeIdempotencyKey(init);
|
|
42
|
+
const retryAdvice = key
|
|
43
|
+
? ` If it landed, do not send it again. If you cannot tell and retry, reuse this exact ${keyArg} ("${key}") so the API cannot run it twice. If the re-read shows it did not land, send it again with a new ${keyArg}.`
|
|
44
|
+
: '';
|
|
45
|
+
const details = `Thirdfy did not answer within ${Math.round(Number(ctx.timeoutMs || 0) / 1000)}s. The write may have executed. Re-read positions, orders, or balances first, and do not resend the same write blindly.${retryAdvice}`;
|
|
46
|
+
const error = new Error(`WRITE_OUTCOME_UNKNOWN: ${route} timed out after ${ctx.timeoutMs}ms. ${details}`);
|
|
28
47
|
error.code = 'WRITE_OUTCOME_UNKNOWN';
|
|
29
48
|
error.statusCode = 504;
|
|
30
|
-
error.payload = {
|
|
49
|
+
error.payload = {
|
|
50
|
+
error: 'WRITE_OUTCOME_UNKNOWN',
|
|
51
|
+
code: 504,
|
|
52
|
+
...(action ? { action: String(action) } : {}),
|
|
53
|
+
...(key ? { [keyArg]: key } : {}),
|
|
54
|
+
details,
|
|
55
|
+
nextAction: 'reread_state_before_retry',
|
|
56
|
+
timeoutMs: ctx.timeoutMs,
|
|
57
|
+
};
|
|
31
58
|
error.cause = cause;
|
|
32
59
|
return error;
|
|
33
60
|
}
|
|
34
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Response headers a caller may need after `requestJson` returns only the parsed body. Kept off the
|
|
64
|
+
* body itself so nothing extra is printed. Today: `Idempotent-Replayed` (API 3.15).
|
|
65
|
+
*/
|
|
66
|
+
const responseHeadersByBody = new WeakMap();
|
|
67
|
+
|
|
68
|
+
const TRACKED_RESPONSE_HEADERS = ['idempotent-replayed'];
|
|
69
|
+
|
|
70
|
+
function rememberResponseHeaders(data, response) {
|
|
71
|
+
if (!data || typeof data !== 'object' || typeof response?.headers?.get !== 'function') return;
|
|
72
|
+
const tracked = {};
|
|
73
|
+
for (const name of TRACKED_RESPONSE_HEADERS) {
|
|
74
|
+
const value = response.headers.get(name);
|
|
75
|
+
if (value !== null && value !== undefined) tracked[name] = String(value);
|
|
76
|
+
}
|
|
77
|
+
if (Object.keys(tracked).length) responseHeadersByBody.set(data, tracked);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Tracked response headers (lowercase names) for a body returned by `apiGet` / `apiPost`. */
|
|
81
|
+
export function responseHeadersFor(data) {
|
|
82
|
+
return (data && typeof data === 'object' && responseHeadersByBody.get(data)) || {};
|
|
83
|
+
}
|
|
84
|
+
|
|
35
85
|
/**
|
|
36
86
|
* Whether a request that timed out may be sent again. GETs, validation-only previews, and calls the
|
|
37
87
|
* caller marks `writeAction: false` are safe. Preflight sends `executionLane: validation_only`
|
|
@@ -96,6 +146,7 @@ async function requestJsonOnce(ctx, route, init) {
|
|
|
96
146
|
});
|
|
97
147
|
const text = await response.text();
|
|
98
148
|
const data = text ? safeJsonParse(text) : {};
|
|
149
|
+
rememberResponseHeaders(data, response);
|
|
99
150
|
if (!response.ok) {
|
|
100
151
|
const error = new Error(data?.error || data?.message || `HTTP ${response.status}`);
|
|
101
152
|
error.statusCode = response.status;
|
|
@@ -116,7 +167,7 @@ export async function requestJson(ctx, route, init) {
|
|
|
116
167
|
} catch (error) {
|
|
117
168
|
lastError = error;
|
|
118
169
|
if (isTimeoutError(error) && !resendAfterTimeoutIsSafe(ctx, init)) {
|
|
119
|
-
throw writeOutcomeUnknownError(route, ctx, error);
|
|
170
|
+
throw writeOutcomeUnknownError(route, ctx, error, init);
|
|
120
171
|
}
|
|
121
172
|
if (attempt === 0 && isRetryableApiError(error)) {
|
|
122
173
|
await sleep(250 + Math.floor(Math.random() * 250));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Veranta (formerly Avantis) naming.
|
|
3
|
+
*
|
|
4
|
+
* Avantis, the Base perps venue, rebranded to Veranta. The Thirdfy API is moving its action names
|
|
5
|
+
* from `*_avantis_*` to `*_veranta_*` (Veranta canonical, Avantis kept as aliases). Until every API
|
|
6
|
+
* deployment has the Veranta rows, the CLI has to work against both catalogs:
|
|
7
|
+
*
|
|
8
|
+
* - A Veranta action name resolves to whatever the catalog in hand has (the Avantis row on an older API).
|
|
9
|
+
* - An Avantis action name keeps working unchanged.
|
|
10
|
+
* - Hardcoded CLI lists (lanes, timeouts, address defaults) are keyed by the Avantis spelling, so they
|
|
11
|
+
* compare through `toLegacyVenueActionKey`.
|
|
12
|
+
*
|
|
13
|
+
* Provider ids `avantis` / `avantis-vault` stay the internal identifiers. `veranta` / `veranta-vault`
|
|
14
|
+
* are accepted wherever the CLI reads a provider id.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const LEGACY_TOKEN = 'avantis';
|
|
18
|
+
const BRAND_TOKEN = 'veranta';
|
|
19
|
+
|
|
20
|
+
// The token only counts as a whole word inside an action or provider id (`_`/`-` separated).
|
|
21
|
+
const BRAND_TOKEN_RE = /(^|[_-])veranta(?=[_-]|$)/gi;
|
|
22
|
+
const LEGACY_TOKEN_RE = /(^|[_-])avantis(?=[_-]|$)/gi;
|
|
23
|
+
|
|
24
|
+
/** Human-facing venue name. */
|
|
25
|
+
export const VERANTA_DISPLAY_NAME = 'Veranta';
|
|
26
|
+
|
|
27
|
+
/** True when the id carries the Veranta token (`place_veranta_perps_order`, `veranta-vault`). */
|
|
28
|
+
export function hasVerantaToken(value) {
|
|
29
|
+
BRAND_TOKEN_RE.lastIndex = 0;
|
|
30
|
+
return BRAND_TOKEN_RE.test(String(value || ''));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when the id carries the Avantis token (`place_avantis_perps_order`, `avantis-vault`). */
|
|
34
|
+
export function hasAvantisToken(value) {
|
|
35
|
+
LEGACY_TOKEN_RE.lastIndex = 0;
|
|
36
|
+
return LEGACY_TOKEN_RE.test(String(value || ''));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `place_veranta_perps_order` -> `place_avantis_perps_order`. Other names pass through unchanged. */
|
|
40
|
+
export function toAvantisSpelling(value) {
|
|
41
|
+
return String(value ?? '').replace(BRAND_TOKEN_RE, `$1${LEGACY_TOKEN}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `place_avantis_perps_order` -> `place_veranta_perps_order`. Other names pass through unchanged. */
|
|
45
|
+
export function toVerantaSpelling(value) {
|
|
46
|
+
return String(value ?? '').replace(LEGACY_TOKEN_RE, `$1${BRAND_TOKEN}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The other spelling of a venue action name, or null when the name has no Avantis/Veranta token.
|
|
51
|
+
* Used to retry catalog resolution with the spelling the API in hand still uses.
|
|
52
|
+
*/
|
|
53
|
+
export function alternateVenueActionSpelling(value) {
|
|
54
|
+
const text = String(value ?? '');
|
|
55
|
+
if (hasVerantaToken(text)) return toAvantisSpelling(text);
|
|
56
|
+
if (hasAvantisToken(text)) return toVerantaSpelling(text);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Brand-insensitive action key for hardcoded CLI lists: trimmed, lowercased, Veranta mapped to the
|
|
62
|
+
* Avantis spelling. Separators are preserved; callers still pick `_` or `-`.
|
|
63
|
+
*/
|
|
64
|
+
export function toLegacyVenueActionKey(value) {
|
|
65
|
+
return toAvantisSpelling(String(value ?? '').trim().toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Normalize a provider or venue id the CLI reads from flags: trimmed, lowercased, and
|
|
70
|
+
* `veranta` / `veranta-vault` mapped to the internal `avantis` / `avantis-vault`.
|
|
71
|
+
*/
|
|
72
|
+
export function normalizeVenueProviderId(value) {
|
|
73
|
+
const id = String(value ?? '').trim().toLowerCase();
|
|
74
|
+
if (!id) return id;
|
|
75
|
+
return toAvantisSpelling(id);
|
|
76
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { requireFlag } from '../../core/args.mjs';
|
|
2
2
|
import { getCachedActionsCatalog } from '../../core/context.mjs';
|
|
3
|
+
import { suggestCatalogActions } from '../validation/actionParamsGate.mjs';
|
|
4
|
+
import {
|
|
5
|
+
alternateVenueActionSpelling,
|
|
6
|
+
normalizeVenueProviderId,
|
|
7
|
+
toAvantisSpelling,
|
|
8
|
+
toLegacyVenueActionKey,
|
|
9
|
+
} from '../../core/venueBrand.mjs';
|
|
3
10
|
export function createActionSelection({ getTradingProviderHint, formatProviderAmbiguousActionHint, formatProviderUnknownActionHint }) {
|
|
4
11
|
function getActionKey(action) {
|
|
5
12
|
return String(action?.action || action?.id || action?.key || action?.name || '')
|
|
@@ -20,7 +27,8 @@ function formatActionList(items) {
|
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
function normalizeProviderId(value) {
|
|
23
|
-
|
|
30
|
+
// Lowercased id; `veranta` / `veranta-vault` map to the internal `avantis` / `avantis-vault`.
|
|
31
|
+
return normalizeVenueProviderId(value || '');
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
function rowActionKey(action) {
|
|
@@ -45,7 +53,8 @@ function canonicalizeCatalogActionKey(value) {
|
|
|
45
53
|
function inferActionProvider(action) {
|
|
46
54
|
// API catalog metadata (`providerId`, `domain`, `category`) is the source of truth.
|
|
47
55
|
// This fallback only keeps older catalog rows / older API deployments discoverable.
|
|
48
|
-
|
|
56
|
+
// Veranta action names infer like their Avantis spelling (`avantis` / `avantis-vault`).
|
|
57
|
+
const key = toLegacyVenueActionKey(canonicalActionKey(action));
|
|
49
58
|
const knownMap = {
|
|
50
59
|
swap: 'trading',
|
|
51
60
|
'execute-optimal-swap': 'trading',
|
|
@@ -262,7 +271,8 @@ function isSpotTradingCatalogAction(action) {
|
|
|
262
271
|
}
|
|
263
272
|
|
|
264
273
|
function isProviderActionKey(action, provider) {
|
|
265
|
-
|
|
274
|
+
// Brand-insensitive: `place_veranta_perps_order` matches provider `avantis`.
|
|
275
|
+
const actionKey = toLegacyVenueActionKey(rowActionKey(action));
|
|
266
276
|
const normalizedProvider = normalizeProviderId(provider).replace(/-/g, '_');
|
|
267
277
|
if (!actionKey || !normalizedProvider) return false;
|
|
268
278
|
return (
|
|
@@ -282,6 +292,7 @@ function actionMatchesProvider(action, requestedProvider, allActions) {
|
|
|
282
292
|
if (requestedProvider === 'hyperliquid') return isProviderActionKey(action, 'hyperliquid');
|
|
283
293
|
if (requestedProvider === 'lighter') return isProviderActionKey(action, 'lighter');
|
|
284
294
|
if (requestedProvider === 'avantis') {
|
|
295
|
+
// Veranta (formerly Avantis) perps; `--provider veranta` normalizes to `avantis` before this runs.
|
|
285
296
|
return isProviderActionKey(action, 'avantis') && !actionKey.includes('vault');
|
|
286
297
|
}
|
|
287
298
|
if (requestedProvider === 'polymarket') return isProviderActionKey(action, 'polymarket');
|
|
@@ -350,20 +361,12 @@ async function resolveActionSelection(ctx, flags) {
|
|
|
350
361
|
return resolveActionFromRows(requestedAction, actions, flags);
|
|
351
362
|
}
|
|
352
363
|
|
|
353
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Exact catalog match for one spelling: canonical overrides, action id, `_`/`-` canonical id, then
|
|
366
|
+
* alias. Returns null when nothing matches; throws on ambiguity.
|
|
367
|
+
*/
|
|
368
|
+
function matchCatalogEntry(requestedAction, keyed, flags = {}) {
|
|
354
369
|
const requestedLower = String(requestedAction || '').toLowerCase();
|
|
355
|
-
const keyed = actions
|
|
356
|
-
.map((action) => ({
|
|
357
|
-
key: getActionKey(action),
|
|
358
|
-
aliases: getActionAliases(action),
|
|
359
|
-
action,
|
|
360
|
-
}))
|
|
361
|
-
.filter((entry) => entry.key);
|
|
362
|
-
|
|
363
|
-
if (!keyed.length) {
|
|
364
|
-
return { requestedAction, resolvedAction: requestedAction, resolvedActionMeta: null };
|
|
365
|
-
}
|
|
366
|
-
|
|
367
370
|
const canonicalAliasOverrides = {
|
|
368
371
|
get_vaults_fyi_vaults: 'get-vaults-fyi-vaults',
|
|
369
372
|
get_vaults_fyi_opportunities: 'get-vaults-fyi-vaults',
|
|
@@ -445,11 +448,47 @@ function resolveActionFromRows(requestedAction, actions, flags = {}) {
|
|
|
445
448
|
)}.${formatProviderAmbiguousActionHint(providerHint)}`
|
|
446
449
|
);
|
|
447
450
|
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function resolveActionFromRows(requestedAction, actions, flags = {}) {
|
|
455
|
+
const requestedLower = String(requestedAction || '').toLowerCase();
|
|
456
|
+
const keyed = actions
|
|
457
|
+
.map((action) => ({
|
|
458
|
+
key: getActionKey(action),
|
|
459
|
+
aliases: getActionAliases(action),
|
|
460
|
+
action,
|
|
461
|
+
}))
|
|
462
|
+
.filter((entry) => entry.key);
|
|
463
|
+
|
|
464
|
+
if (!keyed.length) {
|
|
465
|
+
// No catalog to check against: send the Avantis spelling of a Veranta name, which every API
|
|
466
|
+
// deployment accepts (older ones only know `*_avantis_*`; newer ones keep it as an alias).
|
|
467
|
+
return {
|
|
468
|
+
requestedAction,
|
|
469
|
+
resolvedAction: alternateVenueActionSpelling(requestedAction) ? toAvantisSpelling(requestedAction) : requestedAction,
|
|
470
|
+
resolvedActionMeta: null,
|
|
471
|
+
suggestions: [],
|
|
472
|
+
};
|
|
473
|
+
}
|
|
448
474
|
|
|
475
|
+
const direct = matchCatalogEntry(requestedAction, keyed, flags);
|
|
476
|
+
if (direct) return direct;
|
|
477
|
+
|
|
478
|
+
// Veranta (formerly Avantis): retry with the other spelling so `place_veranta_perps_order` finds the
|
|
479
|
+
// `place_avantis_perps_order` row on an API that has not shipped Veranta names, and the reverse.
|
|
480
|
+
const alternate = alternateVenueActionSpelling(requestedAction);
|
|
481
|
+
if (alternate) {
|
|
482
|
+
const viaAlternate = matchCatalogEntry(alternate, keyed, flags);
|
|
483
|
+
if (viaAlternate) return { ...viaAlternate, requestedAction };
|
|
484
|
+
}
|
|
485
|
+
const alternateLower = alternate ? alternate.toLowerCase() : null;
|
|
486
|
+
|
|
487
|
+
const fuzzyNeedles = alternateLower ? [requestedLower, alternateLower] : [requestedLower];
|
|
449
488
|
const fuzzy = keyed.filter(
|
|
450
489
|
(entry) =>
|
|
451
|
-
entry.key.toLowerCase().includes(
|
|
452
|
-
entry.aliases.some((a) => a.toLowerCase().includes(
|
|
490
|
+
fuzzyNeedles.some((needle) => entry.key.toLowerCase().includes(needle)) ||
|
|
491
|
+
entry.aliases.some((a) => fuzzyNeedles.some((needle) => a.toLowerCase().includes(needle)))
|
|
453
492
|
);
|
|
454
493
|
if (fuzzy.length === 1) {
|
|
455
494
|
return { requestedAction, resolvedAction: fuzzy[0].key, resolvedActionMeta: fuzzy[0].action || null };
|
|
@@ -462,7 +501,15 @@ function resolveActionFromRows(requestedAction, actions, flags = {}) {
|
|
|
462
501
|
)}?${formatProviderAmbiguousActionHint(providerHint)}`
|
|
463
502
|
);
|
|
464
503
|
}
|
|
465
|
-
return {
|
|
504
|
+
return {
|
|
505
|
+
requestedAction,
|
|
506
|
+
resolvedAction: requestedAction,
|
|
507
|
+
resolvedActionMeta: null,
|
|
508
|
+
suggestions: suggestCatalogActions(
|
|
509
|
+
requestedAction,
|
|
510
|
+
keyed.map((entry) => ({ ...entry.action, action: entry.key })),
|
|
511
|
+
),
|
|
512
|
+
};
|
|
466
513
|
}
|
|
467
514
|
|
|
468
515
|
return {
|