@thirdfy/agent-cli 0.1.48 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +25 -23
  3. package/bin/thirdfy-agent.mjs +3 -5106
  4. package/package.json +17 -4
  5. package/scripts/validate-npm-pack.mjs +43 -0
  6. package/src/cli/errors.mjs +103 -0
  7. package/src/cli/flags.mjs +66 -0
  8. package/src/cli/help.mjs +89 -0
  9. package/src/cli/manifest.mjs +422 -0
  10. package/src/cli/options/agent.mjs +9 -0
  11. package/src/cli/options/analytics.mjs +5 -0
  12. package/src/cli/options/auth.mjs +10 -0
  13. package/src/cli/options/bootstrap.mjs +19 -0
  14. package/src/cli/options/chains.mjs +3 -0
  15. package/src/cli/options/credentials.mjs +15 -0
  16. package/src/cli/options/delegation.mjs +22 -0
  17. package/src/cli/options/execution.mjs +24 -0
  18. package/src/cli/options/global.mjs +16 -0
  19. package/src/cli/options/index.mjs +37 -0
  20. package/src/cli/options/polymarket.mjs +3 -0
  21. package/src/cli/options/tracking.mjs +18 -0
  22. package/src/cli/options/wallet.mjs +7 -0
  23. package/src/cli/program.mjs +5 -0
  24. package/src/cli/register.mjs +118 -0
  25. package/src/commands/agent.mjs +194 -0
  26. package/src/commands/bootstrap.mjs +762 -0
  27. package/src/commands/credentials.mjs +102 -0
  28. package/src/commands/delegation.mjs +355 -0
  29. package/src/commands/discovery.mjs +77 -0
  30. package/src/commands/doctor.mjs +320 -0
  31. package/src/commands/execute.mjs +255 -0
  32. package/src/commands/hyperliquid.mjs +51 -0
  33. package/src/commands/index.mjs +6 -0
  34. package/src/commands/polymarket.mjs +89 -0
  35. package/src/commands/prompt.mjs +33 -0
  36. package/src/commands/telemetry.mjs +146 -0
  37. package/src/commands/wallet.mjs +129 -0
  38. package/src/core/args.mjs +68 -0
  39. package/src/core/constants.mjs +31 -0
  40. package/src/core/context.mjs +145 -0
  41. package/src/core/envelope.mjs +20 -0
  42. package/src/core/http.mjs +77 -0
  43. package/src/core/index.mjs +6 -0
  44. package/src/core/runMode.mjs +72 -0
  45. package/src/runtime/actions/selection.mjs +339 -0
  46. package/src/runtime/agentExtract.mjs +52 -0
  47. package/src/runtime/authHelpers.mjs +53 -0
  48. package/src/runtime/bitfinexProbe.mjs +121 -0
  49. package/src/runtime/context.mjs +28 -0
  50. package/src/runtime/createThirdfyAgentRuntime.mjs +2 -0
  51. package/src/runtime/errors.mjs +3 -0
  52. package/src/runtime/execution/amounts.mjs +193 -0
  53. package/src/runtime/execution/lanes.mjs +75 -0
  54. package/src/runtime/execution/payloads.mjs +148 -0
  55. package/src/runtime/execution/runners.mjs +702 -0
  56. package/src/runtime/handlers.mjs +181 -0
  57. package/src/runtime/index.mjs +4 -0
  58. package/src/runtime/main.mjs +204 -0
  59. package/src/runtime/onboardingHints.mjs +54 -0
  60. package/src/runtime/owsSigning.mjs +118 -0
  61. package/src/runtime/providerHints.mjs +414 -0
