@thirdfy/agent-cli 0.2.81 → 0.2.83

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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.2.83] - 2026-10-08
8
+
9
+ ### Fixed
10
+
11
+ - **`preflight --run-mode agent_wallet` said `ready` for a venue order the venue could not take.** On production 2026-10-08 (API 3.19.3, CLI 0.2.82) `preflight --action place_hyperliquid_perps_order --run-mode agent_wallet` returned `PREFLIGHT_OK` with `policyEvaluated: false` and `chainId: 8453`, while `venue readiness hyperliquid` returned `HYPERLIQUID_DEPOSIT_REQUIRED`: the preflight sent `/execution-wallet` only `userDid`, `chainId`, `runMode` and `tokenIn`, so the API never saw the action. `preflight` now also sends `action`, `params` and `estimatedAmountUsd`. With Thirdfy API that supports it (capability `commandFeatures.preflightVenueReadiness`), the API checks agent policy, action limits and, for venue orders, venue readiness. A blocked answer (409 `VENUE_NOT_READY`, 403 policy, 429 limits) is `PREFLIGHT_BLOCKED` with `data.status: "blocked"`, `blockedReason`, `blockedStage`, `blockingReasons` and `venueReadiness`, instead of an HTTP error. `data.policyEvaluated` is now always present (false against an older API) and survives a later CLI-local failure (wallet mismatch, funding, native gas), whose `status` stays `failed`; only an API refusal is `blocked`. Venue actions also report `data.executionVenue` (`venue` as the API names it, `brand` for Veranta, `settlement`, `settlementChainId`, `walletLookupChainId`): `chainId` stays the chain the managed wallet was looked up on (Base by default), not where a Hyperliquid, Lighter or Polymarket order runs. `run` and the address-default lookups keep the wallet-only preflight. MCP `walletExecute` has no preflight-only mode and needs no change. Pin: `test/preflight-venue-readiness.test.cjs`.
12
+
13
+ ## [0.2.82] - 2026-10-01
14
+
15
+ ### Fixed
16
+
17
+ - **`preflight` / `run --run-mode agent_wallet` keep the API's native gas readiness.** On user-pays-gas chains (DogeOS 6281971) Thirdfy API 3.18.2 returns `gasSponsored`, `gasReady`, `nativeGasBalance` and, at 0, `nativeGasWarning` from `/execution-wallet`; the CLI rebuilt the preflight from a fixed field list and dropped them. They now appear under `data.executionWalletPreflight`, and a write whose wallet holds 0 native coin stops before `/execute` with `INSUFFICIENT_NATIVE_GAS` and the faucet message. Sponsored chains and older APIs are unchanged. Pin: `test/preflight-native-gas.test.cjs`.
18
+ - **DogeOS Laika writes check the right funding token.** `dogeos-laika-buy-token` and `dogeos-laika-launch-token` spend native DOGE (`dogeAmountWei`, `initialPurchaseWei`), and `dogeos-laika-claim` receives `tokenAddress`; the CLI sent that bonding-curve token as the execution-wallet `tokenIn` and blocked a buy with `INSUFFICIENT_FUNDS`. They now skip the ERC-20 balance check, as does any action that carries `dogeAmountWei`. `dogeos-laika-sell-token` still checks the token it sells. Pin: `test/funding-token-in.test.cjs`.
19
+
7
20
  ## [0.2.81] - 2026-10-01
8
21
 
9
22
  ### Added
package/README.md CHANGED
@@ -40,11 +40,10 @@ Run without global install:
40
40
  npx @thirdfy/agent-cli --help
