@thirdfy/agent-cli 0.1.7 → 0.1.8

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,57 @@
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
+ ## [0.1.8] - 2026-04-07
8
+
9
+ **npm:** [`@thirdfy/agent-cli@0.1.8`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.8)
10
+ **Git tag:** `v0.1.8`
11
+
12
+ ### Added
13
+
14
+ - **Managed custodial lifecycle validation:** Added CLI E2E path for managed wallet delegation parity (`e2e:managed:delegation:parity`) and integrated coverage into production certification flow.
15
+
16
+ ### Changed
17
+
18
+ - **Self execution diagnostics:** `doctor self` is now routed as an offline/local command and no longer depends on capabilities negotiation.
19
+ - **Managed wallet command routing:** Removed thin wrapper indirection and wired `managed wallet init/grant` directly to delegation custodial handlers.
20
+
21
+ ### Fixed
22
+
23
+ - **Effect check payload safety:** Hardened relaxed-mode effect-check fallback so warning payload shape remains deterministic in edge paths.
24
+ - **Managed wallet integration test contract:** Validation-only detection now correctly uses `executionLane === 'validation_only'`, with explicit auth env isolation for deterministic header assertions.
25
+
26
+ ## [0.1.7] - 2026-04-05
27
+
28
+ **npm:** [`@thirdfy/agent-cli@0.1.7`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.7)
29
+ **Git tag:** [`v0.1.7`](https://github.com/thirdfy/agent-cli/releases/tag/v0.1.7)
30
+
31
+ ### Added
32
+
33
+ - **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).
34
+ - **Custodial delegation (MetaMask-compatible path):** `delegation init-custodial` and `delegation custodial-grant` alongside the existing `delegation create` / `delegation activate` lifecycle.
35
+ - **Delegation operator ergonomics:** `delegation show` as an alias for `delegation status`; expanded delegation inspection and parity-focused behavior for automation.
36
+ - **E2E / certification:** Delegation parity and production certification scripts (`e2e:self:delegation:parity`, `e2e:delegation:prod:certify`) and related gates documented in internal runbooks.
37
+
38
+ ### Changed
39
+
40
+ - **`delegation redeem`:** Respects the effective run mode (flag / env / profile) when building the execution path.
41
+ - **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.
42
+
43
+ ### Fixed
44
+
45
+ - **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.
46
+ - **Login / token handling:** Stricter verification of token shape after login and safer config cloning for delegation-related routing.
47
+ - **E2E:** Session-auth paths keep a dev-mock wallet fallback where intended so local and CI simulations stay stable.
48
+
49
+ ### Tooling
50
+
51
+ - **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.
52
+
53
+ ---
54
+
55
+ ## [0.1.6] - 2026-03-30
56
+
57
+ 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
@@ -133,6 +133,41 @@ thirdfy-agent whoami --json
133
133
 
134
134
  `profile use --profile <name>` resets run mode to that profile default unless `--run-mode` is explicitly provided.
135
135
 
136
+ ### One-command self execution (BYOW)
137
+
138
+ Use `--broadcast` with `run --run-mode self` to keep one deterministic command for build/sign/send:
139
+
140
+ ```bash
141
+ thirdfy-agent run \
142
+ --agent-api-key "$AGENT_API_KEY" \
143
+ --run-mode self \
144
+ --broadcast \
145
+ --action swap \
146
+ --params '{"tokenIn":"0x...","tokenOut":"0x...","amountInRaw":"1000000","chainId":8453}' \
147
+ --json
148
+ ```
149
+
150
+ The default `run --run-mode self` behavior is still unsigned-only (advanced users can sign externally).
151
+
152
+ Self lane effect-check controls:
153
+
154
+ ```bash
155
+ # strict: fail if no observable token delta
156
+ thirdfy-agent self-exec ... --effect-check strict --json
157
+
158
+ # relaxed (default): retry/read balances, warn on mismatch, do not false-fail the tx
159
+ thirdfy-agent self-exec ... --effect-check relaxed --json
160
+
161
+ # off: skip post-trade token-delta verification
162
+ thirdfy-agent self-exec ... --effect-check off --json
163
+ ```
164
+
165
+ Signer diagnostics:
166
+
167
+ ```bash
168
+ thirdfy-agent doctor self --json
169
+ ```
170
+
136
171
  ## Swap amount contract (agent-safe)
