@thirdfy/agent-cli 0.1.8 → 0.1.10

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,15 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.9] - 2026-04-08
8
+
9
+ **npm:** [`@thirdfy/agent-cli@0.1.9`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.9)
10
+ **Git tag:** [`v0.1.9`](https://github.com/thirdfy/agent-cli/releases/tag/v0.1.9)
11
+
12
+ ### Fixed
13
+
14
+ - **Self preflight routing for swap-like actions:** `preflight --run-mode self` now selects the validation route by action capability. When an action advertises `supportsExecuteIntent=false` and `supportsBuildTx=true`, preflight uses `build-tx` directly instead of forcing execute-intent validation.
15
+
7
16
  ## [0.1.8] - 2026-04-07
8
17
 
9
18
  **npm:** [`@thirdfy/agent-cli@0.1.8`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.8)
package/README.md CHANGED
@@ -16,8 +16,13 @@ If you want the full developer docs, start here:
16
16
  - [API Reference home](https://docs.thirdfy.com/api)
17
17
  - [LLM docs map (llms.txt)](https://docs.thirdfy.com/llms.txt)
18
18
  - [LLM full API snapshot (llms-full.txt)](https://docs.thirdfy.com/llms-full.txt)
19
+ - [Provider docs index (repo)](./docs/providers/index.md)
20
+ - [Action inventory and compatibility](./docs/action-inventory-and-compatibility.md)
21
+ - [Command reference](./docs/command-reference.md)
19
22
  - [DogeOS quickstart (repo)](./docs/dogeos-quickstart.md)
23
+ - [Bitfinex CEX workflow (repo)](./docs/bitfinex-cex-workflow.md)
20
24
  - [Action onboarding standard](./docs/action-onboarding-standard.md)
25
+ - [Provider docs governance model](./docs/provider-docs-governance.md)
21
26
  - [Skill integration RFC](./docs/skill-integration-rfc.md)
22
27
 
23
28
  ## Install
@@ -33,6 +38,13 @@ Run without global install:
33
38
  npx @thirdfy/agent-cli --help
34
39
  ```
35
40
 
41
+ ## What's new in 0.1.8
42
+
43
+ - Managed custodial lifecycle parity validation is now first-class in CLI E2E/certification.
44
+ - `doctor self` runs as a local diagnostic command without capabilities negotiation.
45
+ - Self-exec effect-check fallback payloads are hardened for deterministic automation parsing.
46
+ - Managed wallet aliases (`managed wallet init/grant`) are wired directly to custodial delegation handlers.
47
+
36
48
  ## Quick start
37
49
 
38
50
  ```bash
@@ -101,6 +113,9 @@ thirdfy-agent agent register --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN
101
113
  - `run` auto-generates `idempotencyKey` by default when not provided.
102
114
  - For deterministic retries across workers/restarts, pass your own stable `--idempotency-key`.
103
115
  - `preflight` does not force idempotency keys.
116
+ - `preflight --run-mode self` is capability-aware:
117
+ - if an action supports execute-intent validation, CLI uses `executionLane=validation_only` before building tx;
118
+ - if an action advertises `supportsExecuteIntent=false` and `supportsBuildTx=true`, CLI routes preflight directly to `build-tx`.
104
119
 
105
120
  ## Run modes and profiles
106
121
 
@@ -110,6 +125,12 @@ thirdfy-agent agent register --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN
110
125
  - `--run-mode self` -> self-custody rail (`build-tx` flow; default for `profile=personal`)
111
126
  - `--run-mode hybrid` -> self-custody + governance mirror metadata (default for `profile=builder`)
112
127
 
128
+ | Goal | Recommended mode | Why |
129
+ | --- | --- | --- |
130
+ | BYOW signing and local custody | `self` | Keeps signing and broadcast on your wallet path. |
131
+ | Managed delegated execution | `thirdfy` | Uses delegated governance rail for hosted execution. |
132
+ | Self execution plus governance mirror checks | `hybrid` | Preserves local execution while emitting managed-lane mirrors. |
133
+
113
134
  Auth expectations by mode:
114
135
 
115
136
  - `self`: requires execution identity (`agentApiKey`) today; keyless self lane is blocked with deterministic `SELF_OPEN_DISABLED`.
@@ -199,6 +220,14 @@ Jeff-friendly aliases map directly to core commands:
199
220
  - `thirdfy-agent jeff status --intent-id ...` -> alias of `intent-status`
200
221
  - `thirdfy-agent prompt "<text>"` -> captures prompt intent and suggests deterministic execution commands
201
222
 
223
+ ## Provider-specific workflows
224
+
225
+ Provider and venue guides are kept outside `README` so this page stays stable as integrations grow.
226
+
227
+ - Provider index: [`docs/providers/index.md`](./docs/providers/index.md)
228
+ - Bitfinex runbook: [`docs/providers/bitfinex.md`](./docs/providers/bitfinex.md)
229
+ - DogeOS runbook: [`docs/providers/dogeos.md`](./docs/providers/dogeos.md)
230
+
202
231
  ## Command groups
203
232
 
204
233
  - Discovery: `catalogs list`, `actions`
@@ -210,6 +239,12 @@ Jeff-friendly aliases map directly to core commands:
210
239
  - Delegation execution helpers: `delegation balance`, `delegation redeem`
211
240
  - Account: `credits balance`
212
241
 
242
+ ## Where to find what is supported
243
+
244
+ - Full command groups and usage map: [`docs/command-reference.md`](./docs/command-reference.md)
245
+ - Live action inventory in your environment: [`docs/action-inventory-and-compatibility.md`](./docs/action-inventory-and-compatibility.md)
246
+ - Provider runbooks and provider status: [`docs/providers/index.md`](./docs/providers/index.md)
247
+
213
248
  ## Delegation lifecycle (canonical)
214
249
 
215
250
  Use `create` + `activate` as the default delegated execution lifecycle:
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { randomUUID } from 'crypto';
3
+ import { createHmac, randomUUID } from 'crypto';
4
4
  import { spawnSync } from 'child_process';
5
5
  import fs from 'fs';
6
6
  import os from 'os';
@@ -18,6 +18,11 @@ const {
18
18
  const DEFAULT_API_BASE = 'https://api.thirdfy.com';
19
19
  const DEFAULT_TIMEOUT_MS = 30000;
20
20
  const DEFAULT_CATALOG_CACHE_TTL_MS = 60_000;
21
+ const DEFAULT_BITFINEX_BASE_URL = 'https://api.bitfinex.com';
22
+ const BITFINEX_REQUIRED_SCOPE_PERMISSIONS = {
23
+ funding: { read: 1, write: 1 },
24
+ wallets: { read: 1, write: 1 },
25
+ };
21
26
  const RUN_MODES = ['thirdfy', 'self', 'hybrid'];
22
27
  const EFFECT_CHECK_MODES = ['strict', 'relaxed', 'off'];
23
28
  const PROFILE_DEFAULTS = {
@@ -52,6 +57,7 @@ main().catch((error) => {
52
57
  const payload = error?.payload && typeof error.payload === 'object' ? error.payload : {};
53
58
  const blockedReason = String(payload.blockedReason || payload.error || '').trim() || undefined;
54
59
  const blockedStage = String(payload.blockedStage || '').trim() || undefined;
60
+ const onboardingHint = buildTopLevelOnboardingHint(payload);
55
61
  printEnvelope({
56
62
  success: false,
57
63
  code: statusCode ? 'API_REQUEST_FAILED' : 'CLI_UNHANDLED_ERROR',
@@ -60,6 +66,7 @@ main().catch((error) => {
60
66
  statusCode,
61
67
  blockedReason,
62
68
  blockedStage,
69
+ onboardingHint,
63
70
  payload,
64
71
  },
65
72
  meta: { version: CLI_VERSION, apiBase: context.apiBase },
@@ -67,6 +74,22 @@ main().catch((error) => {
67
74
  process.exit(1);
68
75
  });
69
76
 
77
+ function buildTopLevelOnboardingHint(payload) {
78
+ const reason = String(payload?.blockedReason || payload?.error || '').trim();
79
+ if (!reason) return null;
80
+ if (
81
+ ['CEX_CREDENTIAL_NOT_FOUND', 'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT', 'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED'].includes(
82
+ reason
83
+ )
84
+ ) {
85
+ return {
86
+ type: 'bitfinex_credentials',
87
+ command: buildBitfinexOnboardingCommand(),
88
+ };
89
+ }
90
+ return null;
91
+ }
92
+
70
93
  async function main() {
71
94
  const profileState = resolveProfileState(globalFlags);
72
95
  context.profile = profileState.profile;
@@ -517,11 +540,16 @@ async function commandPreflight(ctx, flags, capabilities) {
517
540
  prepareActionParamsForFlags(flags, resolved.resolvedAction);
518
541
  const runMode = getEffectiveRunMode(ctx, flags);
519
542
  enforceChainCompatibility(capabilities, flags, runMode);
520
- const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved.resolvedAction);
521
- const normalized = backendResult.normalized;
543
+ const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved);
544
+ const normalized = applyExecutionFallbackHints(backendResult.normalized, {
545
+ flags,
546
+ runMode,
547
+ resolvedAction: resolved.resolvedAction,
548
+ });
549
+ const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
522
550
  printEnvelope({
523
551
  success: normalized.success,
524
- code: normalized.success ? 'PREFLIGHT_OK' : 'PREFLIGHT_FAILED',
552
+ code: normalized.success ? 'PREFLIGHT_OK' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'PREFLIGHT_FAILED',
525
553
  message: backendResult.message,
526
554
  data: normalized,
527
555
  meta: {
@@ -529,6 +557,7 @@ async function commandPreflight(ctx, flags, capabilities) {
529
557
  catalog: flags.catalog || null,
530
558
  requestedAction: resolved.requestedAction,
531
559
  resolvedAction: resolved.resolvedAction,
560
+ preflightRoute: backendResult.route,
532
561
  runMode,
533
562
  profile: ctx.profile || null,
534
563
  swapInput: flags.__swapInput || null,
@@ -547,7 +576,11 @@ async function commandRun(ctx, flags, capabilities) {
547
576
  const backendResult = await runExecutionByMode(runMode, ctx, flags, resolved, {
548
577
  skipPreflight,
549
578
  });
550
- const normalized = backendResult.normalized;
579
+ const normalized = applyExecutionFallbackHints(backendResult.normalized, {
580
+ flags,
581
+ runMode,
582
+ resolvedAction: resolved.resolvedAction,
583
+ });
551
584
  if (runMode === 'self' && parseBooleanFlag(flags.broadcast, false) && normalized?.success) {
552
585
  const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, normalized);
553
586
  printEnvelope({
@@ -924,8 +957,9 @@ async function commandDelegationRedeem(ctx, flags, capabilities) {
924
957
 
925
958
  async function commandCredentialsUpsert(ctx, flags, capabilities) {
926
959
  const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials upsert');
960
+ const venue = requireFlag(flags, 'venue', 'Missing --venue');
927
961
  const payload = {
928
- venue: requireFlag(flags, 'venue', 'Missing --venue'),
962
+ venue,
929
963
  apiKey: requireFlag(flags, 'apiKey', 'Missing --api-key'),
930
964
  apiSecret: requireFlag(flags, 'apiSecret', 'Missing --api-secret'),
931
965
  credentialType: String(flags.credentialType || 'api_key'),
@@ -936,6 +970,19 @@ async function commandCredentialsUpsert(ctx, flags, capabilities) {
936
970
  if (flags.policy) payload.policy = parseJsonFlag(flags, 'policy');
937
971
  if (flags.agentKey) payload.agentKey = String(flags.agentKey).trim();
938
972
  if (flags.chainId) payload.chainId = Number(flags.chainId);
973
+ const probeEnabled =
974
+ String(venue).trim().toLowerCase() === 'bitfinex' &&
975
+ !parseBooleanFlag(flags.skipPermissionsProbe, false) &&
976
+ parseBooleanFlag(flags.verifyPermissions, true);
977
+ let permissionProbe = null;
978
+ if (probeEnabled) {
979
+ permissionProbe = await probeBitfinexPermissions({
980
+ apiKey: payload.apiKey,
981
+ apiSecret: payload.apiSecret,
982
+ timeoutMs: ctx.timeoutMs,
983
+ baseUrl: String(flags.bitfinexBaseUrl || process.env.BITFINEX_BASE_URL || DEFAULT_BITFINEX_BASE_URL),
984
+ });
985
+ }
939
986
 
940
987
  const response = await requestWithRouteFallback(ctx, {
941
988
  method: 'POST',
@@ -952,6 +999,7 @@ async function commandCredentialsUpsert(ctx, flags, capabilities) {
952
999
  data: response,
953
1000
  meta: {
954
1001
  apiBase: ctx.apiBase,
1002
+ permissionProbe,
955
1003
  capabilitiesVersion: capabilities?.contractVersion || null,
956
1004
  },
957
1005
  });
@@ -1710,13 +1758,82 @@ function enforceChainCompatibility(capabilities, flags, runMode) {
1710
1758
  }
1711
1759
  }
1712
1760
 
1713
- async function runPreflightByMode(runMode, ctx, flags, resolvedAction) {
1761
+ function shouldUseBuildTxExecution(runMode, resolved) {
1762
+ if (runMode !== 'self' && runMode !== 'hybrid') return false;
1763
+ const meta = resolved?.resolvedActionMeta || null;
1764
+ if (!meta || typeof meta !== 'object') return true;
1765
+ if (meta.supportsBuildTx === false) return false;
1766
+ return true;
1767
+ }
1768
+
1769
+ function buildBitfinexOnboardingCommand() {
1770
+ return [
1771
+ 'thirdfy-agent credentials upsert',
1772
+ '--auth-token "$THIRDFY_AUTH_TOKEN"',
1773
+ '--venue bitfinex',
1774
+ '--api-key "<BITFINEX_API_KEY>"',
1775
+ '--api-secret "<BITFINEX_API_SECRET>"',
1776
+ '--verify-permissions',
1777
+ '--json',
1778
+ ].join(' ');
1779
+ }
1780
+
1781
+ function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedAction }) {
1782
+ if (!normalized || typeof normalized !== 'object') return normalized;
1783
+ const next = { ...normalized };
1784
+ const reasons = new Set();
1785
+ const topBlockedReason = String(next.blockedReason || '').trim();
1786
+ if (topBlockedReason) reasons.add(topBlockedReason);
1787
+ const blockedByReason = next.blockedByReason && typeof next.blockedByReason === 'object' ? next.blockedByReason : {};
1788
+ for (const key of Object.keys(blockedByReason)) {
1789
+ if (String(key || '').trim()) reasons.add(String(key).trim());
1790
+ }
1791
+ const resolvedActionLower = String(resolvedAction || '').trim().toLowerCase();
1792
+ const looksBitfinexAction = resolvedActionLower.includes('bitfinex');
1793
+ const cexCredentialMissingSignals = new Set([
1794
+ 'CEX_CREDENTIAL_NOT_FOUND',
1795
+ 'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT',
1796
+ 'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED',
1797
+ ]);
1798
+ const hasMissingCredentialSignal = Array.from(reasons).some((code) => cexCredentialMissingSignals.has(code));
1799
+ if (looksBitfinexAction && hasMissingCredentialSignal) {
1800
+ next.onboardingHint = {
1801
+ type: 'bitfinex_credentials',
1802
+ message:
1803
+ 'Bitfinex credentials are missing for this delegated account. Onboard credentials first, then retry the same command.',
1804
+ command: buildBitfinexOnboardingCommand(),
1805
+ };
1806
+ }
1807
+ if (
1808
+ looksBitfinexAction &&
1809
+ runMode !== 'thirdfy' &&
1810
+ (topBlockedReason === 'SELF_MODE_REQUIRES_BUILD_TX' || topBlockedReason === 'HYBRID_MODE_REQUIRES_BUILD_TX')
1811
+ ) {
1812
+ next.onboardingHint = {
1813
+ ...(next.onboardingHint || {}),
1814
+ runMode: 'thirdfy',
1815
+ runModeMessage: 'Bitfinex actions require delegated execute-intent rail. Re-run with --run-mode thirdfy.',
1816
+ };
1817
+ }
1818
+ return next;
1819
+ }
1820
+
1821
+ function shouldUseBuildTxPreflight(runMode, resolved) {
1822
+ if (runMode !== 'self') return false;
1823
+ const meta = resolved?.resolvedActionMeta || null;
1824
+ if (!meta || typeof meta !== 'object') return false;
1825
+ return meta.supportsExecuteIntent === false && meta.supportsBuildTx === true;
1826
+ }
1827
+
1828
+ async function runPreflightByMode(runMode, ctx, flags, resolved) {
1829
+ const resolvedAction = resolved.resolvedAction;
1714
1830
  if (runMode === 'self') {
1831
+ const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
1715
1832
  const normalized = await executeSelfRun(
1716
1833
  ctx,
1717
1834
  flags,
1718
- { resolvedAction },
1719
- { skipPreflight: false }
1835
+ resolved,
1836
+ { skipPreflight: route === 'build_tx' }
1720
1837
  );
1721
1838
  const blockedByGovernance = !normalized.success && Boolean(normalized.preflightBlocked);
1722
1839
  return {
@@ -1725,6 +1842,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolvedAction) {
1725
1842
  : normalized.success
1726
1843
  ? 'Self preflight completed (unsigned transaction ready)'
1727
1844
  : 'Self preflight failed',
1845
+ route,
1728
1846
  normalized,
1729
1847
  };
1730
1848
  }
@@ -1745,6 +1863,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolvedAction) {
1745
1863
  : runMode === 'hybrid'
1746
1864
  ? 'Hybrid mirror preflight failed'
1747
1865
  : 'Preflight failed',
1866
+ route: 'execute_intent_validation',
1748
1867
  normalized,
1749
1868
  };
1750
1869
  }
@@ -1831,6 +1950,7 @@ async function executeThirdfyRun(ctx, flags, resolved, options) {
1831
1950
 
1832
1951
  async function executeSelfRun(ctx, flags, resolved, options) {
1833
1952
  const skipPreflight = Boolean(options?.skipPreflight);
1953
+ const blockedReasonCode = String(options?.blockedReasonCode || 'SELF_MODE_REQUIRES_BUILD_TX');
1834
1954
  if (!skipPreflight) {
1835
1955
  const preflightPayload = buildIntentPayload(flags, {
1836
1956
  validationOnly: true,
@@ -1849,6 +1969,17 @@ async function executeSelfRun(ctx, flags, resolved, options) {
1849
1969
  return preflightNormalized;
1850
1970
  }
1851
1971
  }
1972
+ if (!shouldUseBuildTxExecution(options?.runMode || 'self', resolved)) {
1973
+ const normalized = normalizeIntentResponse({
1974
+ success: false,
1975
+ status: 'failed',
1976
+ blockedReason: blockedReasonCode,
1977
+ blockedStage: 'routing',
1978
+ error: 'Selected action does not support build-tx self lane. Use --run-mode thirdfy for delegated CEX execution.',
1979
+ });
1980
+ normalized.preflightBlocked = true;
1981
+ return normalized;
1982
+ }
1852
1983
  const buildTxPayload = buildBuildTxPayload(flags, resolved.resolvedAction);
1853
1984
  const buildTx = await apiPost(ctx, '/api/v1/agent/build-tx', buildTxPayload);
1854
1985
  return normalizeIntentResponse({
@@ -1861,7 +1992,11 @@ async function executeSelfRun(ctx, flags, resolved, options) {
1861
1992
  }
1862
1993
 
1863
1994
  async function executeHybridRun(ctx, flags, resolved, options) {
1864
- const selfResult = await executeSelfRun(ctx, flags, resolved, options);
1995
+ const selfResult = await executeSelfRun(ctx, flags, resolved, {
1996
+ ...(options || {}),
1997
+ runMode: 'hybrid',
1998
+ blockedReasonCode: 'HYBRID_MODE_REQUIRES_BUILD_TX',
1999
+ });
1865
2000
  if (!selfResult.success) return selfResult;
1866
2001
 
1867
2002
  const mirrorPayload = buildIntentPayload(flags, {
@@ -2006,18 +2141,19 @@ async function resolveActionSelection(ctx, flags) {
2006
2141
  .map((action) => ({
2007
2142
  key: getActionKey(action),
2008
2143
  aliases: getActionAliases(action),
2144
+ action,
2009
2145
  }))
2010
2146
  .filter((entry) => entry.key);
2011
2147
 
2012
2148
  if (!keyed.length) {
2013
- return { requestedAction, resolvedAction: requestedAction };
2149
+ return { requestedAction, resolvedAction: requestedAction, resolvedActionMeta: null };
2014
2150
  }
2015
2151
 
2016
2152
  // Prefer direct action id matches over alias matches.
2017
2153
  // This keeps UX simple for commands like `--action swap` when another action aliases to "swap".
2018
2154
  const exactKey = keyed.filter((entry) => entry.key.toLowerCase() === requestedLower);
2019
2155
  if (exactKey.length === 1) {
2020
- return { requestedAction, resolvedAction: exactKey[0].key };
2156
+ return { requestedAction, resolvedAction: exactKey[0].key, resolvedActionMeta: exactKey[0].action || null };
2021
2157
  }
2022
2158
  if (exactKey.length > 1) {
2023
2159
  throw new Error(
@@ -2029,7 +2165,7 @@ async function resolveActionSelection(ctx, flags) {
2029
2165
 
2030
2166
  const exactAlias = keyed.filter((entry) => entry.aliases.some((a) => a.toLowerCase() === requestedLower));
2031
2167
  if (exactAlias.length === 1) {
2032
- return { requestedAction, resolvedAction: exactAlias[0].key };
2168
+ return { requestedAction, resolvedAction: exactAlias[0].key, resolvedActionMeta: exactAlias[0].action || null };
2033
2169
  }
2034
2170
  if (exactAlias.length > 1) {
2035
2171
  throw new Error(
@@ -2045,7 +2181,7 @@ async function resolveActionSelection(ctx, flags) {
2045
2181
  entry.aliases.some((a) => a.toLowerCase().includes(requestedLower))
2046
2182
  );
2047
2183
  if (fuzzy.length === 1) {
2048
- return { requestedAction, resolvedAction: fuzzy[0].key };
2184
+ return { requestedAction, resolvedAction: fuzzy[0].key, resolvedActionMeta: fuzzy[0].action || null };
2049
2185
  }
2050
2186
  if (fuzzy.length > 1) {
2051
2187
  throw new Error(
@@ -2441,6 +2577,114 @@ function safeJsonParse(text) {
2441
2577
  }
2442
2578
  }
2443
2579
 
2580
+ function parseBitfinexPermissionRows(rawData) {
2581
+ const rows = Array.isArray(rawData) ? rawData : [];
2582
+ const parsed = {};
2583
+ for (const row of rows) {
2584
+ if (!Array.isArray(row) || row.length < 3) continue;
2585
+ const scope = String(row[0] || '').trim().toLowerCase();
2586
+ if (!scope) continue;
2587
+ const read = Number(row[1]) === 1 ? 1 : 0;
2588
+ const write = Number(row[2]) === 1 ? 1 : 0;
2589
+ parsed[scope] = { read, write };
2590
+ }
2591
+ return parsed;
2592
+ }
2593
+
2594
+ function getMissingBitfinexScopes(scopePermissions) {
2595
+ const missing = [];
2596
+ for (const [scope, required] of Object.entries(BITFINEX_REQUIRED_SCOPE_PERMISSIONS)) {
2597
+ const actual = scopePermissions[scope] || { read: 0, write: 0 };
2598
+ if (actual.read < required.read || actual.write < required.write) {
2599
+ missing.push({
2600
+ scope,
2601
+ required,
2602
+ actual,
2603
+ });
2604
+ }
2605
+ }
2606
+ return missing;
2607
+ }
2608
+
2609
+ async function probeBitfinexPermissions({ apiKey, apiSecret, timeoutMs, baseUrl }) {
2610
+ const normalizedBaseUrl = String(baseUrl || DEFAULT_BITFINEX_BASE_URL).trim().replace(/\/+$/, '');
2611
+ const apiPath = '/v2/auth/r/permissions';
2612
+ const nonce = Date.now().toString();
2613
+ const payload = '{}';
2614
+ const signature = createHmac('sha384', String(apiSecret || ''))
2615
+ .update(`/api${apiPath}${nonce}${payload}`)
2616
+ .digest('hex');
2617
+ const controller = new AbortController();
2618
+ const timer = setTimeout(() => controller.abort(), Number(timeoutMs || DEFAULT_TIMEOUT_MS));
2619
+ try {
2620
+ const response = await fetch(`${normalizedBaseUrl}${apiPath}`, {
2621
+ method: 'POST',
2622
+ headers: {
2623
+ 'content-type': 'application/json',
2624
+ 'bfx-apikey': String(apiKey || ''),
2625
+ 'bfx-nonce': nonce,
2626
+ 'bfx-signature': signature,
2627
+ },
2628
+ body: payload,
2629
+ signal: controller.signal,
2630
+ });
2631
+ const text = await response.text();
2632
+ const parsed = text ? safeJsonParse(text) : {};
2633
+ const bitfinexApiError =
2634
+ Array.isArray(parsed) && String(parsed[0] || '').trim().toLowerCase() === 'error'
2635
+ ? {
2636
+ code: Number(parsed[1]) || null,
2637
+ message: String(parsed[2] || 'Unknown Bitfinex API error'),
2638
+ }
2639
+ : parsed && typeof parsed === 'object' && String(parsed.event || '').trim().toLowerCase() === 'error'
2640
+ ? {
2641
+ code: Number(parsed.code) || null,
2642
+ message: String(parsed.msg || parsed.message || 'Unknown Bitfinex API error'),
2643
+ }
2644
+ : null;
2645
+ if (!response.ok) {
2646
+ throw createCliError(
2647
+ 'BITFINEX_PERMISSIONS_PROBE_FAILED',
2648
+ `Bitfinex permissions probe failed with HTTP ${response.status}. Verify API key/secret and key validity before onboarding.`,
2649
+ { statusCode: response.status }
2650
+ );
2651
+ }
2652
+ if (bitfinexApiError) {
2653
+ throw createCliError(
2654
+ 'BITFINEX_PERMISSIONS_PROBE_FAILED',
2655
+ `Bitfinex permissions probe failed: ${bitfinexApiError.message}. Verify API key/secret and key validity before onboarding.`,
2656
+ { statusCode: response.status, bitfinexApiError }
2657
+ );
2658
+ }
2659
+ const scopePermissions = parseBitfinexPermissionRows(parsed);
2660
+ const missingScopes = getMissingBitfinexScopes(scopePermissions);
2661
+ if (missingScopes.length > 0) {
2662
+ throw createCliError(
2663
+ 'BITFINEX_PERMISSION_SCOPE_MISSING',
2664
+ `Bitfinex key is missing required scopes (${missingScopes
2665
+ .map((entry) => entry.scope)
2666
+ .join(', ')}). Enable funding+wallets read/write and retry onboarding.`,
2667
+ { missingScopes, scopePermissions }
2668
+ );
2669
+ }
2670
+ return {
2671
+ venue: 'bitfinex',
2672
+ checked: true,
2673
+ scopes: scopePermissions,
2674
+ missingScopes: [],
2675
+ };
2676
+ } catch (error) {
2677
+ if (error?.payload) throw error;
2678
+ throw createCliError(
2679
+ 'BITFINEX_PERMISSIONS_PROBE_FAILED',
2680
+ `Could not verify Bitfinex key permissions. ${String(error?.message || 'Unknown probe error')}`,
2681
+ {}
2682
+ );
2683
+ } finally {
2684
+ clearTimeout(timer);
2685
+ }
2686
+ }
2687
+
2444
2688
  function printEnvelope(payload) {
2445
2689
  if (jsonMode) {
2446
2690
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
@@ -2478,7 +2722,7 @@ Core commands:
2478
2722
  thirdfy-agent managed wallet init [--auth-token <token> | --owner-session-token <token>] [--chain-id <id>] [--json]
2479
2723
  thirdfy-agent managed wallet grant [--auth-token <token> | --owner-session-token <token>] --agent-key <key> --token-address <addr> --max-usd-per-day <usd> [--chain-id <id>] [--period-duration <sec>] [--expiry-seconds <sec>] [--json]
2480
2724
  thirdfy-agent managed run --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--idempotency-key <key>] [--json]
2481
- thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
2725
+ thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--verify-permissions] [--skip-permissions-probe] [--bitfinex-base-url <url>] [--json]
2482
2726
  thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
2483
2727
  thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
2484
2728
  thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "scripts": {
19
19
  "test": "node --test \"test/**/*.test.cjs\"",
20
20
  "lint": "node --check ./bin/thirdfy-agent.mjs",
21
+ "validate:public-docs": "node ./scripts/validate-public-docs.mjs",
21
22
  "validate:cli-contract-schemas": "node ./scripts/validate-cli-contract-schemas.mjs",
22
23
  "validate:actions": "node ./scripts/e2e/validate-actions.mjs",
23
24
  "e2e:base:simulate": "node ./scripts/e2e/base-simulate.mjs",
@@ -30,7 +31,7 @@
30
31
  "e2e:delegation:prod:certify": "node ./scripts/e2e/prod-delegation-certify.mjs",
31
32
  "e2e:report:last": "node ./scripts/e2e/report-last.mjs",
32
33
  "smoke:cli": "node ./bin/thirdfy-agent.mjs --help && node ./bin/thirdfy-agent.mjs --version --json",
33
- "prepublishOnly": "npm run validate:cli-contract-schemas && npm run test && npm run smoke:cli"
34
+ "prepublishOnly": "npm run validate:public-docs && npm run validate:cli-contract-schemas && npm run test && npm run smoke:cli"
34
35
  },
35
36
  "keywords": [
36
37
  "thirdfy",
@@ -33,6 +33,7 @@ function normalizeIntentResponse(response) {
33
33
  const topLevelGovernanceSignal = String(response?.governanceSignal || '').trim() || null;
34
34
  const topLevelDelegationSignal = String(response?.delegationSignal || '').trim() || null;
35
35
  const policyEvaluated = response?.policyEvaluated === true;
36
+ const preflightBlocked = response?.preflightBlocked === true;
36
37
  return {
37
38
  success,
38
39
  status: response?.status || (success ? 'queued' : 'failed'),
@@ -48,6 +49,7 @@ function normalizeIntentResponse(response) {
48
49
  governanceSignal: topLevelGovernanceSignal,
49
50
  delegationSignal: topLevelDelegationSignal,
50
51
  policyEvaluated,
52
+ preflightBlocked,
51
53
  error: response?.error || null,
52
54
  mode: response?.mode || null,
53
55
  unsignedTx: response?.unsignedTx || null,