@thirdfy/agent-cli 0.1.7 → 0.1.9

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 ADDED
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
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
+
5
+ ## [Unreleased]
6
+
7
+ ### Fixed
8
+
9
+ - **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.
10
+
11
+ ## [0.1.8] - 2026-04-07
12
+
13
+ **npm:** [`@thirdfy/agent-cli@0.1.8`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.8)
14
+ **Git tag:** `v0.1.8`
15
+
16
+ ### Added
17
+
18
+ - **Managed custodial lifecycle validation:** Added CLI E2E path for managed wallet delegation parity (`e2e:managed:delegation:parity`) and integrated coverage into production certification flow.
19
+
20
+ ### Changed
21
+
22
+ - **Self execution diagnostics:** `doctor self` is now routed as an offline/local command and no longer depends on capabilities negotiation.
23
+ - **Managed wallet command routing:** Removed thin wrapper indirection and wired `managed wallet init/grant` directly to delegation custodial handlers.
24
+
25
+ ### Fixed
26
+
27
+ - **Effect check payload safety:** Hardened relaxed-mode effect-check fallback so warning payload shape remains deterministic in edge paths.
28
+ - **Managed wallet integration test contract:** Validation-only detection now correctly uses `executionLane === 'validation_only'`, with explicit auth env isolation for deterministic header assertions.
29
+
30
+ ## [0.1.7] - 2026-04-05
31
+
32
+ **npm:** [`@thirdfy/agent-cli@0.1.7`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.7)
33
+ **Git tag:** [`v0.1.7`](https://github.com/thirdfy/agent-cli/releases/tag/v0.1.7)
34
+
35
+ ### Added
36
+
37
+ - **Login and local config:** `thirdfy-agent login`, `logout` (including `logout --all` for local clear plus owner-session revoke when applicable), `whoami`, and `config set` / `config show` with defaults persisted under `~/.thirdfy/config.json`. Credential precedence remains: CLI flags, then environment variables, then saved config (see README).
38
+ - **Custodial delegation (MetaMask-compatible path):** `delegation init-custodial` and `delegation custodial-grant` alongside the existing `delegation create` / `delegation activate` lifecycle.
39
+ - **Delegation operator ergonomics:** `delegation show` as an alias for `delegation status`; expanded delegation inspection and parity-focused behavior for automation.
40
+ - **E2E / certification:** Delegation parity and production certification scripts (`e2e:self:delegation:parity`, `e2e:delegation:prod:certify`) and related gates documented in internal runbooks.
41
+
42
+ ### Changed
43
+
44
+ - **`delegation redeem`:** Respects the effective run mode (flag / env / profile) when building the execution path.
45
+ - **Swap amount handling:** Clearer separation between *missing* and *ambiguous* amount-unit errors; stricter validation of human-form amounts (including excess precision) and decimals handling for `amountInHuman` flows.
46
+
47
+ ### Fixed
48
+
49
+ - **Config and bootstrap:** Safer bootstrap output, tighter validation, preserved config semantics on upgrade paths, and `config set` rejects prototype-path keys to avoid confusing or unsafe key shapes.
50
+ - **Login / token handling:** Stricter verification of token shape after login and safer config cloning for delegation-related routing.
51
+ - **E2E:** Session-auth paths keep a dev-mock wallet fallback where intended so local and CI simulations stay stable.
52
+
53
+ ### Tooling
54
+
55
+ - **Release workflow:** When a version is already published to npm, CI skips `npm publish` so tag-based runs can still create or align GitHub releases without duplicate-publish failures.
56
+
57
+ ---
58
+
59
+ ## [0.1.6] - 2026-03-30
60
+
61
+ Prior release; see [git compare `v0.1.6...v0.1.7`](https://github.com/thirdfy/agent-cli/compare/v0.1.6...v0.1.7) for the full commit list.
package/README.md CHANGED
@@ -33,6 +33,13 @@ Run without global install:
33
33
  npx @thirdfy/agent-cli --help
34
34
  ```
35
35
 
36
+ ## What's new in 0.1.8
37
+
38
+ - Managed custodial lifecycle parity validation is now first-class in CLI E2E/certification.
39
+ - `doctor self` runs as a local diagnostic command without capabilities negotiation.
40
+ - Self-exec effect-check fallback payloads are hardened for deterministic automation parsing.
41
+ - Managed wallet aliases (`managed wallet init/grant`) are wired directly to custodial delegation handlers.
42
+
36
43
  ## Quick start
37
44
 
38
45
  ```bash
@@ -101,6 +108,9 @@ thirdfy-agent agent register --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN
101
108
  - `run` auto-generates `idempotencyKey` by default when not provided.
102
109
  - For deterministic retries across workers/restarts, pass your own stable `--idempotency-key`.
103
110
  - `preflight` does not force idempotency keys.
111
+ - `preflight --run-mode self` is capability-aware:
112
+ - if an action supports execute-intent validation, CLI uses `executionLane=validation_only` before building tx;
113
+ - if an action advertises `supportsExecuteIntent=false` and `supportsBuildTx=true`, CLI routes preflight directly to `build-tx`.
104
114
 
105
115
  ## Run modes and profiles
106
116
 
@@ -110,6 +120,12 @@ thirdfy-agent agent register --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN
110
120
  - `--run-mode self` -> self-custody rail (`build-tx` flow; default for `profile=personal`)
111
121
  - `--run-mode hybrid` -> self-custody + governance mirror metadata (default for `profile=builder`)
112
122
 
123
+ | Goal | Recommended mode | Why |
124
+ | --- | --- | --- |
125
+ | BYOW signing and local custody | `self` | Keeps signing and broadcast on your wallet path. |
126
+ | Managed delegated execution | `thirdfy` | Uses delegated governance rail for hosted execution. |
127
+ | Self execution plus governance mirror checks | `hybrid` | Preserves local execution while emitting managed-lane mirrors. |
128
+
113
129
  Auth expectations by mode:
114
130
 
115
131
  - `self`: requires execution identity (`agentApiKey`) today; keyless self lane is blocked with deterministic `SELF_OPEN_DISABLED`.
@@ -133,6 +149,41 @@ thirdfy-agent whoami --json
133
149
 
134
150
  `profile use --profile <name>` resets run mode to that profile default unless `--run-mode` is explicitly provided.
135
151
 
152
+ ### One-command self execution (BYOW)
153
+
154
+ Use `--broadcast` with `run --run-mode self` to keep one deterministic command for build/sign/send:
155
+
156
+ ```bash
157
+ thirdfy-agent run \
158
+ --agent-api-key "$AGENT_API_KEY" \
159
+ --run-mode self \
160
+ --broadcast \
161
+ --action swap \
162
+ --params '{"tokenIn":"0x...","tokenOut":"0x...","amountInRaw":"1000000","chainId":8453}' \
163
+ --json
164
+ ```
165
+
166
+ The default `run --run-mode self` behavior is still unsigned-only (advanced users can sign externally).
167
+
168
+ Self lane effect-check controls:
169
+
170
+ ```bash
171
+ # strict: fail if no observable token delta
172
+ thirdfy-agent self-exec ... --effect-check strict --json
173
+
174
+ # relaxed (default): retry/read balances, warn on mismatch, do not false-fail the tx
175
+ thirdfy-agent self-exec ... --effect-check relaxed --json
176
+
177
+ # off: skip post-trade token-delta verification
178
+ thirdfy-agent self-exec ... --effect-check off --json
179
+ ```
180
+
181
+ Signer diagnostics:
182
+
183
+ ```bash
184
+ thirdfy-agent doctor self --json
185
+ ```
186
+
136
187
  ## Swap amount contract (agent-safe)
137
188
 
138
189
  For `--action swap`, CLI enforces an explicit amount unit contract:
@@ -251,6 +302,16 @@ The CLI keeps compatibility with legacy `delegation create` + `delegation activa
251
302
 
252
303
  `delegation show` is a direct alias to `delegation status` for gator-style operator ergonomics.
253
304
 
305
+ Managed easy-wallet command group:
306
+
307
+ ```bash
308
+ thirdfy-agent managed wallet init --auth-token "$THIRDFY_AUTH_TOKEN" --json
309
+ # or: --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN"
310
+ thirdfy-agent managed wallet grant --auth-token "$THIRDFY_AUTH_TOKEN" --agent-key "0xYOUR_AGENT_KEY" --token-address "0x..." --max-usd-per-day 250 --json
311
+ # or: --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN"
312
+ thirdfy-agent managed run --agent-api-key "$AGENT_API_KEY" --run-mode thirdfy --action swap --params '{"tokenIn":"0x...","tokenOut":"0x...","amountInRaw":"1000000"}' --json
313
+ ```
314
+
254
315
  ## More docs
255
316
 
256
317
  - OpenClaw CLI onboarding flow:
@@ -264,11 +325,16 @@ The CLI keeps compatibility with legacy `delegation create` + `delegation activa
264
325
  npm run validate:cli-contract-schemas
265
326
  npm test
266
327
  npm run smoke:cli
328
+ npm run e2e:managed:delegation:parity
267
329
  ```
268
330
 
269
331
  ## Release
270
332
 
333
+ Version history and user-facing notes: [CHANGELOG.md](./CHANGELOG.md) (for example [0.1.7](https://github.com/thirdfy/agent-cli/releases/tag/v0.1.7) — login/config UX, custodial delegation helpers, swap and config hardening).
334
+
271
335
  ```bash
272
336
  npm version patch
273
337
  npm publish --access public
274
338
  ```
339
+
340
+ After publishing, push the matching tag (for example `v0.1.7`) so the [Release workflow](.github/workflows/release.yml) can create a GitHub release. If the package version is already on npm, the workflow skips republishing and still creates the release when triggered by a tag.
@@ -19,6 +19,7 @@ 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
21
  const RUN_MODES = ['thirdfy', 'self', 'hybrid'];
22
+ const EFFECT_CHECK_MODES = ['strict', 'relaxed', 'off'];
22
23
  const PROFILE_DEFAULTS = {
23
24
  personal: 'self',
24
25
  builder: 'hybrid',
@@ -118,6 +119,9 @@ async function main() {
118
119
  case 'whoami':
119
120
  await commandProfileShow(context, subFlags);
120
121
  return;
122
+ case 'doctor self':
123
+ await commandDoctorSelf(context, subFlags);
124
+ return;
121
125
  default:
122
126
  break;
123
127
  }
@@ -125,6 +129,9 @@ async function main() {
125
129
  const capabilities = await negotiateCapabilities(context);
126
130
 
127
131
  switch (commandKey) {
132
+ case 'managed run':
133
+ await commandManagedRun(context, subFlags, capabilities);
134
+ return;
128
135
  case 'jeff trade':
129
136
  await commandRun(context, subFlags, capabilities);
130
137
  return;
@@ -201,6 +208,14 @@ async function main() {
201
208
  await commandAgentAuthVerify(context, subFlags, capabilities);
202
209
  return;
203
210
  }
211
+ if (commandKey3 === 'managed wallet init') {
212
+ await commandDelegationInitCustodial(context, subFlags, capabilities);
213
+ return;
214
+ }
215
+ if (commandKey3 === 'managed wallet grant') {
216
+ await commandDelegationCustodialGrant(context, subFlags, capabilities);
217
+ return;
218
+ }
204
219
 
205
220
  const rootCommand = parsed.positionals[0];
206
221
  switch (rootCommand) {
@@ -502,7 +517,7 @@ async function commandPreflight(ctx, flags, capabilities) {
502
517
  prepareActionParamsForFlags(flags, resolved.resolvedAction);
503
518
  const runMode = getEffectiveRunMode(ctx, flags);
504
519
  enforceChainCompatibility(capabilities, flags, runMode);
505
- const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved.resolvedAction);
520
+ const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved);
506
521
  const normalized = backendResult.normalized;
507
522
  printEnvelope({
508
523
  success: normalized.success,
@@ -514,6 +529,7 @@ async function commandPreflight(ctx, flags, capabilities) {
514
529
  catalog: flags.catalog || null,
515
530
  requestedAction: resolved.requestedAction,
516
531
  resolvedAction: resolved.resolvedAction,
532
+ preflightRoute: backendResult.route,
517
533
  runMode,
518
534
  profile: ctx.profile || null,
519
535
  swapInput: flags.__swapInput || null,
@@ -533,6 +549,26 @@ async function commandRun(ctx, flags, capabilities) {
533
549
  skipPreflight,
534
550
  });
535
551
  const normalized = backendResult.normalized;
552
+ if (runMode === 'self' && parseBooleanFlag(flags.broadcast, false) && normalized?.success) {
553
+ const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, normalized);
554
+ printEnvelope({
555
+ success: true,
556
+ code: 'SELF_EXECUTED',
557
+ message: 'Self-custody execution completed',
558
+ data: executed,
559
+ meta: {
560
+ apiBase: ctx.apiBase,
561
+ requestedAction: resolved.requestedAction,
562
+ resolvedAction: resolved.resolvedAction,
563
+ runMode,
564
+ profile: ctx.profile || null,
565
+ skippedPreflight: skipPreflight,
566
+ swapInput: flags.__swapInput || null,
567
+ capabilitiesVersion: capabilities?.contractVersion || null,
568
+ },
569
+ });
570
+ return;
571
+ }
536
572
  printEnvelope({
537
573
  success: normalized.success,
538
574
  code: backendResult.code,
@@ -1194,108 +1230,124 @@ async function commandSelfExec(ctx, flags, capabilities) {
1194
1230
  process.exit(1);
1195
1231
  }
1196
1232
 
1197
- const txd = resolveUnsignedTx(selfResult);
1198
- if (!txd?.to || !txd?.data) {
1199
- printEnvelope({
1200
- success: false,
1201
- code: 'SELF_UNSIGNED_TX_MISSING',
1202
- message: 'Self execution did not return a valid unsigned transaction',
1203
- data: selfResult || {},
1204
- meta: {
1205
- apiBase: ctx.apiBase,
1206
- runMode: 'self',
1207
- resolvedAction: resolved.resolvedAction,
1208
- },
1209
- });
1210
- process.exit(1);
1211
- }
1233
+ const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, selfResult);
1212
1234
 
1213
- const chainId = Number(txd.chainId || flags.chainId || 8453);
1235
+ printEnvelope({
1236
+ success: true,
1237
+ code: 'SELF_EXECUTED',
1238
+ message: 'Self-custody execution completed',
1239
+ data: executed,
1240
+ meta: {
1241
+ apiBase: ctx.apiBase,
1242
+ runMode: 'self',
1243
+ resolvedAction: resolved.resolvedAction,
1244
+ swapInput: flags.__swapInput || null,
1245
+ },
1246
+ });
1247
+ }
1248
+
1249
+ async function commandDoctorSelf(ctx, flags) {
1214
1250
  const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
1215
1251
  const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').trim();
1216
- const rpcUrl = String(flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org').trim();
1217
- if (!walletAddress) {
1218
- throw createCliError('SIGNING_FAILED', 'Missing --wallet-address (or MARKET_MAKER_WALLET_ADDRESS) for self-exec signing.');
1219
- }
1220
- if (!owsWalletName) {
1221
- throw createCliError('SIGNING_FAILED', 'Missing --ows-wallet-name (or OWS_WALLET_NAME) for self-exec signing.');
1222
- }
1223
-
1224
- const params = getPreparedParams(flags);
1225
- const effectTokenIn = String(flags.effectTokenIn || params.tokenIn || '').trim();
1226
- const effectTokenOut = String(flags.effectTokenOut || params.tokenOut || '').trim();
1252
+ const rpcUrl = String(
1253
+ flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org'
1254
+ ).trim();
1255
+ const passphrasePresent = Boolean(String(process.env.OWS_PASSPHRASE || process.env.OWS_API_KEY || '').trim());
1256
+ const checks = [];
1227
1257
 
1228
- let balancesBefore = null;
1229
- if (effectTokenIn && effectTokenOut) {
1230
- balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
1231
- }
1258
+ checks.push({
1259
+ name: 'wallet-address',
1260
+ ok: Boolean(walletAddress),
1261
+ detail: walletAddress || 'missing --wallet-address / MARKET_MAKER_WALLET_ADDRESS',
1262
+ });
1263
+ checks.push({
1264
+ name: 'ows-wallet-name',
1265
+ ok: Boolean(owsWalletName),
1266
+ detail: owsWalletName || 'missing --ows-wallet-name / OWS_WALLET_NAME',
1267
+ });
1268
+ checks.push({
1269
+ name: 'ows-passphrase',
1270
+ ok: passphrasePresent,
1271
+ detail: passphrasePresent ? 'present' : 'missing OWS_PASSPHRASE (or OWS_API_KEY fallback)',
1272
+ });
1232
1273
 
1233
- let submission;
1234
1274
  try {
1235
- submission = await signAndBroadcastWithOws({
1236
- txd,
1237
- chainId,
1238
- walletAddress,
1239
- owsWalletName,
1240
- rpcUrl,
1241
- });
1275
+ resolveOwsBinary();
1276
+ checks.push({ name: 'ows-binary', ok: true, detail: 'found' });
1242
1277
  } catch (error) {
1243
- throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
1278
+ checks.push({ name: 'ows-binary', ok: false, detail: error?.message || 'not found' });
1244
1279
  }
1245
1280
 
1246
- let receipt;
1247
1281
  try {
1248
- receipt = await submission.sent.wait();
1282
+ await loadEthers();
1283
+ checks.push({ name: 'ethers-runtime', ok: true, detail: 'loaded' });
1249
1284
  } catch (error) {
1250
- throw createCliError('BROADCAST_FAILED', error?.message || 'Broadcast wait failed');
1251
- }
1252
- const status = Number(receipt?.status ?? 0);
1253
- if (status !== 1) {
1254
- throw createCliError('BROADCAST_FAILED', 'Transaction receipt status is not success', {
1255
- receiptStatus: receipt?.status ?? null,
1256
- txHash: submission.sent.hash,
1257
- });
1285
+ checks.push({ name: 'ethers-runtime', ok: false, detail: error?.message || 'missing ethers dependency' });
1258
1286
  }
1259
1287
 
1260
- let effectCheck = { checked: false, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null };
1261
- if (balancesBefore) {
1262
- const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
1263
- const inDeltaRaw = (BigInt(balancesBefore.tokenIn) - BigInt(balancesAfter.tokenIn)).toString();
1264
- const outDeltaRaw = (BigInt(balancesAfter.tokenOut) - BigInt(balancesBefore.tokenOut)).toString();
1265
- const hasEffect = BigInt(inDeltaRaw) > 0n || BigInt(outDeltaRaw) > 0n;
1266
- effectCheck = {
1267
- checked: true,
1268
- hasEffect,
1269
- inDeltaRaw,
1270
- outDeltaRaw,
1271
- };
1272
- if (!hasEffect) {
1273
- throw createCliError('EFFECT_CHECK_FAILED', 'Transaction mined but did not produce observable token effect.', {
1274
- txHash: submission.sent.hash,
1275
- effectCheck,
1276
- });
1277
- }
1288
+ try {
1289
+ const ethers = await loadEthers();
1290
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
1291
+ const block = await provider.getBlockNumber();
1292
+ checks.push({ name: 'rpc-connectivity', ok: Number.isFinite(block), detail: `latestBlock=${block}` });
1293
+ } catch (error) {
1294
+ checks.push({ name: 'rpc-connectivity', ok: false, detail: error?.message || 'rpc check failed' });
1278
1295
  }
1279
1296
 
1297
+ const ok = checks.every((entry) => entry.ok);
1280
1298
  printEnvelope({
1281
- success: true,
1282
- code: 'SELF_EXECUTED',
1283
- message: 'Self-custody execution completed',
1299
+ success: ok,
1300
+ code: ok ? 'DOCTOR_SELF_OK' : 'DOCTOR_SELF_FAILED',
1301
+ message: ok ? 'Self lane signer diagnostics passed' : 'Self lane signer diagnostics found blocking issues',
1284
1302
  data: {
1285
- txHash: submission.sent.hash,
1286
- chainId,
1287
- blockNumber: receipt?.blockNumber ?? null,
1288
- status: receipt?.status ?? null,
1289
- unsignedTx: txd,
1290
- effectCheck,
1303
+ checks,
1304
+ hints: [
1305
+ 'For one-command self execution use: thirdfy-agent run --run-mode self --broadcast ...',
1306
+ 'Set --effect-check strict|relaxed|off to control post-trade effect validation behavior.',
1307
+ ],
1291
1308
  },
1292
1309
  meta: {
1293
1310
  apiBase: ctx.apiBase,
1294
1311
  runMode: 'self',
1312
+ rpcUrl,
1313
+ },
1314
+ });
1315
+ if (!ok) process.exit(1);
1316
+ }
1317
+
1318
+ async function commandManagedRun(ctx, flags, capabilities) {
1319
+ const mode = normalizeManagedRunMode(flags.runMode || 'thirdfy');
1320
+ const managedFlags = { ...flags, runMode: mode };
1321
+ const resolved = await resolveActionSelection(ctx, managedFlags);
1322
+ prepareActionParamsForFlags(managedFlags, resolved.resolvedAction);
1323
+ enforceChainCompatibility(capabilities, managedFlags, mode);
1324
+ const skipPreflight = Boolean(managedFlags.skipPreflight);
1325
+ const backendResult = await runExecutionByMode(mode, ctx, managedFlags, resolved, {
1326
+ skipPreflight,
1327
+ });
1328
+ const normalized = backendResult.normalized;
1329
+ printEnvelope({
1330
+ success: normalized.success,
1331
+ code: normalized.success ? 'MANAGED_RUN_QUEUED' : backendResult.code,
1332
+ message: normalized.success
1333
+ ? mode === 'hybrid'
1334
+ ? 'Managed easy-wallet hybrid execution prepared'
1335
+ : 'Managed easy-wallet execution queued'
1336
+ : backendResult.message,
1337
+ data: normalized,
1338
+ meta: {
1339
+ apiBase: ctx.apiBase,
1340
+ requestedAction: resolved.requestedAction,
1295
1341
  resolvedAction: resolved.resolvedAction,
1296
- swapInput: flags.__swapInput || null,
1342
+ lane: 'managed_easy_wallet',
1343
+ runMode: mode,
1344
+ skippedPreflight: skipPreflight,
1345
+ profile: ctx.profile || null,
1346
+ swapInput: managedFlags.__swapInput || null,
1347
+ capabilitiesVersion: capabilities?.contractVersion || null,
1297
1348
  },
1298
1349
  });
1350
+ if (!normalized.success) process.exit(1);
1299
1351
  }
1300
1352
 
1301
1353
  async function commandProfileInit(ctx, flags) {
@@ -1543,6 +1595,14 @@ function normalizeRunMode(value) {
1543
1595
  throw new Error(`Unsupported --run-mode "${value}". Supported: ${RUN_MODES.join(', ')}`);
1544
1596
  }
1545
1597
 
1598
+ function normalizeManagedRunMode(value) {
1599
+ const mode = normalizeRunMode(value || 'thirdfy');
1600
+ if (mode === 'self') {
1601
+ throw new Error('Managed lane does not support run-mode=self. Use thirdfy or hybrid.');
1602
+ }
1603
+ return mode;
1604
+ }
1605
+
1546
1606
  function getProfileConfigPath() {
1547
1607
  return path.join(os.homedir(), '.thirdfy', 'config.json');
1548
1608
  }
@@ -1651,13 +1711,22 @@ function enforceChainCompatibility(capabilities, flags, runMode) {
1651
1711
  }
1652
1712
  }
1653
1713
 
1654
- async function runPreflightByMode(runMode, ctx, flags, resolvedAction) {
1714
+ function shouldUseBuildTxPreflight(runMode, resolved) {
1715
+ if (runMode !== 'self') return false;
1716
+ const meta = resolved?.resolvedActionMeta || null;
1717
+ if (!meta || typeof meta !== 'object') return false;
1718
+ return meta.supportsExecuteIntent === false && meta.supportsBuildTx === true;
1719
+ }
1720
+
1721
+ async function runPreflightByMode(runMode, ctx, flags, resolved) {
1722
+ const resolvedAction = resolved.resolvedAction;
1655
1723
  if (runMode === 'self') {
1724
+ const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
1656
1725
  const normalized = await executeSelfRun(
1657
1726
  ctx,
1658
1727
  flags,
1659
1728
  { resolvedAction },
1660
- { skipPreflight: false }
1729
+ { skipPreflight: route === 'build_tx' }
1661
1730
  );
1662
1731
  const blockedByGovernance = !normalized.success && Boolean(normalized.preflightBlocked);
1663
1732
  return {
@@ -1666,6 +1735,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolvedAction) {
1666
1735
  : normalized.success
1667
1736
  ? 'Self preflight completed (unsigned transaction ready)'
1668
1737
  : 'Self preflight failed',
1738
+ route,
1669
1739
  normalized,
1670
1740
  };
1671
1741
  }
@@ -1686,6 +1756,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolvedAction) {
1686
1756
  : runMode === 'hybrid'
1687
1757
  ? 'Hybrid mirror preflight failed'
1688
1758
  : 'Preflight failed',
1759
+ route: 'execute_intent_validation',
1689
1760
  normalized,
1690
1761
  };
1691
1762
  }
@@ -1947,18 +2018,19 @@ async function resolveActionSelection(ctx, flags) {
1947
2018
  .map((action) => ({
1948
2019
  key: getActionKey(action),
1949
2020
  aliases: getActionAliases(action),
2021
+ action,
1950
2022
  }))
1951
2023
  .filter((entry) => entry.key);
1952
2024
 
1953
2025
  if (!keyed.length) {
1954
- return { requestedAction, resolvedAction: requestedAction };
2026
+ return { requestedAction, resolvedAction: requestedAction, resolvedActionMeta: null };
1955
2027
  }
1956
2028
 
1957
2029
  // Prefer direct action id matches over alias matches.
1958
2030
  // This keeps UX simple for commands like `--action swap` when another action aliases to "swap".
1959
2031
  const exactKey = keyed.filter((entry) => entry.key.toLowerCase() === requestedLower);
1960
2032
  if (exactKey.length === 1) {
1961
- return { requestedAction, resolvedAction: exactKey[0].key };
2033
+ return { requestedAction, resolvedAction: exactKey[0].key, resolvedActionMeta: exactKey[0].action || null };
1962
2034
  }
1963
2035
  if (exactKey.length > 1) {
1964
2036
  throw new Error(
@@ -1970,7 +2042,7 @@ async function resolveActionSelection(ctx, flags) {
1970
2042
 
1971
2043
  const exactAlias = keyed.filter((entry) => entry.aliases.some((a) => a.toLowerCase() === requestedLower));
1972
2044
  if (exactAlias.length === 1) {
1973
- return { requestedAction, resolvedAction: exactAlias[0].key };
2045
+ return { requestedAction, resolvedAction: exactAlias[0].key, resolvedActionMeta: exactAlias[0].action || null };
1974
2046
  }
1975
2047
  if (exactAlias.length > 1) {
1976
2048
  throw new Error(
@@ -1986,7 +2058,7 @@ async function resolveActionSelection(ctx, flags) {
1986
2058
  entry.aliases.some((a) => a.toLowerCase().includes(requestedLower))
1987
2059
  );
1988
2060
  if (fuzzy.length === 1) {
1989
- return { requestedAction, resolvedAction: fuzzy[0].key };
2061
+ return { requestedAction, resolvedAction: fuzzy[0].key, resolvedActionMeta: fuzzy[0].action || null };
1990
2062
  }
1991
2063
  if (fuzzy.length > 1) {
1992
2064
  throw new Error(
@@ -2048,6 +2120,171 @@ function resolveUnsignedTx(result) {
2048
2120
  return null;
2049
2121
  }
2050
2122
 
2123
+ function parseBooleanFlag(value, defaultValue = false) {
2124
+ if (value === undefined || value === null) return defaultValue;
2125
+ if (typeof value === 'boolean') return value;
2126
+ const normalized = String(value).trim().toLowerCase();
2127
+ if (!normalized) return defaultValue;
2128
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
2129
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
2130
+ return defaultValue;
2131
+ }
2132
+
2133
+ function normalizeEffectCheckMode(value) {
2134
+ const normalized = String(value || '').trim().toLowerCase();
2135
+ if (!normalized) return 'relaxed';
2136
+ if (EFFECT_CHECK_MODES.includes(normalized)) return normalized;
2137
+ throw createCliError(
2138
+ 'EFFECT_CHECK_MODE_INVALID',
2139
+ `Unsupported --effect-check "${value}". Supported: ${EFFECT_CHECK_MODES.join(', ')}.`
2140
+ );
2141
+ }
2142
+
2143
+ async function sleepMs(ms) {
2144
+ const delay = Number(ms || 0);
2145
+ if (!Number.isFinite(delay) || delay <= 0) return;
2146
+ await new Promise((resolve) => setTimeout(resolve, delay));
2147
+ }
2148
+
2149
+ async function resolveEffectCheckResult({
2150
+ mode,
2151
+ rpcUrl,
2152
+ walletAddress,
2153
+ tokenIn,
2154
+ tokenOut,
2155
+ balancesBefore,
2156
+ retries,
2157
+ waitMs,
2158
+ }) {
2159
+ const baseResult = {
2160
+ checked: true,
2161
+ mode,
2162
+ hasEffect: false,
2163
+ inDeltaRaw: '0',
2164
+ outDeltaRaw: '0',
2165
+ attempts: 0,
2166
+ retries: Math.max(0, Number(retries || 0)),
2167
+ };
2168
+ if (mode === 'off') {
2169
+ return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'disabled' };
2170
+ }
2171
+ if (!tokenIn || !tokenOut || !balancesBefore) {
2172
+ return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'token_pair_missing' };
2173
+ }
2174
+ const maxAttempts = Math.max(1, Number(retries || 0) + 1);
2175
+ let last = null;
2176
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2177
+ const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut);
2178
+ const inDeltaRaw = (BigInt(balancesBefore.tokenIn) - BigInt(balancesAfter.tokenIn)).toString();
2179
+ const outDeltaRaw = (BigInt(balancesAfter.tokenOut) - BigInt(balancesBefore.tokenOut)).toString();
2180
+ const hasEffect = BigInt(inDeltaRaw) > 0n || BigInt(outDeltaRaw) > 0n;
2181
+ last = {
2182
+ checked: true,
2183
+ mode,
2184
+ hasEffect,
2185
+ inDeltaRaw,
2186
+ outDeltaRaw,
2187
+ attempts: attempt,
2188
+ retries: maxAttempts - 1,
2189
+ };
2190
+ if (hasEffect) return last;
2191
+ if (attempt < maxAttempts) await sleepMs(waitMs);
2192
+ }
2193
+ return {
2194
+ ...(last || baseResult),
2195
+ warning: 'No balance delta observed after retries; treating as non-fatal in relaxed mode.',
2196
+ };
2197
+ }
2198
+
2199
+ async function performSelfBroadcast(ctx, flags, resolvedAction, selfResult) {
2200
+ const txd = resolveUnsignedTx(selfResult);
2201
+ if (!txd?.to || !txd?.data) {
2202
+ throw createCliError('SELF_UNSIGNED_TX_MISSING', 'Self execution did not return a valid unsigned transaction');
2203
+ }
2204
+ const chainId = Number(txd.chainId || flags.chainId || 8453);
2205
+ const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
2206
+ const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').trim();
2207
+ const rpcUrl = String(
2208
+ flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org'
2209
+ ).trim();
2210
+ if (!walletAddress) {
2211
+ throw createCliError('SIGNING_FAILED', 'Missing --wallet-address (or MARKET_MAKER_WALLET_ADDRESS) for self-exec signing.');
2212
+ }
2213
+ if (!owsWalletName) {
2214
+ throw createCliError('SIGNING_FAILED', 'Missing --ows-wallet-name (or OWS_WALLET_NAME) for self-exec signing.');
2215
+ }
2216
+
2217
+ const params = getPreparedParams(flags);
2218
+ const effectTokenIn = String(flags.effectTokenIn || params.tokenIn || '').trim();
2219
+ const effectTokenOut = String(flags.effectTokenOut || params.tokenOut || '').trim();
2220
+ const effectMode = normalizeEffectCheckMode(flags.effectCheck || process.env.THIRDFY_EFFECT_CHECK || 'relaxed');
2221
+ const effectRetries = Number(flags.effectCheckRetries || process.env.THIRDFY_EFFECT_CHECK_RETRIES || 2);
2222
+ const effectWaitMs = Number(flags.effectCheckWaitMs || process.env.THIRDFY_EFFECT_CHECK_WAIT_MS || 1200);
2223
+
2224
+ let balancesBefore = null;
2225
+ if (effectMode !== 'off' && effectTokenIn && effectTokenOut) {
2226
+ balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
2227
+ }
2228
+
2229
+ let submission;
2230
+ try {
2231
+ submission = await signAndBroadcastWithOws({
2232
+ txd,
2233
+ chainId,
2234
+ walletAddress,
2235
+ owsWalletName,
2236
+ rpcUrl,
2237
+ });
2238
+ } catch (error) {
2239
+ throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
2240
+ }
2241
+
2242
+ let receipt;
2243
+ try {
2244
+ receipt = await submission.sent.wait();
2245
+ } catch (error) {
2246
+ throw createCliError('BROADCAST_FAILED', error?.message || 'Broadcast wait failed');
2247
+ }
2248
+ const status = Number(receipt?.status ?? 0);
2249
+ if (status !== 1) {
2250
+ throw createCliError('BROADCAST_FAILED', 'Transaction receipt status is not success', {
2251
+ receiptStatus: receipt?.status ?? null,
2252
+ txHash: submission.sent.hash,
2253
+ });
2254
+ }
2255
+
2256
+ const effectCheck = await resolveEffectCheckResult({
2257
+ mode: effectMode,
2258
+ rpcUrl,
2259
+ walletAddress,
2260
+ tokenIn: effectTokenIn,
2261
+ tokenOut: effectTokenOut,
2262
+ balancesBefore,
2263
+ retries: effectRetries,
2264
+ waitMs: effectWaitMs,
2265
+ });
2266
+ if (effectCheck.checked && !effectCheck.hasEffect && effectMode === 'strict') {
2267
+ throw createCliError('EFFECT_CHECK_FAILED', 'Transaction mined but did not produce observable token effect.', {
2268
+ txHash: submission.sent.hash,
2269
+ effectCheck,
2270
+ mode: effectMode,
2271
+ });
2272
+ }
2273
+
2274
+ return {
2275
+ txHash: submission.sent.hash,
2276
+ chainId,
2277
+ blockNumber: receipt?.blockNumber ?? null,
2278
+ status: receipt?.status ?? null,
2279
+ unsignedTx: txd,
2280
+ effectCheck,
2281
+ lane: 'self',
2282
+ signer: 'ows',
2283
+ resolvedAction,
2284
+ apiBase: ctx.apiBase,
2285
+ };
2286
+ }
2287
+
2051
2288
  async function loadEthers() {
2052
2289
  const candidates = ['ethers', path.join(process.cwd(), 'node_modules', 'ethers')];
2053
2290
  for (const candidate of candidates) {
@@ -2242,8 +2479,8 @@ Core commands:
2242
2479
  thirdfy-agent catalogs list [--json]
2243
2480
  thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--chain-id <id>] [--run-mode <mode>] [--json]
2244
2481
  thirdfy-agent delegation create --auth-token <token> --agent-key <key> --owner-address <addr> --token-address <addr> --max-usd-per-day <usd> [--session-account-address <addr>] [--json]
2245
- thirdfy-agent delegation init-custodial --auth-token <token> [--chain-id <id>] [--json]
2246
- thirdfy-agent delegation custodial-grant --auth-token <token> --agent-key <key> --token-address <addr> --max-usd-per-day <usd> [--chain-id <id>] [--period-duration <sec>] [--expiry-seconds <sec>] [--json]
2482
+ thirdfy-agent delegation init-custodial [--auth-token <token> | --owner-session-token <token>] [--chain-id <id>] [--json]
2483
+ thirdfy-agent delegation custodial-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]
2247
2484
  thirdfy-agent delegation activate --auth-token <token> --agent-key <key> --wallet-address <addr> --session-account-address <addr> --delegation-manager <addr> --delegation <json> --signature <hex> [--json]
2248
2485
  thirdfy-agent delegation status --auth-token <token> --agent-key <key> [--verify] [--json]
2249
2486
  thirdfy-agent delegation show --auth-token <token> --agent-key <key> [--verify] [--json]
@@ -2251,6 +2488,9 @@ Core commands:
2251
2488
  thirdfy-agent delegation balance --auth-token <token> --agent-key <key> [--chain-id <id>] [--json]
2252
2489
  thirdfy-agent delegation revoke --auth-token <token> --agent-key <key> [--reason <text>] [--chain-id <id>] [--json]
2253
2490
  thirdfy-agent delegation redeem --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--estimated-amount-usd <usd>] [--json]
2491
+ thirdfy-agent managed wallet init [--auth-token <token> | --owner-session-token <token>] [--chain-id <id>] [--json]
2492
+ 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]
2493
+ thirdfy-agent managed run --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--idempotency-key <key>] [--json]
2254
2494
  thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
2255
2495
  thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
2256
2496
  thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
@@ -2260,8 +2500,9 @@ Core commands:
2260
2500
  thirdfy-agent agent key rotate [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
2261
2501
  thirdfy-agent agent key revoke [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
2262
2502
  thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
2263
- thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--idempotency-key <key>] [--json]
2264
- thirdfy-agent self-exec --agent-api-key <key> --action <id> --params <json> --wallet-address <addr> --ows-wallet-name <name> [--rpc-url <url>] [--json]
2503
+ thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--broadcast] [--idempotency-key <key>] [--json]
2504
+ thirdfy-agent self-exec --agent-api-key <key> --action <id> --params <json> --wallet-address <addr> --ows-wallet-name <name> [--rpc-url <url>] [--effect-check strict|relaxed|off] [--json]
2505
+ thirdfy-agent doctor self [--wallet-address <addr>] [--ows-wallet-name <name>] [--rpc-url <url>] [--json]
2265
2506
  thirdfy-agent jeff preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
2266
2507
  thirdfy-agent jeff trade --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
2267
2508
  thirdfy-agent jeff status --intent-id <id> [--json]
@@ -2276,6 +2517,8 @@ Global flags:
2276
2517
  --env <file> load env vars from file
2277
2518
  --timeout <ms> request timeout
2278
2519
  --params <json> for swap: exactly one of {amountInRaw} or {amountInHuman + tokenInDecimals}
2520
+ --broadcast with run --run-mode self, sign and broadcast using OWS
2521
+ --effect-check <mode> self-exec check mode: strict | relaxed | off (default: relaxed)
2279
2522
  --owner-session-token <token> owner session token (or THIRDFY_OWNER_SESSION_TOKEN)
2280
2523
  --json stable machine-readable output
2281
2524
  --verbose print adapter diagnostics
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "src/contracts/cli/schemas/",
13
13
  "scripts/validate-cli-contract-schemas.mjs",
14
14
  "README.md",
15
+ "CHANGELOG.md",
15
16
  "LICENSE"
16
17
  ],
17
18
  "scripts": {
@@ -25,6 +26,7 @@
25
26
  "e2e:delegation:lifecycle": "node ./scripts/e2e/delegation-lifecycle.mjs",
26
27
  "e2e:delegation:commands": "node ./scripts/e2e/delegation-command-parity.mjs",
27
28
  "e2e:self:delegation:parity": "node ./scripts/e2e/self-delegation-parity.mjs",
29
+ "e2e:managed:delegation:parity": "node ./scripts/e2e/managed-custodial-lifecycle.mjs",
28
30
  "e2e:delegation:prod:certify": "node ./scripts/e2e/prod-delegation-certify.mjs",
29
31
  "e2e:report:last": "node ./scripts/e2e/report-last.mjs",
30
32
  "smoke:cli": "node ./bin/thirdfy-agent.mjs --help && node ./bin/thirdfy-agent.mjs --version --json",
@@ -48,5 +50,8 @@
48
50
  "bugs": {
49
51
  "url": "https://github.com/thirdfy/agent-cli/issues"
50
52
  },
51
- "homepage": "https://github.com/thirdfy/agent-cli#readme"
53
+ "homepage": "https://github.com/thirdfy/agent-cli#readme",
54
+ "dependencies": {
55
+ "ethers": "^6.16.0"
56
+ }
52
57
  }