41
41
  ```
42
42
 
43
- ## What's new in v0.2.81
43
+ ## What's new in v0.2.83
44
44
 
45
- - **DogeOS onboarding**: the quickstart starts with `login email`, warns not to use `managed wallet init` for `agent_wallet`, funds gas and test USDC from the DogeOS faucet, starts with a USDC→WDOGE swap, and adds `dogeos-laika-sell-token`. See [docs/dogeos-quickstart.md](./docs/dogeos-quickstart.md).
46
- - **Chain-aware gas notes**: `help onboarding --chain-id` and `doctor auth --chain-id` say when the sending wallet pays gas (DogeOS) and link the faucet.
47
- - **`wallet submit --chain-id` works**, reads the chain from the signed transaction when `--chain-id` is omitted, and picks the RPC for that chain. Base keeps its order (`BASE_RPC_URL`, `BASE_RPC_HTTP_URL`, `RPC_URL`); other chains use `--rpc-url`, then `RPC_URL`, then the built-in DogeOS RPC. Self-mode broadcast (`self-exec`) uses the same rule. A Base RPC is never used on DogeOS.
45
+ - **`preflight --run-mode agent_wallet` checks the action, not just the wallet.** The CLI sends `action` and `params` to the API, which checks agent policy, limits and venue readiness (Thirdfy API 3.19.4). A venue that is not ready (for example `HYPERLIQUID_DEPOSIT_REQUIRED`) returns `PREFLIGHT_BLOCKED` with `blockingReasons` and `venueReadiness`, matching `venue readiness <venue>`.
46
+ - **`data.policyEvaluated`** is always present, and venue actions report `data.executionVenue` (where the order settles, next to the chain the wallet was looked up on). `run` is unchanged.
48
47
  - See [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases for older versions.
49
48
 
50
49
  ## Quick start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.2.81",
3
+ "version": "0.2.83",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,9 @@ const NATIVE_ETH_SPEND_ACTIONS = new Set([
9
9
  'basedbid_create_lbp',
10
10
  'basedbid_create_flash',
11
11
  'basedbid_create_board',
12
+ // DogeOS Laika: buys and launches spend native DOGE; tokenAddress is the bonding-curve token received.
13
+ 'dogeos_laika_buy_token',
14
+ 'dogeos_laika_launch_token',
12
15
  ]);
13
16
 
14
17
  /**
@@ -18,6 +21,7 @@ const NATIVE_ETH_SPEND_ACTIONS = new Set([
18
21
  const POSITION_EXIT_ACTIONS = new Set([
19
22
  'withdraw_earn_position',
20
23
  'claim_earn_rewards',
24
+ 'dogeos_laika_claim',
21
25
  ]);
22
26
 
23
27
  export function resolveTokenInForFunding(params, action) {
@@ -28,5 +32,6 @@ export function resolveTokenInForFunding(params, action) {
28
32
  if (NATIVE_ETH_SPEND_ACTIONS.has(actionId)) return '';
29
33
  if (POSITION_EXIT_ACTIONS.has(actionId)) return '';
30
34
  if (params.amountEth != null && String(params.amountEth).trim() !== '') return '';
35
+ if (params.dogeAmountWei != null && String(params.dogeAmountWei).trim() !== '') return '';
31
36
  return String(params.tokenAddress || params.token || '').trim();
32
37
  }
@@ -0,0 +1,93 @@
1
+ import { toLegacyVenueActionKey } from '../../core/venueBrand.mjs';
2
+ import { resolveExecuteEstimatedAmountUsd } from './payloads.mjs';
3
+
4
+ /**
5
+ * Action-aware `agent_wallet` preflight (Thirdfy API: `GET /agent/execution-wallet?action=…`).
6
+ *
7
+ * Up to 0.2.82 the CLI sent only `userDid`, `chainId`, `runMode` and `tokenIn`, so the API never
8
+ * saw the action: `preflight --action place_hyperliquid_perps_order --run-mode agent_wallet` said
9
+ * `ready` with `policyEvaluated: false` while `venue readiness hyperliquid` said
10
+ * `HYPERLIQUID_DEPOSIT_REQUIRED` (production, 2026-10-08). Sending the action lets the API run
11
+ * agent policy, action limits and, for venue orders, venue readiness. An API without the feature
12
+ * ignores the extra query fields and answers `policyEvaluated` absent (reported as false).
13
+ */
14
+
15
+ /** Venue an action trades on, and where it settles. Null for actions outside the four venues. */
16
+ const VENUE_SETTLEMENT = Object.freeze({
17
+ hyperliquid: { venue: 'hyperliquid', settlement: 'hyperliquid_l1', settlementChainId: null },
18
+ lighter: { venue: 'lighter', settlement: 'lighter_l2', settlementChainId: null },
19
+ polymarket: { venue: 'polymarket', settlement: 'polygon', settlementChainId: 137 },
20
+ // `venue` matches the API's stored id; `brand` is the public name.
21
+ avantis: { venue: 'avantis', brand: 'veranta', settlement: 'base', settlementChainId: 8453 },
22
+ });
23
+
24
+ export function resolveActionVenue(action) {
25
+ const key = toLegacyVenueActionKey(action).replace(/-/g, '_');
26
+ for (const token of Object.keys(VENUE_SETTLEMENT)) {
27
+ if (new RegExp(`(^|_)${token}(_|$)`).test(key)) return VENUE_SETTLEMENT[token];
28
+ }
29
+ return null;
30
+ }
31
+
32
+ /**
33
+ * The venue an action executes on, next to the chain the managed wallet was looked up on. The
34
+ * managed wallet is one EVM address on every chain, so a Hyperliquid order still resolves it on the
35
+ * default chain; this says the order itself does not run there.
36
+ */
37
+ export function describeExecutionVenue(action, walletLookupChainId) {
38
+ const venue = resolveActionVenue(action);
39
+ if (!venue) return null;
40
+ return { ...venue, walletLookupChainId: Number(walletLookupChainId) || null };
41
+ }
42
+
43
+ /** Adds `action`, `params` and `estimatedAmountUsd` to an `/execution-wallet` query. */
44
+ export function appendActionPreflightQuery(query, { action, params, flags }) {
45
+ const actionId = String(action || '').trim();
46
+ if (!actionId) return query;
47
+ query.set('action', actionId);
48
+ if (params && typeof params === 'object' && !Array.isArray(params) && Object.keys(params).length) {
49
+ query.set('params', JSON.stringify(params));
50
+ }
51
+ const estimatedAmountUsd = resolveExecuteEstimatedAmountUsd(actionId, flags);
52
+ if (estimatedAmountUsd != null) query.set('estimatedAmountUsd', String(estimatedAmountUsd));
53
+ return query;
54
+ }
55
+
56
+ /** Venue fields an action preflight carries onto the CLI envelope. */
57
+ export const ACTION_PREFLIGHT_VENUE_FIELDS = Object.freeze(['venue', 'venueReadiness', 'blockingReasons']);
58
+
59
+ /** Stages at which the API refused the action itself (as opposed to a CLI-local check). */
60
+ const API_BLOCK_STAGES = new Set(['catalog', 'policy', 'venue_readiness']);
61
+
62
+ /** True when the API blocked the action (policy, catalog, venue readiness). */
63
+ export function isApiActionBlock(preflight) {
64
+ return preflight?.success === false && API_BLOCK_STAGES.has(String(preflight?.blockedStage || ''));
65
+ }
66
+
67
+ /** Policy and venue fields of an action-aware `/execution-wallet` answer (allowed or blocked). */
68
+ export function pickActionPreflightFields(response) {
69
+ const out = { policyEvaluated: response?.policyEvaluated === true };
70
+ for (const key of [...ACTION_PREFLIGHT_VENUE_FIELDS, 'actionPreflight']) {
71
+ if (response?.[key] != null) out[key] = response[key];
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /**
77
+ * A blocked action preflight arrives as a non-2xx answer (409 `VENUE_NOT_READY`, 403 policy, 429
78
+ * limits) with `preflightBlocked: true`. Turn it into a blocked preflight result; null otherwise.
79
+ */
80
+ export function blockedActionPreflightFromError(error) {
81
+ const payload = error?.payload;
82
+ if (!payload || typeof payload !== 'object' || payload.preflightBlocked !== true) return null;
83
+ return {
84
+ success: false,
85
+ blockedReason: String(payload.blockedReason || 'PREFLIGHT_BLOCKED'),
86
+ blockedStage: String(payload.blockedStage || 'policy'),
87
+ error: String(payload.error || payload.message || error?.message || 'Preflight blocked'),
88
+ statusCode: Number(error?.statusCode) || null,
89
+ signerMethod: payload.signerMethod || 'managed_wallet_server',
90
+ ...pickActionPreflightFields(payload),
91
+ ...(payload.data ? { data: payload.data } : {}),
92
+ };
93
+ }
@@ -0,0 +1,13 @@
1
+ const NATIVE_GAS_FIELDS = ['gasSponsored', 'gasReady', 'nativeGasBalance', 'nativeGasBalanceUnavailable', 'nativeGasWarning'];
2
+
3
+ /**
4
+ * Native gas readiness from an `/execution-wallet` reply (Thirdfy API 3.18.2+). Present only on
5
+ * user-pays-gas chains (DogeOS 6281971); empty for sponsored chains and older APIs.
6
+ */
7
+ export function pickNativeGasFields(response) {
8
+ const out = {};
9
+ for (const key of NATIVE_GAS_FIELDS) {
10
+ if (response && response[key] !== undefined) out[key] = response[key];
11
+ }
12
+ return out;
13
+ }
@@ -1,4 +1,13 @@
1
1
  import { randomUUID } from 'crypto';
2
+ import { pickNativeGasFields } from './nativeGas.mjs';
3
+ import {
4
+ ACTION_PREFLIGHT_VENUE_FIELDS,
5
+ appendActionPreflightQuery,
6
+ blockedActionPreflightFromError,
7
+ describeExecutionVenue,
8
+ isApiActionBlock,
9
+ pickActionPreflightFields,
10
+ } from './actionPreflight.mjs';
2
11
  import { requireFlag } from '../../core/args.mjs';
3
12
  import { createCliError } from '../../core/envelope.mjs';
4
13
  import {
@@ -203,6 +212,8 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
203
212
  // Reads carry no transaction; keep funding optional so preflight matches run.
204
213
  requireFundingCheck: !isReadOnlyAction(resolved),
205
214
  preflightSeed: options.managedPreflight,
215
+ // Ask the API to check policy and venue readiness for this action, not just the wallet.
216
+ evaluateAction: true,
206
217
  });
207
218
  const normalized = normalizeIntentResponse({
208
219
  success: preflight.success,
@@ -221,8 +232,21 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
221
232
  normalized.signerMethod = preflight.signerMethod || 'managed_wallet_server';
222
233
  normalized.executionWalletPreflight = preflight;
223
234
  normalized.fundingTokenBalance = preflight.fundingTokenBalance || null;
235
+ normalized.policyEvaluated = preflight.policyEvaluated === true;
236
+ for (const key of [...ACTION_PREFLIGHT_VENUE_FIELDS, 'executionVenue']) {
237
+ if (preflight[key] != null) normalized[key] = preflight[key];
238
+ }
239
+ // Only an API refusal of the action is `blocked`; CLI-local checks keep `failed`.
240
+ const apiBlocked = isApiActionBlock(preflight);
241
+ if (apiBlocked) normalized.status = 'blocked';
224
242
  return {
225
- message: preflight.success ? 'Managed wallet preflight completed' : 'Managed wallet preflight failed',
243
+ message: preflight.success
244
+ ? 'Managed wallet preflight completed'
245
+ : preflight.blockedStage === 'venue_readiness'
246
+ ? 'Managed wallet preflight blocked: venue not ready'
247
+ : apiBlocked
248
+ ? 'Managed wallet preflight blocked by policy'
249
+ : 'Managed wallet preflight failed',
226
250
  route: 'execution_wallet_preflight',
227
251
  normalized,
228
252
  };
@@ -767,21 +791,42 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
767
791
  runMode,
768
792
  });
769
793
  if (tokenIn) query.set('tokenIn', tokenIn);
794
+ const evaluateAction = options.evaluateAction === true;
795
+ if (evaluateAction) appendActionPreflightQuery(query, { action, params, flags });
796
+ const executionVenue = describeExecutionVenue(action, chainId);
770
797
  const preflightSeed = options.preflightSeed && typeof options.preflightSeed === 'object'
771
798
  ? options.preflightSeed
772
799
  : null;
773
800
  if (preflightSeed && preflightSeed.success === false) {
774
801
  return preflightSeed;
775
802
  }
776
- const response = preflightSeed
777
- ? {
803
+ // A seed (address default lookup) carries no policy answer; an action preflight asks the API again.
804
+ const useSeed = Boolean(preflightSeed) && !evaluateAction;
805
+ let response;
806
+ if (useSeed) {
807
+ response = {
778
808
  executionWalletAddress: preflightSeed.executionWalletAddress || null,
779
809
  signerMethod: preflightSeed.signerMethod || null,
780
810
  fundingTokenBalance: preflightSeed.fundingTokenBalance || null,
811
+ ...pickNativeGasFields(preflightSeed),
812
+ };
813
+ } else {
814
+ try {
815
+ response = await apiGet(ctx, `/api/v1/agent/execution-wallet?${query.toString()}`, {
816
+ 'x-agent-api-key': agentApiKey,
817
+ });
818
+ } catch (error) {
819
+ const blocked = evaluateAction ? blockedActionPreflightFromError(error) : null;
820
+ if (!blocked) throw error;
821
+ return {
822
+ ...blocked,
823
+ userDid,
824
+ chainId,
825
+ ...(executionVenue ? { executionVenue } : {}),
826
+ };
781
827
  }
782
- : await apiGet(ctx, `/api/v1/agent/execution-wallet?${query.toString()}`, {
783
- 'x-agent-api-key': agentApiKey,
784
- });
828
+ }
829
+ const actionFields = evaluateAction ? pickActionPreflightFields(response) : {};
785
830
  const executionWalletAddress = String(response?.executionWalletAddress || '').trim().toLowerCase();
786
831
  const signerMethod = String(response?.signerMethod || 'managed_wallet_server').trim();
787
832
  const rawBalance = String(response?.fundingTokenBalance?.raw || '').trim();
@@ -801,6 +846,8 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
801
846
  executionWalletAddress,
802
847
  signerMethod,
803
848
  fundingTokenBalance: response?.fundingTokenBalance || null,
849
+ ...actionFields,
850
+ ...(executionVenue ? { executionVenue } : {}),
804
851
  };
805
852
  }
806
853
  }
@@ -817,6 +864,8 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
817
864
  executionWalletAddress,
818
865
  signerMethod,
819
866
  fundingTokenBalance: response?.fundingTokenBalance || null,
867
+ ...actionFields,
868
+ ...(executionVenue ? { executionVenue } : {}),
820
869
  };
821
870
  }
822
871
 
@@ -829,6 +878,27 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
829
878
  executionWalletAddress,
830
879
  signerMethod,
831
880
  fundingTokenBalance: response?.fundingTokenBalance || null,
881
+ ...actionFields,
882
+ ...(executionVenue ? { executionVenue } : {}),
883
+ };
884
+ }
885
+
886
+ // User-pays-gas chains (DogeOS): the API reports native gas readiness (API 3.18.2+). Keep it, and
887
+ // stop a write before /execute when the wallet holds 0 native coin.
888
+ const nativeGas = pickNativeGasFields(response);
889
+ if (options.requireFundingCheck && nativeGas.gasReady === false) {
890
+ return {
891
+ success: false,
892
+ blockedReason: 'INSUFFICIENT_NATIVE_GAS',
893
+ blockedStage: 'sizing',
894
+ error: nativeGas.nativeGasWarning?.message
895
+ || `Execution wallet ${executionWalletAddress || 'unknown'} has 0 native gas on chain ${chainId}; gas on this chain is paid by the wallet.`,
896
+ executionWalletAddress,
897
+ signerMethod,
898
+ fundingTokenBalance: response?.fundingTokenBalance || null,
899
+ ...actionFields,
900
+ ...(executionVenue ? { executionVenue } : {}),
901
+ ...nativeGas,
832
902
  };
833
903
  }
834
904
 
@@ -840,6 +910,9 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
840
910
  userDid,
841
911
  chainId,
842
912
  tokenIn: tokenIn || null,
913
+ ...nativeGas,
914
+ ...actionFields,
915
+ ...(executionVenue ? { executionVenue } : {}),
843
916
  };
844
917
  }
845
918