@@ -0,0 +1,121 @@
1
+ import { createHmac } from 'crypto';
2
+ import { DEFAULT_BITFINEX_BASE_URL, BITFINEX_PERMISSION_PROFILES } from '../core/constants.mjs';
3
+ import { createCliError } from '../core/envelope.mjs';
4
+ import { requestJson, safeJsonParse } from '../core/http.mjs';
5
+ export function createBitfinexProbe() {
6
+ function parseBitfinexPermissionRows(rawData) {
7
+ const rows = Array.isArray(rawData) ? rawData : [];
8
+ const parsed = {};
9
+ for (const row of rows) {
10
+ if (!Array.isArray(row) || row.length < 3) continue;
11
+ const scope = String(row[0] || '').trim().toLowerCase();
12
+ if (!scope) continue;
13
+ const read = Number(row[1]) === 1 ? 1 : 0;
14
+ const write = Number(row[2]) === 1 ? 1 : 0;
15
+ parsed[scope] = { read, write };
16
+ }
17
+ return parsed;
18
+ }
19
+
20
+ function getMissingBitfinexScopes(scopePermissions, permissionProfile = 'funding') {
21
+ const profile = BITFINEX_PERMISSION_PROFILES[permissionProfile] || BITFINEX_PERMISSION_PROFILES.funding;
22
+ const missing = [];
23
+ for (const [scope, required] of Object.entries(profile.required)) {
24
+ const actual = scopePermissions[scope] || { read: 0, write: 0 };
25
+ if (actual.read < required.read || actual.write < required.write) {
26
+ missing.push({
27
+ scope,
28
+ required,
29
+ actual,
30
+ });
31
+ }
32
+ }
33
+ return missing;
34
+ }
35
+
36
+ async function probeBitfinexPermissions({ apiKey, apiSecret, permissionProfile = 'funding', timeoutMs, baseUrl }) {
37
+ const normalizedBaseUrl = String(baseUrl || DEFAULT_BITFINEX_BASE_URL).trim().replace(/\/+$/, '');
38
+ const apiPath = '/v2/auth/r/permissions';
39
+ const nonce = Date.now().toString();
40
+ const payload = '{}';
41
+ const signature = createHmac('sha384', String(apiSecret || ''))
42
+ .update(`/api${apiPath}${nonce}${payload}`)
43
+ .digest('hex');
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(), Number(timeoutMs || DEFAULT_TIMEOUT_MS));
46
+ try {
47
+ const response = await fetch(`${normalizedBaseUrl}${apiPath}`, {
48
+ method: 'POST',
49
+ headers: {
50
+ 'content-type': 'application/json',
51
+ 'bfx-apikey': String(apiKey || ''),
52
+ 'bfx-nonce': nonce,
53
+ 'bfx-signature': signature,
54
+ },
55
+ body: payload,
56
+ signal: controller.signal,
57
+ });
58
+ const text = await response.text();
59
+ const parsed = text ? safeJsonParse(text) : {};
60
+ const bitfinexApiError =
61
+ Array.isArray(parsed) && String(parsed[0] || '').trim().toLowerCase() === 'error'
62
+ ? {
63
+ code: Number(parsed[1]) || null,
64
+ message: String(parsed[2] || 'Unknown Bitfinex API error'),
65
+ }
66
+ : parsed && typeof parsed === 'object' && String(parsed.event || '').trim().toLowerCase() === 'error'
67
+ ? {
68
+ code: Number(parsed.code) || null,
69
+ message: String(parsed.msg || parsed.message || 'Unknown Bitfinex API error'),
70
+ }
71
+ : null;
72
+ if (!response.ok) {
73
+ throw createCliError(
74
+ 'BITFINEX_PERMISSIONS_PROBE_FAILED',
75
+ `Bitfinex permissions probe failed with HTTP ${response.status}. Verify API key/secret and key validity before onboarding.`,
76
+ { statusCode: response.status }
77
+ );
78
+ }
79
+ if (bitfinexApiError) {
80
+ throw createCliError(
81
+ 'BITFINEX_PERMISSIONS_PROBE_FAILED',
82
+ `Bitfinex permissions probe failed: ${bitfinexApiError.message}. Verify API key/secret and key validity before onboarding.`,
83
+ { statusCode: response.status, bitfinexApiError }
84
+ );
85
+ }
86
+ const scopePermissions = parseBitfinexPermissionRows(parsed);
87
+ const missingScopes = getMissingBitfinexScopes(scopePermissions, permissionProfile);
88
+ if (missingScopes.length > 0) {
89
+ throw createCliError(
90
+ 'BITFINEX_PERMISSION_SCOPE_MISSING',
91
+ `Bitfinex key is missing required scopes (${missingScopes
92
+ .map((entry) => entry.scope)
93
+ .join(', ')}). Enable the ${permissionProfile} profile scopes and retry onboarding.`,
94
+ { missingScopes, scopePermissions, permissionProfile }
95
+ );
96
+ }
97
+ return {
98
+ venue: 'bitfinex',
99
+ permissionProfile,
100
+ checked: true,
101
+ scopes: scopePermissions,
102
+ missingScopes: [],
103
+ };
104
+ } catch (error) {
105
+ if (error?.payload) throw error;
106
+ throw createCliError(
107
+ 'BITFINEX_PERMISSIONS_PROBE_FAILED',
108
+ `Could not verify Bitfinex key permissions. ${String(error?.message || 'Unknown probe error')}`,
109
+ {}
110
+ );
111
+ } finally {
112
+ clearTimeout(timer);
113
+ }
114
+ }
115
+
116
+ return {
117
+ parseBitfinexPermissionRows,
118
+ getMissingBitfinexScopes,
119
+ probeBitfinexPermissions,
120
+ };
121
+ }
@@ -0,0 +1,28 @@
1
+ import { DEFAULT_API_BASE, DEFAULT_TIMEOUT_MS } from '../core/constants.mjs';
2
+ import { loadEnvFile } from '../core/args.mjs';
3
+ import { resolveProfileState } from '../core/context.mjs';
4
+
5
+ export function createInitialRuntimeContext() {
6
+ return {
7
+ apiBase: DEFAULT_API_BASE,
8
+ timeoutMs: DEFAULT_TIMEOUT_MS,
9
+ jsonMode: false,
10
+ verbose: false,
11
+ negotiatedCapabilitiesCache: null,
12
+ };
13
+ }
14
+
15
+ export function applyFlagsToRuntimeContext(context, flags) {
16
+ if (flags.env) loadEnvFile(flags.env);
17
+ context.apiBase = String(flags.apiBase || process.env.THIRDFY_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, '');
18
+ const timeoutMs = Number(flags.timeout || process.env.THIRDFY_CLI_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
19
+ context.timeoutMs = Number.isFinite(timeoutMs) ? timeoutMs : DEFAULT_TIMEOUT_MS;
20
+ context.jsonMode = Boolean(flags.json);
21
+ context.verbose = Boolean(flags.verbose);
22
+ const profileState = resolveProfileState(flags);
23
+ context.profile = profileState.profile;
24
+ context.runMode = profileState.runMode;
25
+ context.custodyMode = profileState.custodyMode;
26
+ context.profileConfigPath = profileState.configPath;
27
+ return context;
28
+ }
@@ -0,0 +1,2 @@
1
+ /** @deprecated Import from `./main.mjs` or `./index.mjs`. Kept for backward-compatible deep imports. */
2
+ export { createThirdfyAgentRuntime } from './main.mjs';
@@ -0,0 +1,3 @@
1
+ export function isCommanderCliError(err) {
2
+ return Boolean(err && typeof err === 'object' && String(err.code || '').startsWith('commander.'));
3
+ }
@@ -0,0 +1,193 @@
1
+ import { parseJsonFlag } from '../../core/args.mjs';
2
+ import { createCliError } from '../../core/envelope.mjs';
3
+ export function createExecutionAmounts() {
4
+ function shouldApplySwapAmountContract(actionName) {
5
+ const action = String(actionName || '').trim().toLowerCase();
6
+ return action === 'swap' || /(^|:|-)swap$/.test(action);
7
+ }
8
+
9
+ function normalizeIntegerRawAmount(input, code = 'AMOUNT_UNIT_INVALID') {
10
+ const raw = String(input ?? '').trim();
11
+ if (!/^\d+$/.test(raw)) {
12
+ throw createCliError(code, 'amountInRaw must be an integer base-unit string (example: "1000000").');
13
+ }
14
+ return raw;
15
+ }
16
+
17
+ function decimalToRawUnits(amountHuman, decimals) {
18
+ const s = String(amountHuman ?? '').trim();
19
+ if (!/^\d+(\.\d+)?$/.test(s)) {
20
+ throw createCliError('AMOUNT_UNIT_INVALID', 'amountInHuman must be a decimal string (example: "100.5").');
21
+ }
22
+ const [whole, frac = ''] = s.split('.');
23
+ if (decimals === 0) {
24
+ if (frac && /[1-9]/.test(frac)) {
25
+ throw createCliError(
26
+ 'AMOUNT_PRECISION_EXCESS',
27
+ 'amountInHuman has fractional digits but tokenInDecimals is 0 (example: use "2" not "2.5").'
28
+ );
29
+ }
30
+ } else if (frac.length > decimals && /[1-9]/.test(frac.slice(decimals))) {
31
+ throw createCliError(
32
+ 'AMOUNT_PRECISION_EXCESS',
33
+ `amountInHuman has more than ${decimals} fractional digit(s) for tokenInDecimals; round or use amountInRaw.`
34
+ );
35
+ }
36
+ const fracNorm = (frac + '0'.repeat(decimals)).slice(0, decimals);
37
+ const wholePart = BigInt(whole || '0') * 10n ** BigInt(decimals);
38
+ const fracPart = BigInt(fracNorm || '0');
39
+ return (wholePart + fracPart).toString();
40
+ }
41
+
42
+ function rawUnitsToDecimalString(amountRaw, decimals) {
43
+ const raw = normalizeIntegerRawAmount(amountRaw);
44
+ const base = 10n ** BigInt(decimals);
45
+ const n = BigInt(raw);
46
+ const whole = n / base;
47
+ const frac = n % base;
48
+ if (decimals === 0 || frac === 0n) return whole.toString();
49
+ const fracText = frac.toString().padStart(decimals, '0').replace(/0+$/, '');
50
+ return `${whole.toString()}.${fracText}`;
51
+ }
52
+
53
+ function normalizeSwapAmountContract(params) {
54
+ const hasRaw = params.amountInRaw !== undefined && params.amountInRaw !== null && String(params.amountInRaw).trim() !== '';
55
+ const hasHuman = params.amountInHuman !== undefined && params.amountInHuman !== null && String(params.amountInHuman).trim() !== '';
56
+ const hasLegacy = params.amountIn !== undefined && params.amountIn !== null && String(params.amountIn).trim() !== '';
57
+ const populated = [hasRaw, hasHuman, hasLegacy].filter(Boolean).length;
58
+ if (populated === 0) {
59
+ throw createCliError(
60
+ 'AMOUNT_UNIT_MISSING',
61
+ 'Missing swap amount: provide one of amountInRaw, amountInHuman, or legacy amountIn.'
62
+ );
63
+ }
64
+ if (populated > 1) {
65
+ throw createCliError(
66
+ 'AMOUNT_UNIT_AMBIGUOUS',
67
+ 'Provide exactly one of amountInRaw, amountInHuman, or legacy amountIn.'
68
+ );
69
+ }
70
+
71
+ if (hasRaw) {
72
+ const amountInRaw = normalizeIntegerRawAmount(params.amountInRaw);
73
+ const hasDecimals =
74
+ params.tokenInDecimals !== undefined &&
75
+ params.tokenInDecimals !== null &&
76
+ String(params.tokenInDecimals).trim() !== '';
77
+ if (!hasDecimals) {
78
+ throw createCliError(
79
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
80
+ 'amountInRaw requires tokenInDecimals (integer 0-36).'
81
+ );
82
+ }
83
+ const decimals = Number(params.tokenInDecimals);
84
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
85
+ throw createCliError(
86
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
87
+ 'amountInRaw requires tokenInDecimals (integer 0-36).'
88
+ );
89
+ }
90
+ const amountInHuman = rawUnitsToDecimalString(amountInRaw, decimals);
91
+ return {
92
+ params: {
93
+ ...params,
94
+ amountInRaw,
95
+ amountIn: amountInHuman,
96
+ tokenInDecimals: decimals,
97
+ },
98
+ swapInput: {
99
+ amountInRaw,
100
+ amountInHuman,
101
+ tokenInDecimals: decimals,
102
+ unitSource: 'raw',
103
+ },
104
+ };
105
+ }
106
+
107
+ if (hasLegacy) {
108
+ const amountInHuman = String(params.amountIn).trim();
109
+ if (!/^\d+(\.\d+)?$/.test(amountInHuman)) {
110
+ throw createCliError(
111
+ 'AMOUNT_UNIT_INVALID',
112
+ 'Legacy swap amountIn must be a decimal token-unit string (example: "1.25"). For base units use amountInRaw with tokenInDecimals.'
113
+ );
114
+ }
115
+ return {
116
+ params: {
117
+ ...params,
118
+ amountIn: amountInHuman,
119
+ },
120
+ swapInput: {
121
+ amountInRaw: null,
122
+ amountInHuman,
123
+ tokenInDecimals: null,
124
+ unitSource: 'legacy_human',
125
+ },
126
+ };
127
+ }
128
+
129
+ const hasDecimals =
130
+ params.tokenInDecimals !== undefined &&
131
+ params.tokenInDecimals !== null &&
132
+ String(params.tokenInDecimals).trim() !== '';
133
+ if (!hasDecimals) {
134
+ throw createCliError(
135
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
136
+ 'amountInHuman requires tokenInDecimals (integer 0-36).'
137
+ );
138
+ }
139
+ const decimals = Number(params.tokenInDecimals);
140
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
141
+ throw createCliError(
142
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
143
+ 'amountInHuman requires tokenInDecimals (integer 0-36).'
144
+ );
145
+ }
146
+ const amountInHuman = String(params.amountInHuman).trim();
147
+ const amountInRaw = decimalToRawUnits(amountInHuman, decimals);
148
+ return {
149
+ params: {
150
+ ...params,
151
+ amountInHuman,
152
+ amountInRaw,
153
+ amountIn: amountInHuman,
154
+ tokenInDecimals: decimals,
155
+ },
156
+ swapInput: {
157
+ amountInRaw,
158
+ amountInHuman,
159
+ tokenInDecimals: decimals,
160
+ unitSource: 'human',
161
+ },
162
+ };
163
+ }
164
+
165
+ function prepareActionParamsForFlags(flags, resolvedAction) {
166
+ const parsedParams = parseJsonFlag(flags, 'params');
167
+ if (!shouldApplySwapAmountContract(resolvedAction)) {
168
+ flags.__preparedParams = parsedParams;
169
+ flags.__swapInput = null;
170
+ return;
171
+ }
172
+ const normalized = normalizeSwapAmountContract(parsedParams);
173
+ flags.__preparedParams = normalized.params;
174
+ flags.__swapInput = normalized.swapInput;
175
+ }
176
+
177
+ function getPreparedParams(flags) {
178
+ if (flags.__preparedParams && typeof flags.__preparedParams === 'object') {
179
+ return flags.__preparedParams;
180
+ }
181
+ return parseJsonFlag(flags, 'params');
182
+ }
183
+
184
+ return {
185
+ shouldApplySwapAmountContract,
186
+ normalizeIntegerRawAmount,
187
+ decimalToRawUnits,
188
+ rawUnitsToDecimalString,
189
+ normalizeSwapAmountContract,
190
+ prepareActionParamsForFlags,
191
+ getPreparedParams,
192
+ };
193
+ }
@@ -0,0 +1,75 @@
1
+ import { createCliError } from '../../core/envelope.mjs';
2
+ export function createExecutionLanes() {
3
+ const OFFCHAIN_VENUE_ORDER_ACTIONS = new Set([
4
+ 'place_hyperliquid_perps_order',
5
+ 'place-hyperliquid-perps-order',
6
+ 'cancel_hyperliquid_perps_order',
7
+ 'cancel-hyperliquid-perps-order',
8
+ 'place_polymarket_order',
9
+ 'place-polymarket-order',
10
+ ]);
11
+ function enforceOffchainVenueLaneCompatibility(action, runMode) {
12
+ const normalizedAction = String(action || '').trim().toLowerCase();
13
+ if (runMode !== 'self' || !OFFCHAIN_VENUE_ORDER_ACTIONS.has(normalizedAction)) return;
14
+ throw createCliError(
15
+ 'OFFCHAIN_VENUE_ORDER_SELF_UNSUPPORTED',
16
+ 'Polymarket CLOB and Hyperliquid perps orders are offchain signed API actions, not EVM build-tx actions. Use self for setup/funding signatures, then run venue orders with --run-mode thirdfy, --run-mode hybrid, or --run-mode agent_wallet.',
17
+ {
18
+ recommendedRunModes: ['thirdfy', 'hybrid', 'agent_wallet'],
19
+ setupOnlyRunMode: 'self',
20
+ }
21
+ );
22
+ }
23
+
24
+ function resolveChainSupport(capabilities, chainId, runMode) {
25
+ const chains = Array.isArray(capabilities?.chains) ? capabilities.chains : [];
26
+ if (!chainId) return { chainId: null, supported: null };
27
+ const match = chains.find((entry) => Number(entry.chainId) === Number(chainId));
28
+ if (!match) {
29
+ return { chainId, supported: false, reason: 'chain_not_advertised_by_api' };
30
+ }
31
+ const lanes = Array.isArray(match.supportedExecutionLanes) ? match.supportedExecutionLanes : [];
32
+ return {
33
+ chainId,
34
+ supported: lanes.includes(runMode),
35
+ runMode,
36
+ chainKey: match.chainKey || null,
37
+ displayName: match.displayName || null,
38
+ };
39
+ }
40
+
41
+ function enforceChainCompatibility(capabilities, flags, runMode) {
42
+ const chainId = Number(flags.chainId || 0) || undefined;
43
+ if (!chainId) return;
44
+ if (!capabilities || !Array.isArray(capabilities.chains)) return;
45
+ const support = resolveChainSupport(capabilities, chainId, runMode);
46
+ if (support?.supported === false) {
47
+ throw new Error(
48
+ `Chain ${chainId} is not advertised as compatible with run-mode=${runMode}. Check \`thirdfy-agent actions --chain-id ${chainId}\`.`
49
+ );
50
+ }
51
+ }
52
+
53
+ function shouldUseBuildTxExecution(runMode, resolved) {
54
+ if (runMode !== 'self' && runMode !== 'hybrid') return false;
55
+ const meta = resolved?.resolvedActionMeta || null;
56
+ if (!meta || typeof meta !== 'object') return true;
57
+ if (meta.supportsBuildTx === false) return false;
58
+ return true;
59
+ }
60
+
61
+ function shouldUseBuildTxPreflight(runMode, resolved) {
62
+ if (runMode !== 'self') return false;
63
+ const meta = resolved?.resolvedActionMeta || null;
64
+ if (!meta || typeof meta !== 'object') return false;
65
+ return meta.supportsExecuteIntent === false && meta.supportsBuildTx === true;
66
+ }
67
+
68
+ return {
69
+ enforceOffchainVenueLaneCompatibility,
70
+ resolveChainSupport,
71
+ enforceChainCompatibility,
72
+ shouldUseBuildTxExecution,
73
+ shouldUseBuildTxPreflight,
74
+ };
75
+ }
@@ -0,0 +1,148 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { requireFlag, parseJsonFlag } from '../../core/args.mjs';
3
+ import { loadProfileConfig } from '../../core/context.mjs';
4
+ import { createCliError } from '../../core/envelope.mjs';
5
+ import { normalizeRunMode, normalizeManagedRunMode, normalizeHybridWalletMode } from '../../core/runMode.mjs';
6
+ export function createExecutionPayloads({ getPreparedParams, rawUnitsToDecimalString }) {
7
+ function buildBuildTxPayload(flags, resolvedAction) {
8
+ const runMode = String(flags.runMode || '').trim().toLowerCase() || 'self';
9
+ const payload = {
10
+ agentApiKey: resolveAgentApiKey(flags, runMode),
11
+ action: String(resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
12
+ params: getPreparedParams(flags),
13
+ };
14
+ if (flags.chainId) payload.chainId = Number(flags.chainId);
15
+ if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
16
+ return payload;
17
+ }
18
+
19
+ function resolveEffectiveUserDid(flags) {
20
+ const explicit = String(flags.userDid || '').trim();
21
+ if (explicit) return explicit;
22
+ const envUserDid = String(process.env.THIRDFY_USER_DID || '').trim();
23
+ if (envUserDid) return envUserDid;
24
+ const config = loadProfileConfig();
25
+ return String(config?.wallets?.userDid || config?.identity?.userDid || '').trim();
26
+ }
27
+
28
+ function buildManagedExecutePayload(flags, options) {
29
+ const runMode = String(options.runMode || flags.runMode || 'agent_wallet').trim().toLowerCase();
30
+ const params = getPreparedParams(flags);
31
+ const managedParams = { ...params };
32
+ if (flags.__swapInput?.unitSource === 'human' && flags.__swapInput.amountInHuman) {
33
+ delete managedParams.amountInRaw;
34
+ delete managedParams.amountInHuman;
35
+ delete managedParams.tokenInDecimals;
36
+ managedParams.amountIn = flags.__swapInput.amountInHuman;
37
+ } else if (flags.__swapInput?.unitSource === 'raw' && flags.__swapInput.amountInRaw) {
38
+ const decimals = flags.__swapInput.tokenInDecimals;
39
+ delete managedParams.amountInHuman;
40
+ delete managedParams.amountInRaw;
41
+ delete managedParams.tokenInDecimals;
42
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
43
+ throw createCliError(
44
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
45
+ 'amountInRaw requires tokenInDecimals (integer 0-36).'
46
+ );
47
+ }
48
+ managedParams.amountIn = rawUnitsToDecimalString(flags.__swapInput.amountInRaw, decimals);
49
+ } else if (flags.__swapInput?.unitSource === 'legacy_human' && flags.__swapInput.amountInHuman) {
50
+ delete managedParams.amountInRaw;
51
+ delete managedParams.amountInHuman;
52
+ delete managedParams.tokenInDecimals;
53
+ managedParams.amountIn = flags.__swapInput.amountInHuman;
54
+ }
55
+ const payload = {
56
+ agentApiKey: resolveAgentApiKey(flags, runMode),
57
+ action: String(options.resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
58
+ userDid: resolveEffectiveUserDid(flags),
59
+ params: managedParams,
60
+ chainId: Number(flags.chainId || 8453),
61
+ executionStrategy: {
62
+ runMode,
63
+ mirrorOnly: false,
64
+ },
65
+ };
66
+ if (!payload.userDid) {
67
+ throw createCliError(
68
+ 'MISSING_USER_DID',
69
+ 'Managed wallet execution requires --user-did, THIRDFY_USER_DID, or a saved userDid from `thirdfy-agent login email`.'
70
+ );
71
+ }
72
+ if (flags.estimatedAmountUsd) payload.estimatedAmountUsd = Number(flags.estimatedAmountUsd);
73
+ if (options.forceIdempotency) {
74
+ payload.executionIdempotencyKey = String(
75
+ flags.idempotencyKey || `cli:walletExecute:${payload.action}:${Date.now()}:${randomUUID()}`
76
+ );
77
+ } else if (flags.idempotencyKey) {
78
+ payload.executionIdempotencyKey = String(flags.idempotencyKey);
79
+ }
80
+ return payload;
81
+ }
82
+
83
+ function resolveTokenInForFunding(params) {
84
+ if (!params || typeof params !== 'object') return '';
85
+ const tokenIn = String(params.tokenIn || params.tokenAddress || params.token || '').trim();
86
+ return tokenIn;
87
+ }
88
+
89
+ function buildIntentPayload(flags, options) {
90
+ const runMode = String(options.runMode || flags.runMode || '').trim().toLowerCase() || 'thirdfy';
91
+ const payload = {
92
+ agentApiKey: resolveAgentApiKey(flags, runMode),
93
+ action: String(options.resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
94
+ params: getPreparedParams(flags),
95
+ };
96
+ if (flags.catalog) payload.catalog = String(flags.catalog).trim();
97
+ if (flags.chainId) payload.chainId = Number(flags.chainId);
98
+ if (flags.estimatedAmountUsd) payload.estimatedAmountUsd = Number(flags.estimatedAmountUsd);
99
+ const userDid = resolveEffectiveUserDid(flags);
100
+ if (flags.userDid || (options.validationOnly && (runMode === 'agent_wallet' || (runMode === 'hybrid' && options.mirrorOnly)))) {
101
+ if (userDid) payload.userDid = userDid;
102
+ }
103
+ if (options.validationOnly) payload.executionLane = 'validation_only';
104
+ if (flags.executionScope) {
105
+ payload.executionScope = String(flags.executionScope).trim();
106
+ } else if (options.validationOnly && (runMode === 'agent_wallet' || (runMode === 'hybrid' && options.mirrorOnly))) {
107
+ payload.executionScope = 'solo_owner_mirror';
108
+ }
109
+ if (options.runMode) {
110
+ payload.executionStrategy = {
111
+ runMode: String(options.runMode),
112
+ mirrorOnly: Boolean(options.mirrorOnly),
113
+ ...(options.hybridWalletMode ? { hybridWalletMode: String(options.hybridWalletMode) } : {}),
114
+ };
115
+ }
116
+ if (options.forceIdempotency) {
117
+ payload.idempotencyKey = String(flags.idempotencyKey || `cli:${payload.action}:${Date.now()}:${randomUUID()}`);
118
+ } else if (flags.idempotencyKey) {
119
+ payload.idempotencyKey = String(flags.idempotencyKey);
120
+ }
121
+ return payload;
122
+ }
123
+
124
+ function resolveAgentApiKey(flags, runMode) {
125
+ const explicit = String(flags.agentApiKey || '').trim();
126
+ if (explicit) return explicit;
127
+ const envKey = String(process.env.THIRDFY_AGENT_API_KEY || '').trim();
128
+ if (envKey) return envKey;
129
+ const config = loadProfileConfig();
130
+ const configKey = String(config?.agent?.apiKey || '').trim();
131
+ if (configKey) return configKey;
132
+ if (runMode === 'self') {
133
+ throw new Error(
134
+ 'SELF_OPEN_DISABLED: self mode without agent identity is not enabled on this API yet. Set --agent-api-key, THIRDFY_AGENT_API_KEY, or login/config default.'
135
+ );
136
+ }
137
+ throw new Error('Missing --agent-api-key (or THIRDFY_AGENT_API_KEY / config.agent.apiKey)');
138
+ }
139
+
140
+ return {
141
+ buildBuildTxPayload,
142
+ resolveEffectiveUserDid,
143
+ buildManagedExecutePayload,
144
+ resolveTokenInForFunding,
145
+ buildIntentPayload,
146
+ resolveAgentApiKey,
147
+ };
148
+ }