137
172
 
138
173
  For `--action swap`, CLI enforces an explicit amount unit contract:
@@ -251,6 +286,16 @@ The CLI keeps compatibility with legacy `delegation create` + `delegation activa
251
286
 
252
287
  `delegation show` is a direct alias to `delegation status` for gator-style operator ergonomics.
253
288
 
289
+ Managed easy-wallet command group:
290
+
291
+ ```bash
292
+ thirdfy-agent managed wallet init --auth-token "$THIRDFY_AUTH_TOKEN" --json
293
+ # or: --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN"
294
+ thirdfy-agent managed wallet grant --auth-token "$THIRDFY_AUTH_TOKEN" --agent-key "0xYOUR_AGENT_KEY" --token-address "0x..." --max-usd-per-day 250 --json
295
+ # or: --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN"
296
+ thirdfy-agent managed run --agent-api-key "$AGENT_API_KEY" --run-mode thirdfy --action swap --params '{"tokenIn":"0x...","tokenOut":"0x...","amountInRaw":"1000000"}' --json
297
+ ```
298
+
254
299
  ## More docs
255
300
 
256
301
  - OpenClaw CLI onboarding flow:
@@ -264,11 +309,16 @@ The CLI keeps compatibility with legacy `delegation create` + `delegation activa
264
309
  npm run validate:cli-contract-schemas
265
310
  npm test
266
311
  npm run smoke:cli
312
+ npm run e2e:managed:delegation:parity
267
313
  ```
268
314
 
269
315
  ## Release
270
316
 
317
+ 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).
318
+
271
319
  ```bash
272
320
  npm version patch
273
321
  npm publish --access public
274
322
  ```
323
+
324
+ 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) {
@@ -533,6 +548,26 @@ async function commandRun(ctx, flags, capabilities) {
533
548
  skipPreflight,
534
549
  });
535
550
  const normalized = backendResult.normalized;
551
+ if (runMode === 'self' && parseBooleanFlag(flags.broadcast, false) && normalized?.success) {
552
+ const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, normalized);
553
+ printEnvelope({
554
+ success: true,
555
+ code: 'SELF_EXECUTED',
556
+ message: 'Self-custody execution completed',
557
+ data: executed,
558
+ meta: {
559
+ apiBase: ctx.apiBase,
560
+ requestedAction: resolved.requestedAction,
561
+ resolvedAction: resolved.resolvedAction,
562
+ runMode,
563
+ profile: ctx.profile || null,
564
+ skippedPreflight: skipPreflight,
565
+ swapInput: flags.__swapInput || null,
566
+ capabilitiesVersion: capabilities?.contractVersion || null,
567
+ },
568
+ });
569
+ return;
570
+ }
536
571
  printEnvelope({
537
572
  success: normalized.success,
538
573
  code: backendResult.code,
@@ -1194,108 +1229,124 @@ async function commandSelfExec(ctx, flags, capabilities) {
1194
1229
  process.exit(1);
1195
1230
  }
1196
1231
 
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
- }
1232
+ const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, selfResult);
1212
1233
 
1213
- const chainId = Number(txd.chainId || flags.chainId || 8453);
1234
+ printEnvelope({
1235
+ success: true,
1236
+ code: 'SELF_EXECUTED',
1237
+ message: 'Self-custody execution completed',
1238
+ data: executed,
1239
+ meta: {
1240
+ apiBase: ctx.apiBase,
1241
+ runMode: 'self',
1242
+ resolvedAction: resolved.resolvedAction,
1243
+ swapInput: flags.__swapInput || null,
1244
+ },
1245
+ });
1246
+ }
1247
+
1248
+ async function commandDoctorSelf(ctx, flags) {
1214
1249
  const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
1215
1250
  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();
1251
+ const rpcUrl = String(
1252
+ flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org'
1253
+ ).trim();
1254
+ const passphrasePresent = Boolean(String(process.env.OWS_PASSPHRASE || process.env.OWS_API_KEY || '').trim());
1255
+ const checks = [];
1227
1256
 
1228
- let balancesBefore = null;
1229
- if (effectTokenIn && effectTokenOut) {
1230
- balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
1231
- }
1257
+ checks.push({
1258
+ name: 'wallet-address',
1259
+ ok: Boolean(walletAddress),
1260
+ detail: walletAddress || 'missing --wallet-address / MARKET_MAKER_WALLET_ADDRESS',
1261
+ });
1262
+ checks.push({
1263
+ name: 'ows-wallet-name',
1264
+ ok: Boolean(owsWalletName),
1265
+ detail: owsWalletName || 'missing --ows-wallet-name / OWS_WALLET_NAME',
1266
+ });
1267
+ checks.push({
1268
+ name: 'ows-passphrase',
1269
+ ok: passphrasePresent,
1270
+ detail: passphrasePresent ? 'present' : 'missing OWS_PASSPHRASE (or OWS_API_KEY fallback)',
1271
+ });
1232
1272
 
1233
- let submission;
1234
1273
  try {
1235
- submission = await signAndBroadcastWithOws({
1236
- txd,
1237
- chainId,
1238
- walletAddress,
1239
- owsWalletName,
1240
- rpcUrl,
1241
- });
1274
+ resolveOwsBinary();
1275
+ checks.push({ name: 'ows-binary', ok: true, detail: 'found' });
1242
1276
  } catch (error) {
1243
- throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
1277
+ checks.push({ name: 'ows-binary', ok: false, detail: error?.message || 'not found' });
1244
1278
  }
1245
1279
 
1246
- let receipt;
1247
1280
  try {
1248
- receipt = await submission.sent.wait();
1281
+ await loadEthers();
1282
+ checks.push({ name: 'ethers-runtime', ok: true, detail: 'loaded' });
1249
1283
  } 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
- });
1284
+ checks.push({ name: 'ethers-runtime', ok: false, detail: error?.message || 'missing ethers dependency' });
1258
1285
  }
1259
1286
 
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
- }
1287
+ try {
1288
+ const ethers = await loadEthers();
1289
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
1290
+ const block = await provider.getBlockNumber();
1291
+ checks.push({ name: 'rpc-connectivity', ok: Number.isFinite(block), detail: `latestBlock=${block}` });
1292
+ } catch (error) {
1293
+ checks.push({ name: 'rpc-connectivity', ok: false, detail: error?.message || 'rpc check failed' });
1278
1294
  }
1279
1295
 
1296
+ const ok = checks.every((entry) => entry.ok);
1280
1297
  printEnvelope({
1281
- success: true,
1282
- code: 'SELF_EXECUTED',
1283
- message: 'Self-custody execution completed',
1298
+ success: ok,
1299
+ code: ok ? 'DOCTOR_SELF_OK' : 'DOCTOR_SELF_FAILED',
1300
+ message: ok ? 'Self lane signer diagnostics passed' : 'Self lane signer diagnostics found blocking issues',
1284
1301
  data: {
1285
- txHash: submission.sent.hash,
1286
- chainId,
1287
- blockNumber: receipt?.blockNumber ?? null,
1288
- status: receipt?.status ?? null,
1289
- unsignedTx: txd,
1290
- effectCheck,
1302
+ checks,
1303
+ hints: [
1304
+ 'For one-command self execution use: thirdfy-agent run --run-mode self --broadcast ...',
1305
+ 'Set --effect-check strict|relaxed|off to control post-trade effect validation behavior.',
1306
+ ],
1291
1307
  },
1292
1308
  meta: {
1293
1309
  apiBase: ctx.apiBase,
1294
1310
  runMode: 'self',
1311
+ rpcUrl,
1312
+ },
1313
+ });
1314
+ if (!ok) process.exit(1);
1315
+ }
1316
+
1317
+ async function commandManagedRun(ctx, flags, capabilities) {
1318
+ const mode = normalizeManagedRunMode(flags.runMode || 'thirdfy');
1319
+ const managedFlags = { ...flags, runMode: mode };
1320
+ const resolved = await resolveActionSelection(ctx, managedFlags);
1321
+ prepareActionParamsForFlags(managedFlags, resolved.resolvedAction);
1322
+ enforceChainCompatibility(capabilities, managedFlags, mode);
1323
+ const skipPreflight = Boolean(managedFlags.skipPreflight);
1324
+ const backendResult = await runExecutionByMode(mode, ctx, managedFlags, resolved, {
1325
+ skipPreflight,
1326
+ });
1327
+ const normalized = backendResult.normalized;
1328
+ printEnvelope({
1329
+ success: normalized.success,
1330
+ code: normalized.success ? 'MANAGED_RUN_QUEUED' : backendResult.code,
1331
+ message: normalized.success
1332
+ ? mode === 'hybrid'
1333
+ ? 'Managed easy-wallet hybrid execution prepared'
1334
+ : 'Managed easy-wallet execution queued'
1335
+ : backendResult.message,
1336
+ data: normalized,
1337
+ meta: {
1338
+ apiBase: ctx.apiBase,
1339
+ requestedAction: resolved.requestedAction,
1295
1340
  resolvedAction: resolved.resolvedAction,
1296
- swapInput: flags.__swapInput || null,
1341
+ lane: 'managed_easy_wallet',
1342
+ runMode: mode,
1343
+ skippedPreflight: skipPreflight,
1344
+ profile: ctx.profile || null,
1345
+ swapInput: managedFlags.__swapInput || null,
1346
+ capabilitiesVersion: capabilities?.contractVersion || null,
1297
1347
  },
1298
1348
  });
1349
+ if (!normalized.success) process.exit(1);
1299
1350
  }
1300
1351
 
1301
1352
  async function commandProfileInit(ctx, flags) {
@@ -1543,6 +1594,14 @@ function normalizeRunMode(value) {
1543
1594
  throw new Error(`Unsupported --run-mode "${value}". Supported: ${RUN_MODES.join(', ')}`);
1544
1595
  }
1545
1596
 
1597
+ function normalizeManagedRunMode(value) {
1598
+ const mode = normalizeRunMode(value || 'thirdfy');
1599
+ if (mode === 'self') {
1600
+ throw new Error('Managed lane does not support run-mode=self. Use thirdfy or hybrid.');
1601
+ }
1602
+ return mode;
1603
+ }
1604
+
1546
1605
  function getProfileConfigPath() {
1547
1606
  return path.join(os.homedir(), '.thirdfy', 'config.json');
1548
1607
  }
@@ -2048,6 +2107,171 @@ function resolveUnsignedTx(result) {
2048
2107
  return null;
2049
2108
  }
2050
2109
 
2110
+ function parseBooleanFlag(value, defaultValue = false) {
2111
+ if (value === undefined || value === null) return defaultValue;
2112
+ if (typeof value === 'boolean') return value;
2113
+ const normalized = String(value).trim().toLowerCase();
2114
+ if (!normalized) return defaultValue;
2115
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
2116
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
2117
+ return defaultValue;
2118
+ }
2119
+
2120
+ function normalizeEffectCheckMode(value) {
2121
+ const normalized = String(value || '').trim().toLowerCase();
2122
+ if (!normalized) return 'relaxed';
2123
+ if (EFFECT_CHECK_MODES.includes(normalized)) return normalized;
2124
+ throw createCliError(
2125
+ 'EFFECT_CHECK_MODE_INVALID',
2126
+ `Unsupported --effect-check "${value}". Supported: ${EFFECT_CHECK_MODES.join(', ')}.`
2127
+ );
2128
+ }
2129
+
2130
+ async function sleepMs(ms) {
2131
+ const delay = Number(ms || 0);
2132
+ if (!Number.isFinite(delay) || delay <= 0) return;
2133
+ await new Promise((resolve) => setTimeout(resolve, delay));
2134
+ }
2135
+
2136
+ async function resolveEffectCheckResult({
2137
+ mode,
2138
+ rpcUrl,
2139
+ walletAddress,
2140
+ tokenIn,
2141
+ tokenOut,
2142
+ balancesBefore,
2143
+ retries,
2144
+ waitMs,
2145
+ }) {
2146
+ const baseResult = {
2147
+ checked: true,
2148
+ mode,
2149
+ hasEffect: false,
2150
+ inDeltaRaw: '0',
2151
+ outDeltaRaw: '0',
2152
+ attempts: 0,
2153
+ retries: Math.max(0, Number(retries || 0)),
2154
+ };
2155
+ if (mode === 'off') {
2156
+ return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'disabled' };
2157
+ }
2158
+ if (!tokenIn || !tokenOut || !balancesBefore) {
2159
+ return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'token_pair_missing' };
2160
+ }
2161
+ const maxAttempts = Math.max(1, Number(retries || 0) + 1);
2162
+ let last = null;
2163
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2164
+ const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut);
2165
+ const inDeltaRaw = (BigInt(balancesBefore.tokenIn) - BigInt(balancesAfter.tokenIn)).toString();
2166
+ const outDeltaRaw = (BigInt(balancesAfter.tokenOut) - BigInt(balancesBefore.tokenOut)).toString();
2167
+ const hasEffect = BigInt(inDeltaRaw) > 0n || BigInt(outDeltaRaw) > 0n;
2168
+ last = {
2169
+ checked: true,
2170
+ mode,
2171
+ hasEffect,
2172
+ inDeltaRaw,
2173
+ outDeltaRaw,
2174
+ attempts: attempt,
2175
+ retries: maxAttempts - 1,
2176
+ };
2177
+ if (hasEffect) return last;
2178
+ if (attempt < maxAttempts) await sleepMs(waitMs);
2179
+ }
2180
+ return {
2181
+ ...(last || baseResult),
2182
+ warning: 'No balance delta observed after retries; treating as non-fatal in relaxed mode.',
2183
+ };
2184
+ }
2185
+
2186
+ async function performSelfBroadcast(ctx, flags, resolvedAction, selfResult) {
2187
+ const txd = resolveUnsignedTx(selfResult);
2188
+ if (!txd?.to || !txd?.data) {
2189
+ throw createCliError('SELF_UNSIGNED_TX_MISSING', 'Self execution did not return a valid unsigned transaction');
2190
+ }
2191
+ const chainId = Number(txd.chainId || flags.chainId || 8453);
2192
+ const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
2193
+ const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').trim();
2194
+ const rpcUrl = String(
2195
+ flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org'
2196
+ ).trim();
2197
+ if (!walletAddress) {
2198
+ throw createCliError('SIGNING_FAILED', 'Missing --wallet-address (or MARKET_MAKER_WALLET_ADDRESS) for self-exec signing.');
2199
+ }
2200
+ if (!owsWalletName) {
2201
+ throw createCliError('SIGNING_FAILED', 'Missing --ows-wallet-name (or OWS_WALLET_NAME) for self-exec signing.');
2202
+ }
2203
+
2204
+ const params = getPreparedParams(flags);
2205
+ const effectTokenIn = String(flags.effectTokenIn || params.tokenIn || '').trim();
2206
+ const effectTokenOut = String(flags.effectTokenOut || params.tokenOut || '').trim();
2207
+ const effectMode = normalizeEffectCheckMode(flags.effectCheck || process.env.THIRDFY_EFFECT_CHECK || 'relaxed');
2208
+ const effectRetries = Number(flags.effectCheckRetries || process.env.THIRDFY_EFFECT_CHECK_RETRIES || 2);
2209
+ const effectWaitMs = Number(flags.effectCheckWaitMs || process.env.THIRDFY_EFFECT_CHECK_WAIT_MS || 1200);
2210
+
2211
+ let balancesBefore = null;
2212
+ if (effectMode !== 'off' && effectTokenIn && effectTokenOut) {
2213
+ balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
2214
+ }
2215
+
2216
+ let submission;
2217
+ try {
2218
+ submission = await signAndBroadcastWithOws({
2219
+ txd,
2220
+ chainId,
2221
+ walletAddress,
2222
+ owsWalletName,
2223
+ rpcUrl,
2224
+ });
2225
+ } catch (error) {
2226
+ throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
2227
+ }
2228
+
2229
+ let receipt;
2230
+ try {
2231
+ receipt = await submission.sent.wait();
2232
+ } catch (error) {
2233
+ throw createCliError('BROADCAST_FAILED', error?.message || 'Broadcast wait failed');
2234
+ }
2235
+ const status = Number(receipt?.status ?? 0);
2236
+ if (status !== 1) {
2237
+ throw createCliError('BROADCAST_FAILED', 'Transaction receipt status is not success', {
2238
+ receiptStatus: receipt?.status ?? null,
2239
+ txHash: submission.sent.hash,
2240
+ });
2241
+ }
2242
+
2243
+ const effectCheck = await resolveEffectCheckResult({
2244
+ mode: effectMode,
2245
+ rpcUrl,
2246
+ walletAddress,
2247
+ tokenIn: effectTokenIn,
2248
+ tokenOut: effectTokenOut,
2249
+ balancesBefore,
2250
+ retries: effectRetries,
2251
+ waitMs: effectWaitMs,
2252
+ });
2253
+ if (effectCheck.checked && !effectCheck.hasEffect && effectMode === 'strict') {
2254
+ throw createCliError('EFFECT_CHECK_FAILED', 'Transaction mined but did not produce observable token effect.', {
2255
+ txHash: submission.sent.hash,
2256
+ effectCheck,
2257
+ mode: effectMode,
2258
+ });
2259
+ }
2260
+
2261
+ return {
2262
+ txHash: submission.sent.hash,
2263
+ chainId,
2264
+ blockNumber: receipt?.blockNumber ?? null,
2265
+ status: receipt?.status ?? null,
2266
+ unsignedTx: txd,
2267
+ effectCheck,
2268
+ lane: 'self',
2269
+ signer: 'ows',
2270
+ resolvedAction,
2271
+ apiBase: ctx.apiBase,
2272
+ };
2273
+ }
2274
+
2051
2275
  async function loadEthers() {
2052
2276
  const candidates = ['ethers', path.join(process.cwd(), 'node_modules', 'ethers')];
2053
2277
  for (const candidate of candidates) {
@@ -2242,8 +2466,8 @@ Core commands:
2242
2466
  thirdfy-agent catalogs list [--json]
2243
2467
  thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--chain-id <id>] [--run-mode <mode>] [--json]
2244
2468
  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]
2469
+ thirdfy-agent delegation init-custodial [--auth-token <token> | --owner-session-token <token>] [--chain-id <id>] [--json]
2470
+ 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
2471
  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
2472
  thirdfy-agent delegation status --auth-token <token> --agent-key <key> [--verify] [--json]
2249
2473
  thirdfy-agent delegation show --auth-token <token> --agent-key <key> [--verify] [--json]
@@ -2251,6 +2475,9 @@ Core commands:
2251
2475
  thirdfy-agent delegation balance --auth-token <token> --agent-key <key> [--chain-id <id>] [--json]
2252
2476
  thirdfy-agent delegation revoke --auth-token <token> --agent-key <key> [--reason <text>] [--chain-id <id>] [--json]
2253
2477
  thirdfy-agent delegation redeem --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--estimated-amount-usd <usd>] [--json]
2478
+ thirdfy-agent managed wallet init [--auth-token <token> | --owner-session-token <token>] [--chain-id <id>] [--json]
2479
+ 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
+ thirdfy-agent managed run --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--idempotency-key <key>] [--json]
2254
2481
  thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
2255
2482
  thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
2256
2483
  thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
@@ -2260,8 +2487,9 @@ Core commands:
2260
2487
  thirdfy-agent agent key rotate [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
2261
2488
  thirdfy-agent agent key revoke [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
2262
2489
  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]
2490
+ thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--broadcast] [--idempotency-key <key>] [--json]
2491
+ 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]
2492
+ thirdfy-agent doctor self [--wallet-address <addr>] [--ows-wallet-name <name>] [--rpc-url <url>] [--json]
2265
2493
  thirdfy-agent jeff preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
2266
2494
  thirdfy-agent jeff trade --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
2267
2495
  thirdfy-agent jeff status --intent-id <id> [--json]
@@ -2276,6 +2504,8 @@ Global flags:
2276
2504
  --env <file> load env vars from file
2277
2505
  --timeout <ms> request timeout
2278
2506
  --params <json> for swap: exactly one of {amountInRaw} or {amountInHuman + tokenInDecimals}
2507
+ --broadcast with run --run-mode self, sign and broadcast using OWS
2508
+ --effect-check <mode> self-exec check mode: strict | relaxed | off (default: relaxed)
2279
2509
  --owner-session-token <token> owner session token (or THIRDFY_OWNER_SESSION_TOKEN)
2280
2510
  --json stable machine-readable output
2281
2511
  --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.8",
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
  }