@thirdfy/agent-cli 0.1.6 → 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.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { randomUUID } from 'crypto';
4
+ import { spawnSync } from 'child_process';
4
5
  import fs from 'fs';
5
6
  import os from 'os';
6
7
  import path from 'path';
@@ -18,6 +19,7 @@ const DEFAULT_API_BASE = 'https://api.thirdfy.com';
18
19
  const DEFAULT_TIMEOUT_MS = 30000;
19
20
  const DEFAULT_CATALOG_CACHE_TTL_MS = 60_000;
20
21
  const RUN_MODES = ['thirdfy', 'self', 'hybrid'];
22
+ const EFFECT_CHECK_MODES = ['strict', 'relaxed', 'off'];
21
23
  const PROFILE_DEFAULTS = {
22
24
  personal: 'self',
23
25
  builder: 'hybrid',
@@ -105,9 +107,21 @@ async function main() {
105
107
  case 'config get':
106
108
  await commandProfileShow(context, subFlags);
107
109
  return;
110
+ case 'config set':
111
+ await commandConfigSet(context, subFlags);
112
+ return;
113
+ case 'login':
114
+ await commandLogin(context, subFlags);
115
+ return;
116
+ case 'logout':
117
+ await commandLogout(context, subFlags);
118
+ return;
108
119
  case 'whoami':
109
120
  await commandProfileShow(context, subFlags);
110
121
  return;
122
+ case 'doctor self':
123
+ await commandDoctorSelf(context, subFlags);
124
+ return;
111
125
  default:
112
126
  break;
113
127
  }
@@ -115,6 +129,9 @@ async function main() {
115
129
  const capabilities = await negotiateCapabilities(context);
116
130
 
117
131
  switch (commandKey) {
132
+ case 'managed run':
133
+ await commandManagedRun(context, subFlags, capabilities);
134
+ return;
118
135
  case 'jeff trade':
119
136
  await commandRun(context, subFlags, capabilities);
120
137
  return;
@@ -133,12 +150,33 @@ async function main() {
133
150
  case 'delegation create':
134
151
  await commandDelegationCreate(context, subFlags, capabilities);
135
152
  return;
153
+ case 'delegation init-custodial':
154
+ await commandDelegationInitCustodial(context, subFlags, capabilities);
155
+ return;
156
+ case 'delegation custodial-grant':
157
+ await commandDelegationCustodialGrant(context, subFlags, capabilities);
158
+ return;
136
159
  case 'delegation activate':
137
160
  await commandDelegationActivate(context, subFlags, capabilities);
138
161
  return;
139
162
  case 'delegation status':
140
163
  await commandDelegationStatus(context, subFlags, capabilities);
141
164
  return;
165
+ case 'delegation show':
166
+ await commandDelegationStatus(context, subFlags, capabilities);
167
+ return;
168
+ case 'delegation inspect':
169
+ await commandDelegationInspect(context, subFlags, capabilities);
170
+ return;
171
+ case 'delegation balance':
172
+ await commandDelegationBalance(context, subFlags, capabilities);
173
+ return;
174
+ case 'delegation revoke':
175
+ await commandDelegationRevoke(context, subFlags, capabilities);
176
+ return;
177
+ case 'delegation redeem':
178
+ await commandDelegationRedeem(context, subFlags, capabilities);
179
+ return;
142
180
  case 'credentials upsert':
143
181
  await commandCredentialsUpsert(context, subFlags, capabilities);
144
182
  return;
@@ -148,6 +186,9 @@ async function main() {
148
186
  case 'agent register':
149
187
  await commandAgentRegister(context, subFlags, capabilities);
150
188
  return;
189
+ case 'developer bootstrap':
190
+ await commandDeveloperBootstrap(context, subFlags, capabilities);
191
+ return;
151
192
  default:
152
193
  break;
153
194
  }
@@ -167,6 +208,14 @@ async function main() {
167
208
  await commandAgentAuthVerify(context, subFlags, capabilities);
168
209
  return;
169
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
+ }
170
219
 
171
220
  const rootCommand = parsed.positionals[0];
172
221
  switch (rootCommand) {
@@ -182,6 +231,9 @@ async function main() {
182
231
  case 'run':
183
232
  await commandRun(context, subFlags, capabilities);
184
233
  return;
234
+ case 'self-exec':
235
+ await commandSelfExec(context, subFlags, capabilities);
236
+ return;
185
237
  case 'intent-status':
186
238
  await commandIntentStatus(context, subFlags, capabilities);
187
239
  return;
@@ -256,6 +308,162 @@ function parseJsonFlag(flags, key) {
256
308
  }
257
309
  }
258
310
 
311
+ function shouldApplySwapAmountContract(actionName) {
312
+ const action = String(actionName || '').trim().toLowerCase();
313
+ return action === 'swap' || /(^|:|-)swap$/.test(action);
314
+ }
315
+
316
+ function createCliError(code, message, payload = {}) {
317
+ const error = new Error(message);
318
+ error.payload = {
319
+ error: code,
320
+ blockedReason: code,
321
+ blockedStage: 'client_validation',
322
+ ...payload,
323
+ };
324
+ return error;
325
+ }
326
+
327
+ function normalizeIntegerRawAmount(input, code = 'AMOUNT_UNIT_INVALID') {
328
+ const raw = String(input ?? '').trim();
329
+ if (!/^\d+$/.test(raw)) {
330
+ throw createCliError(code, 'amountInRaw must be an integer base-unit string (example: "1000000").');
331
+ }
332
+ return raw;
333
+ }
334
+
335
+ function decimalToRawUnits(amountHuman, decimals) {
336
+ const s = String(amountHuman ?? '').trim();
337
+ if (!/^\d+(\.\d+)?$/.test(s)) {
338
+ throw createCliError('AMOUNT_UNIT_INVALID', 'amountInHuman must be a decimal string (example: "100.5").');
339
+ }
340
+ const [whole, frac = ''] = s.split('.');
341
+ if (decimals === 0) {
342
+ if (frac && /[1-9]/.test(frac)) {
343
+ throw createCliError(
344
+ 'AMOUNT_PRECISION_EXCESS',
345
+ 'amountInHuman has fractional digits but tokenInDecimals is 0 (example: use "2" not "2.5").'
346
+ );
347
+ }
348
+ } else if (frac.length > decimals && /[1-9]/.test(frac.slice(decimals))) {
349
+ throw createCliError(
350
+ 'AMOUNT_PRECISION_EXCESS',
351
+ `amountInHuman has more than ${decimals} fractional digit(s) for tokenInDecimals; round or use amountInRaw.`
352
+ );
353
+ }
354
+ const fracNorm = (frac + '0'.repeat(decimals)).slice(0, decimals);
355
+ const wholePart = BigInt(whole || '0') * 10n ** BigInt(decimals);
356
+ const fracPart = BigInt(fracNorm || '0');
357
+ return (wholePart + fracPart).toString();
358
+ }
359
+
360
+ function normalizeSwapAmountContract(params) {
361
+ const hasRaw = params.amountInRaw !== undefined && params.amountInRaw !== null && String(params.amountInRaw).trim() !== '';
362
+ const hasHuman = params.amountInHuman !== undefined && params.amountInHuman !== null && String(params.amountInHuman).trim() !== '';
363
+ const hasLegacy = params.amountIn !== undefined && params.amountIn !== null && String(params.amountIn).trim() !== '';
364
+ const populated = [hasRaw, hasHuman, hasLegacy].filter(Boolean).length;
365
+ if (populated === 0) {
366
+ throw createCliError(
367
+ 'AMOUNT_UNIT_MISSING',
368
+ 'Missing swap amount: provide one of amountInRaw, amountInHuman, or legacy amountIn.'
369
+ );
370
+ }
371
+ if (populated > 1) {
372
+ throw createCliError(
373
+ 'AMOUNT_UNIT_AMBIGUOUS',
374
+ 'Provide exactly one of amountInRaw, amountInHuman, or legacy amountIn.'
375
+ );
376
+ }
377
+
378
+ if (hasRaw) {
379
+ const amountInRaw = normalizeIntegerRawAmount(params.amountInRaw);
380
+ return {
381
+ params: {
382
+ ...params,
383
+ amountInRaw,
384
+ amountIn: amountInRaw,
385
+ },
386
+ swapInput: {
387
+ amountInRaw,
388
+ amountInHuman: null,
389
+ tokenInDecimals: null,
390
+ unitSource: 'raw',
391
+ },
392
+ };
393
+ }
394
+
395
+ if (hasLegacy) {
396
+ const amountInRaw = normalizeIntegerRawAmount(params.amountIn, 'AMOUNT_UNIT_INVALID');
397
+ return {
398
+ params: {
399
+ ...params,
400
+ amountInRaw,
401
+ amountIn: amountInRaw,
402
+ },
403
+ swapInput: {
404
+ amountInRaw,
405
+ amountInHuman: null,
406
+ tokenInDecimals: null,
407
+ unitSource: 'legacy_raw',
408
+ },
409
+ };
410
+ }
411
+
412
+ const hasDecimals =
413
+ params.tokenInDecimals !== undefined &&
414
+ params.tokenInDecimals !== null &&
415
+ String(params.tokenInDecimals).trim() !== '';
416
+ if (!hasDecimals) {
417
+ throw createCliError(
418
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
419
+ 'amountInHuman requires tokenInDecimals (integer 0-36).'
420
+ );
421
+ }
422
+ const decimals = Number(params.tokenInDecimals);
423
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
424
+ throw createCliError(
425
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
426
+ 'amountInHuman requires tokenInDecimals (integer 0-36).'
427
+ );
428
+ }
429
+ const amountInHuman = String(params.amountInHuman).trim();
430
+ const amountInRaw = decimalToRawUnits(amountInHuman, decimals);
431
+ return {
432
+ params: {
433
+ ...params,
434
+ amountInHuman,
435
+ amountInRaw,
436
+ amountIn: amountInRaw,
437
+ tokenInDecimals: decimals,
438
+ },
439
+ swapInput: {
440
+ amountInRaw,
441
+ amountInHuman,
442
+ tokenInDecimals: decimals,
443
+ unitSource: 'human',
444
+ },
445
+ };
446
+ }
447
+
448
+ function prepareActionParamsForFlags(flags, resolvedAction) {
449
+ const parsedParams = parseJsonFlag(flags, 'params');
450
+ if (!shouldApplySwapAmountContract(resolvedAction)) {
451
+ flags.__preparedParams = parsedParams;
452
+ flags.__swapInput = null;
453
+ return;
454
+ }
455
+ const normalized = normalizeSwapAmountContract(parsedParams);
456
+ flags.__preparedParams = normalized.params;
457
+ flags.__swapInput = normalized.swapInput;
458
+ }
459
+
460
+ function getPreparedParams(flags) {
461
+ if (flags.__preparedParams && typeof flags.__preparedParams === 'object') {
462
+ return flags.__preparedParams;
463
+ }
464
+ return parseJsonFlag(flags, 'params');
465
+ }
466
+
259
467
  async function commandCatalogsList(ctx, flags, capabilities) {
260
468
  const response = await apiGet(ctx, '/api/v1/agent/actions/catalog');
261
469
  const actions = extractActions(response);
@@ -306,6 +514,7 @@ async function commandActions(ctx, flags, capabilities) {
306
514
 
307
515
  async function commandPreflight(ctx, flags, capabilities) {
308
516
  const resolved = await resolveActionSelection(ctx, flags);
517
+ prepareActionParamsForFlags(flags, resolved.resolvedAction);
309
518
  const runMode = getEffectiveRunMode(ctx, flags);
310
519
  enforceChainCompatibility(capabilities, flags, runMode);
311
520
  const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved.resolvedAction);
@@ -322,6 +531,7 @@ async function commandPreflight(ctx, flags, capabilities) {
322
531
  resolvedAction: resolved.resolvedAction,
323
532
  runMode,
324
533
  profile: ctx.profile || null,
534
+ swapInput: flags.__swapInput || null,
325
535
  capabilitiesVersion: capabilities?.contractVersion || null,
326
536
  },
327
537
  });
@@ -330,6 +540,7 @@ async function commandPreflight(ctx, flags, capabilities) {
330
540
 
331
541
  async function commandRun(ctx, flags, capabilities) {
332
542
  const resolved = await resolveActionSelection(ctx, flags);
543
+ prepareActionParamsForFlags(flags, resolved.resolvedAction);
333
544
  const runMode = getEffectiveRunMode(ctx, flags);
334
545
  enforceChainCompatibility(capabilities, flags, runMode);
335
546
  const skipPreflight = Boolean(flags.skipPreflight);
@@ -337,6 +548,26 @@ async function commandRun(ctx, flags, capabilities) {
337
548
  skipPreflight,
338
549
  });
339
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
+ }
340
571
  printEnvelope({
341
572
  success: normalized.success,
342
573
  code: backendResult.code,
@@ -350,6 +581,7 @@ async function commandRun(ctx, flags, capabilities) {
350
581
  runMode,
351
582
  profile: ctx.profile || null,
352
583
  skippedPreflight: skipPreflight,
584
+ swapInput: flags.__swapInput || null,
353
585
  capabilitiesVersion: capabilities?.contractVersion || null,
354
586
  },
355
587
  });
@@ -392,12 +624,15 @@ async function commandCreditsBalance(ctx, flags, capabilities) {
392
624
  }
393
625
 
394
626
  async function commandDelegationCreate(ctx, flags, capabilities) {
395
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for delegation create');
627
+ const authHeaders = buildOwnerAuthHeaders(
628
+ flags,
629
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation create'
630
+ );
396
631
  let sessionAccountAddress = String(flags.sessionAccountAddress || '').trim();
397
632
  if (!sessionAccountAddress) {
398
633
  try {
399
634
  const executorAddressResponse = await apiGet(ctx, '/api/v1/agent/delegation/executor-address', {
400
- Authorization: `Bearer ${authToken}`,
635
+ ...authHeaders,
401
636
  });
402
637
  sessionAccountAddress = String(executorAddressResponse?.data?.executorAddress || '').trim();
403
638
  } catch (error) {
@@ -419,7 +654,7 @@ async function commandDelegationCreate(ctx, flags, capabilities) {
419
654
  expirySeconds: Number(flags.expirySeconds || 86_400),
420
655
  };
421
656
  const response = await apiPost(ctx, '/api/v1/agent/delegation/create', payload, {
422
- Authorization: `Bearer ${authToken}`,
657
+ ...authHeaders,
423
658
  });
424
659
  const success = Boolean(response?.success !== false);
425
660
  printEnvelope({
@@ -434,8 +669,74 @@ async function commandDelegationCreate(ctx, flags, capabilities) {
434
669
  });
435
670
  }
436
671
 
672
+ async function commandDelegationInitCustodial(ctx, flags, capabilities) {
673
+ const authHeaders = buildOwnerAuthHeaders(
674
+ flags,
675
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation init-custodial'
676
+ );
677
+ const payload = {
678
+ chainId: Number(flags.chainId || 8453),
679
+ };
680
+ const response = await requestWithRouteFallback(ctx, {
681
+ method: 'POST',
682
+ routes: ['/api/v1/agent/delegation/init-custodial'],
683
+ body: payload,
684
+ headers: {
685
+ ...authHeaders,
686
+ },
687
+ });
688
+ const success = Boolean(response?.success !== false);
689
+ printEnvelope({
690
+ success,
691
+ code: success ? 'DELEGATION_CUSTODIAL_INITIALIZED' : 'DELEGATION_CUSTODIAL_INIT_FAILED',
692
+ message: response?.message || (success ? 'Custodial wallet initialized' : 'Custodial wallet init failed'),
693
+ data: response,
694
+ meta: {
695
+ apiBase: ctx.apiBase,
696
+ capabilitiesVersion: capabilities?.contractVersion || null,
697
+ },
698
+ });
699
+ }
700
+
701
+ async function commandDelegationCustodialGrant(ctx, flags, capabilities) {
702
+ const authHeaders = buildOwnerAuthHeaders(
703
+ flags,
704
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation custodial-grant'
705
+ );
706
+ const payload = {
707
+ chainId: Number(flags.chainId || 8453),
708
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
709
+ tokenAddress: requireFlag(flags, 'tokenAddress', 'Missing --token-address'),
710
+ maxUsdPerDay: Number(requireFlag(flags, 'maxUsdPerDay', 'Missing --max-usd-per-day')),
711
+ periodDuration: Number(flags.periodDuration || 86_400),
712
+ expirySeconds: Number(flags.expirySeconds || 86_400),
713
+ };
714
+ const response = await requestWithRouteFallback(ctx, {
715
+ method: 'POST',
716
+ routes: ['/api/v1/agent/delegation/custodial-grant'],
717
+ body: payload,
718
+ headers: {
719
+ ...authHeaders,
720
+ },
721
+ });
722
+ const success = Boolean(response?.success !== false);
723
+ printEnvelope({
724
+ success,
725
+ code: success ? 'DELEGATION_CUSTODIAL_GRANTED' : 'DELEGATION_CUSTODIAL_GRANT_FAILED',
726
+ message: response?.message || (success ? 'Custodial delegation granted' : 'Custodial delegation grant failed'),
727
+ data: response,
728
+ meta: {
729
+ apiBase: ctx.apiBase,
730
+ capabilitiesVersion: capabilities?.contractVersion || null,
731
+ },
732
+ });
733
+ }
734
+
437
735
  async function commandDelegationActivate(ctx, flags, capabilities) {
438
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for delegation activate');
736
+ const authHeaders = buildOwnerAuthHeaders(
737
+ flags,
738
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation activate'
739
+ );
439
740
  const payload = {
440
741
  agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
441
742
  walletAddress: requireFlag(flags, 'walletAddress', 'Missing --wallet-address'),
@@ -454,7 +755,7 @@ async function commandDelegationActivate(ctx, flags, capabilities) {
454
755
  if (flags.permissionData) payload.permissionData = parseJsonFlag(flags, 'permissionData');
455
756
  if (flags.modelAConstraints) payload.modelAConstraints = parseJsonFlag(flags, 'modelAConstraints');
456
757
  const response = await apiPost(ctx, '/api/v1/agent/delegation/activate', payload, {
457
- Authorization: `Bearer ${authToken}`,
758
+ ...authHeaders,
458
759
  });
459
760
  const success = Boolean(response?.success !== false);
460
761
  printEnvelope({
@@ -470,7 +771,10 @@ async function commandDelegationActivate(ctx, flags, capabilities) {
470
771
  }
471
772
 
472
773
  async function commandDelegationStatus(ctx, flags, capabilities) {
473
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for delegation status');
774
+ const authHeaders = buildOwnerAuthHeaders(
775
+ flags,
776
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation status'
777
+ );
474
778
  const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
475
779
  const chainId = Number(flags.chainId || 8453);
476
780
  const verify = Boolean(flags.verify);
@@ -478,7 +782,7 @@ async function commandDelegationStatus(ctx, flags, capabilities) {
478
782
  ? `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`
479
783
  : `/api/v1/agent/delegation/check?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
480
784
  const response = await apiGet(ctx, route, {
481
- Authorization: `Bearer ${authToken}`,
785
+ ...authHeaders,
482
786
  });
483
787
  const normalized = normalizeDelegationStatus(response);
484
788
  printEnvelope({
@@ -495,6 +799,129 @@ async function commandDelegationStatus(ctx, flags, capabilities) {
495
799
  });
496
800
  }
497
801
 
802
+ async function commandDelegationInspect(ctx, flags, capabilities) {
803
+ const authHeaders = buildOwnerAuthHeaders(
804
+ flags,
805
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation inspect'
806
+ );
807
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
808
+ const chainId = Number(flags.chainId || 8453);
809
+ const includeOnchain = Boolean(flags.verify || flags.includeOnchain || flags.onchain);
810
+ const checkRoute = `/api/v1/agent/delegation/check?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
811
+ const checkResponse = await apiGet(ctx, checkRoute, {
812
+ ...authHeaders,
813
+ });
814
+ const checkNormalized = normalizeDelegationStatus(checkResponse);
815
+
816
+ let verifyNormalized = null;
817
+ if (includeOnchain) {
818
+ const verifyRoute = `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
819
+ const verifyResponse = await apiGet(ctx, verifyRoute, {
820
+ ...authHeaders,
821
+ });
822
+ verifyNormalized = normalizeDelegationStatus(verifyResponse);
823
+ }
824
+
825
+ printEnvelope({
826
+ success: true,
827
+ code: 'DELEGATION_INSPECT_OK',
828
+ message: includeOnchain ? 'Delegation inspect loaded (with onchain verification)' : 'Delegation inspect loaded',
829
+ data: {
830
+ check: checkNormalized,
831
+ verify: verifyNormalized,
832
+ state: verifyNormalized?.state || checkNormalized.state,
833
+ rawStatus: verifyNormalized?.rawStatus || checkNormalized.rawStatus,
834
+ },
835
+ meta: {
836
+ apiBase: ctx.apiBase,
837
+ includeOnchain,
838
+ governanceState: verifyNormalized?.state || checkNormalized.state || null,
839
+ capabilitiesVersion: capabilities?.contractVersion || null,
840
+ },
841
+ });
842
+ }
843
+
844
+ async function commandDelegationRevoke(ctx, flags, capabilities) {
845
+ const authHeaders = buildOwnerAuthHeaders(
846
+ flags,
847
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation revoke'
848
+ );
849
+ const payload = {
850
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
851
+ reason: String(flags.reason || 'user_revoked').trim(),
852
+ chainId: Number(flags.chainId || 8453),
853
+ };
854
+ const response = await requestWithRouteFallback(ctx, {
855
+ method: 'POST',
856
+ routes: ['/api/v1/agent/delegation/revoke'],
857
+ body: payload,
858
+ headers: {
859
+ ...authHeaders,
860
+ },
861
+ });
862
+ const success = Boolean(response?.success !== false);
863
+ printEnvelope({
864
+ success,
865
+ code: success ? 'DELEGATION_REVOKED' : 'DELEGATION_REVOKE_FAILED',
866
+ message: response?.message || (success ? 'Delegation revoked' : 'Delegation revoke failed'),
867
+ data: response,
868
+ meta: {
869
+ apiBase: ctx.apiBase,
870
+ capabilitiesVersion: capabilities?.contractVersion || null,
871
+ },
872
+ });
873
+ }
874
+
875
+ async function commandDelegationBalance(ctx, flags, capabilities) {
876
+ const authHeaders = buildOwnerAuthHeaders(
877
+ flags,
878
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation balance'
879
+ );
880
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
881
+ const chainId = Number(flags.chainId || 8453);
882
+ const route = `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
883
+ const response = await apiGet(ctx, route, {
884
+ ...authHeaders,
885
+ });
886
+ const normalized = normalizeDelegationStatus(response);
887
+ const verifyData = normalized?.data?.onChainVerification || {};
888
+ printEnvelope({
889
+ success: true,
890
+ code: 'DELEGATION_BALANCE_OK',
891
+ message: 'Delegation allowance balance loaded',
892
+ data: {
893
+ state: normalized.state,
894
+ rawStatus: normalized.rawStatus,
895
+ delegatedWalletAddress: normalized?.data?.delegatedWalletAddress || null,
896
+ periodAvailableUsd: verifyData?.periodAvailableUsd ?? null,
897
+ periodAvailableBaseUnits: verifyData?.periodAvailableBaseUnits ?? null,
898
+ verified: verifyData?.verified ?? null,
899
+ isRevoked: verifyData?.isRevoked ?? null,
900
+ onChainVerification: verifyData || null,
901
+ },
902
+ meta: {
903
+ apiBase: ctx.apiBase,
904
+ governanceState: normalized.state || null,
905
+ capabilitiesVersion: capabilities?.contractVersion || null,
906
+ },
907
+ });
908
+ }
909
+
910
+ async function commandDelegationRedeem(ctx, flags, capabilities) {
911
+ const selectedRunMode = getEffectiveRunMode(ctx, flags);
912
+ if (!['thirdfy', 'hybrid'].includes(selectedRunMode)) {
913
+ throw createCliError('DELEGATION_REDEEM_MODE_INVALID', 'delegation redeem supports only --run-mode thirdfy|hybrid');
914
+ }
915
+ const delegatedFlags = {
916
+ ...flags,
917
+ runMode: selectedRunMode,
918
+ };
919
+ if (!delegatedFlags.estimatedAmountUsd) {
920
+ delegatedFlags.estimatedAmountUsd = '5';
921
+ }
922
+ await commandRun(ctx, delegatedFlags, capabilities);
923
+ }
924
+
498
925
  async function commandCredentialsUpsert(ctx, flags, capabilities) {
499
926
  const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials upsert');
500
927
  const payload = {
@@ -587,11 +1014,83 @@ async function commandAgentRegister(ctx, flags, capabilities) {
587
1014
  ? '/api/v1/agent/onboarding/register-with-wallet'
588
1015
  : '/api/v1/agent/onboarding/register-with-privy';
589
1016
  const response = await apiPost(ctx, route, payload, ownerHeaders);
1017
+ if (!flags.__suppressOutput) {
1018
+ printEnvelope({
1019
+ success: true,
1020
+ code: 'AGENT_REGISTERED',
1021
+ message: 'Agent registered',
1022
+ data: response,
1023
+ meta: {
1024
+ apiBase: ctx.apiBase,
1025
+ capabilitiesVersion: capabilities?.contractVersion || null,
1026
+ },
1027
+ });
1028
+ }
1029
+ return response;
1030
+ }
1031
+
1032
+ async function commandDeveloperBootstrap(ctx, flags, capabilities) {
1033
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
1034
+ const name = requireFlag(flags, 'name', 'Missing --name');
1035
+ let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
1036
+ let usedWalletFlow = false;
1037
+ const challengeId = String(flags.challengeId || '').trim();
1038
+ const signature = String(flags.signature || '').trim();
1039
+
1040
+ if (!ownerSessionToken && signature) {
1041
+ if (!challengeId) {
1042
+ throw new Error('Missing --challenge-id for wallet bootstrap. Run `agent auth challenge` first.');
1043
+ }
1044
+ const verify = await apiPost(ctx, '/api/v1/agent/onboarding/wallet/verify', {
1045
+ challengeId,
1046
+ signature,
1047
+ agentKey,
1048
+ ...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
1049
+ });
1050
+ ownerSessionToken = String(verify?.ownerSessionToken || verify?.data?.ownerSessionToken || '').trim();
1051
+ usedWalletFlow = true;
1052
+ }
1053
+
1054
+ const registerFlags = {
1055
+ ...flags,
1056
+ agentKey,
1057
+ name,
1058
+ ...(ownerSessionToken ? { ownerSessionToken } : {}),
1059
+ };
1060
+ const registerResponse = await commandAgentRegister(
1061
+ ctx,
1062
+ { ...registerFlags, __suppressOutput: true },
1063
+ capabilities
1064
+ );
1065
+
1066
+ const config = loadProfileConfig();
1067
+ const next = {
1068
+ ...config,
1069
+ auth: {
1070
+ ...(config.auth && typeof config.auth === 'object' ? config.auth : {}),
1071
+ ...(ownerSessionToken ? { ownerSessionToken } : {}),
1072
+ lastLoginAt: new Date().toISOString(),
1073
+ source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
1074
+ },
1075
+ developer: {
1076
+ ...(config.developer && typeof config.developer === 'object' ? config.developer : {}),
1077
+ agentKey,
1078
+ bootstrapCompletedAt: new Date().toISOString(),
1079
+ },
1080
+ };
1081
+ persistProfileConfig(next);
590
1082
  printEnvelope({
591
1083
  success: true,
592
- code: 'AGENT_REGISTERED',
593
- message: 'Agent registered',
594
- data: response,
1084
+ code: 'DEVELOPER_BOOTSTRAP_OK',
1085
+ message: 'Developer bootstrap completed',
1086
+ data: {
1087
+ registration: registerResponse,
1088
+ persisted: {
1089
+ ownerSessionTokenSaved: Boolean(ownerSessionToken),
1090
+ source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
1091
+ configPath: getProfileConfigPath(),
1092
+ },
1093
+ },
595
1094
  meta: {
596
1095
  apiBase: ctx.apiBase,
597
1096
  capabilitiesVersion: capabilities?.contractVersion || null,
@@ -706,10 +1205,156 @@ async function commandPrompt(ctx, flags, capabilities, promptTokens) {
706
1205
  });
707
1206
  }
708
1207
 
1208
+ async function commandSelfExec(ctx, flags, capabilities) {
1209
+ const resolved = await resolveActionSelection(ctx, flags);
1210
+ prepareActionParamsForFlags(flags, resolved.resolvedAction);
1211
+ enforceChainCompatibility(capabilities, flags, 'self');
1212
+ const selfResult = await executeSelfRun(ctx, flags, resolved, {
1213
+ skipPreflight: Boolean(flags.skipPreflight),
1214
+ });
1215
+
1216
+ if (!selfResult?.success) {
1217
+ const blocked = Boolean(selfResult?.preflightBlocked);
1218
+ printEnvelope({
1219
+ success: false,
1220
+ code: blocked ? 'PREFLIGHT_BLOCKED' : 'SELF_EXECUTION_FAILED',
1221
+ message: blocked ? 'Preflight blocked self execution' : 'Self execution preparation failed',
1222
+ data: selfResult || {},
1223
+ meta: {
1224
+ apiBase: ctx.apiBase,
1225
+ runMode: 'self',
1226
+ resolvedAction: resolved.resolvedAction,
1227
+ },
1228
+ });
1229
+ process.exit(1);
1230
+ }
1231
+
1232
+ const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, selfResult);
1233
+
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) {
1249
+ const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
1250
+ const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').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 = [];
1256
+
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
+ });
1272
+
1273
+ try {
1274
+ resolveOwsBinary();
1275
+ checks.push({ name: 'ows-binary', ok: true, detail: 'found' });
1276
+ } catch (error) {
1277
+ checks.push({ name: 'ows-binary', ok: false, detail: error?.message || 'not found' });
1278
+ }
1279
+
1280
+ try {
1281
+ await loadEthers();
1282
+ checks.push({ name: 'ethers-runtime', ok: true, detail: 'loaded' });
1283
+ } catch (error) {
1284
+ checks.push({ name: 'ethers-runtime', ok: false, detail: error?.message || 'missing ethers dependency' });
1285
+ }
1286
+
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' });
1294
+ }
1295
+
1296
+ const ok = checks.every((entry) => entry.ok);
1297
+ printEnvelope({
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',
1301
+ data: {
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
+ ],
1307
+ },
1308
+ meta: {
1309
+ apiBase: ctx.apiBase,
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,
1340
+ resolvedAction: resolved.resolvedAction,
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,
1347
+ },
1348
+ });
1349
+ if (!normalized.success) process.exit(1);
1350
+ }
1351
+
709
1352
  async function commandProfileInit(ctx, flags) {
1353
+ const current = loadProfileConfig();
710
1354
  const profile = normalizeProfileName(flags.profile || flags.name || 'personal');
711
1355
  const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
712
1356
  persistProfileConfig({
1357
+ ...current,
713
1358
  profile,
714
1359
  runMode,
715
1360
  });
@@ -734,6 +1379,7 @@ async function commandProfileUse(ctx, flags) {
734
1379
  // `profile use` intentionally re-applies profile defaults unless caller pins a mode.
735
1380
  const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
736
1381
  persistProfileConfig({
1382
+ ...current,
737
1383
  profile,
738
1384
  runMode,
739
1385
  });
@@ -770,6 +1416,154 @@ async function commandProfileShow(ctx, _flags) {
770
1416
  });
771
1417
  }
772
1418
 
1419
+ async function commandConfigSet(ctx, flags) {
1420
+ const key = String(flags.key || '').trim();
1421
+ const value = flags.value;
1422
+ if (!key) {
1423
+ throw new Error('Missing --key for config set');
1424
+ }
1425
+ const canonicalKey = key.toLowerCase();
1426
+ const hasValue = !(value === undefined || String(value).trim() === '');
1427
+ if (canonicalKey === 'profile' && hasValue) {
1428
+ normalizeProfileName(value);
1429
+ }
1430
+ if (canonicalKey === 'runmode' && hasValue) {
1431
+ normalizeRunMode(value);
1432
+ }
1433
+
1434
+ const current = loadProfileConfig();
1435
+ const next = setNestedConfigValue(current, key, value);
1436
+ persistProfileConfig(next);
1437
+ printEnvelope({
1438
+ success: true,
1439
+ code: 'CONFIG_UPDATED',
1440
+ message: 'Configuration updated',
1441
+ data: {
1442
+ key,
1443
+ value: value === undefined ? null : String(value),
1444
+ configPath: getProfileConfigPath(),
1445
+ },
1446
+ meta: { apiBase: ctx.apiBase },
1447
+ });
1448
+ }
1449
+
1450
+ async function commandLogin(ctx, flags) {
1451
+ const current = loadProfileConfig();
1452
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
1453
+ let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
1454
+ let loginSource = 'token';
1455
+
1456
+ if (!authToken && !ownerSessionToken) {
1457
+ const challengeId = String(flags.challengeId || '').trim();
1458
+ const signature = String(flags.signature || '').trim();
1459
+ const agentKey = String(flags.agentKey || '').trim();
1460
+ if (challengeId && signature && agentKey) {
1461
+ const verifyResponse = await requestWithRouteFallback(ctx, {
1462
+ method: 'POST',
1463
+ routes: ['/api/v1/agent/onboarding/wallet/verify'],
1464
+ body: {
1465
+ challengeId,
1466
+ signature,
1467
+ agentKey,
1468
+ ...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
1469
+ },
1470
+ });
1471
+ ownerSessionToken = String(
1472
+ verifyResponse?.ownerSessionToken || verifyResponse?.data?.ownerSessionToken || ''
1473
+ ).trim();
1474
+ loginSource = 'wallet-verify';
1475
+ }
1476
+ }
1477
+
1478
+ if (!authToken && !ownerSessionToken) {
1479
+ throw new Error(
1480
+ 'Missing login credentials: provide --auth-token, --owner-session-token, or challenge verify inputs (--challenge-id/--signature/--agent-key).'
1481
+ );
1482
+ }
1483
+
1484
+ const next = {
1485
+ ...current,
1486
+ auth: {
1487
+ ...(current.auth && typeof current.auth === 'object' ? current.auth : {}),
1488
+ lastLoginAt: new Date().toISOString(),
1489
+ source: loginSource,
1490
+ },
1491
+ };
1492
+ if (authToken) {
1493
+ next.auth.authToken = authToken;
1494
+ delete next.auth.ownerSessionToken;
1495
+ } else if (ownerSessionToken) {
1496
+ next.auth.ownerSessionToken = ownerSessionToken;
1497
+ delete next.auth.authToken;
1498
+ }
1499
+ if (flags.agentApiKey) {
1500
+ next.agent = {
1501
+ ...(current.agent && typeof current.agent === 'object' ? current.agent : {}),
1502
+ apiKey: String(flags.agentApiKey).trim(),
1503
+ };
1504
+ }
1505
+ persistProfileConfig(next);
1506
+ printEnvelope({
1507
+ success: true,
1508
+ code: 'LOGIN_OK',
1509
+ message: 'Login credentials stored',
1510
+ data: {
1511
+ hasAuthToken: Boolean(authToken),
1512
+ hasOwnerSessionToken: Boolean(ownerSessionToken),
1513
+ hasAgentApiKey: Boolean(flags.agentApiKey),
1514
+ configPath: getProfileConfigPath(),
1515
+ },
1516
+ meta: { apiBase: ctx.apiBase },
1517
+ });
1518
+ }
1519
+
1520
+ async function commandLogout(ctx, flags) {
1521
+ const current = loadProfileConfig();
1522
+ const localOnly = flags.all ? false : true;
1523
+ const auth = current.auth && typeof current.auth === 'object' ? current.auth : {};
1524
+ const ownerSessionToken = String(auth.ownerSessionToken || '').trim();
1525
+ let revokeAttempted = false;
1526
+ let revokeSuccess = false;
1527
+ let revokeError = null;
1528
+
1529
+ if (!localOnly && ownerSessionToken) {
1530
+ revokeAttempted = true;
1531
+ try {
1532
+ await requestWithRouteFallback(ctx, {
1533
+ method: 'POST',
1534
+ routes: ['/api/v1/agent/onboarding/wallet/logout', '/api/v1/agent/onboarding/wallet/revoke-session'],
1535
+ body: {},
1536
+ headers: {
1537
+ 'x-owner-session-token': ownerSessionToken,
1538
+ },
1539
+ });
1540
+ revokeSuccess = true;
1541
+ } catch (error) {
1542
+ revokeError = error?.message || 'OWNER_SESSION_REVOKE_FAILED';
1543
+ }
1544
+ }
1545
+
1546
+ const next = { ...current };
1547
+ delete next.auth;
1548
+ if (flags.clearAgentKey) {
1549
+ delete next.agent;
1550
+ }
1551
+ persistProfileConfig(next);
1552
+ printEnvelope({
1553
+ success: true,
1554
+ code: 'LOGOUT_OK',
1555
+ message: 'Local credentials cleared',
1556
+ data: {
1557
+ localOnly,
1558
+ revokeAttempted,
1559
+ revokeSuccess,
1560
+ revokeError,
1561
+ configPath: getProfileConfigPath(),
1562
+ },
1563
+ meta: { apiBase: ctx.apiBase },
1564
+ });
1565
+ }
1566
+
773
1567
  function resolveProfileState(flags) {
774
1568
  const persisted = loadProfileConfig();
775
1569
  const profile = normalizeProfileName(flags.profile || process.env.THIRDFY_PROFILE || persisted.profile || 'personal');
@@ -800,6 +1594,14 @@ function normalizeRunMode(value) {
800
1594
  throw new Error(`Unsupported --run-mode "${value}". Supported: ${RUN_MODES.join(', ')}`);
801
1595
  }
802
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
+
803
1605
  function getProfileConfigPath() {
804
1606
  return path.join(os.homedir(), '.thirdfy', 'config.json');
805
1607
  }
@@ -818,8 +1620,60 @@ function loadProfileConfig() {
818
1620
  function persistProfileConfig(config) {
819
1621
  const configPath = getProfileConfigPath();
820
1622
  const dirPath = path.dirname(configPath);
821
- fs.mkdirSync(dirPath, { recursive: true });
822
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
1623
+ fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
1624
+ const payload = `${JSON.stringify(config, null, 2)}\n`;
1625
+ fs.writeFileSync(configPath, payload, { encoding: 'utf8', mode: 0o600 });
1626
+ try {
1627
+ fs.chmodSync(configPath, 0o600);
1628
+ } catch {
1629
+ /* chmod unsupported or restricted (e.g. some Windows FS) */
1630
+ }
1631
+ try {
1632
+ fs.chmodSync(dirPath, 0o700);
1633
+ } catch {
1634
+ /* ignore */
1635
+ }
1636
+ }
1637
+
1638
+ function clonePlainConfigTree(input) {
1639
+ const obj = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
1640
+ try {
1641
+ return structuredClone(obj);
1642
+ } catch {
1643
+ return JSON.parse(JSON.stringify(obj));
1644
+ }
1645
+ }
1646
+
1647
+ function setNestedConfigValue(config, keyPath, value) {
1648
+ const next = clonePlainConfigTree(config || {});
1649
+ const parts = String(keyPath || '')
1650
+ .split('.')
1651
+ .map((v) => v.trim())
1652
+ .filter(Boolean);
1653
+ const blocked = new Set(['__proto__', 'prototype', 'constructor']);
1654
+ if (parts.some((part) => blocked.has(part))) {
1655
+ throw createCliError(
1656
+ 'CONFIG_KEY_INVALID',
1657
+ 'Invalid config key path: reserved segments (__proto__, prototype, constructor) are not allowed.'
1658
+ );
1659
+ }
1660
+ if (!parts.length) return next;
1661
+ let cursor = next;
1662
+ for (let i = 0; i < parts.length - 1; i += 1) {
1663
+ const key = parts[i];
1664
+ const existing = cursor[key];
1665
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
1666
+ cursor[key] = {};
1667
+ }
1668
+ cursor = cursor[key];
1669
+ }
1670
+ const finalKey = parts[parts.length - 1];
1671
+ if (value === undefined || String(value).trim() === '') {
1672
+ delete cursor[finalKey];
1673
+ } else {
1674
+ cursor[finalKey] = String(value);
1675
+ }
1676
+ return next;
823
1677
  }
824
1678
 
825
1679
  function getEffectiveRunMode(ctx, flags) {
@@ -1030,10 +1884,11 @@ async function executeHybridRun(ctx, flags, resolved, options) {
1030
1884
  }
1031
1885
 
1032
1886
  function buildBuildTxPayload(flags, resolvedAction) {
1887
+ const runMode = String(flags.runMode || '').trim().toLowerCase() || 'self';
1033
1888
  const payload = {
1034
- agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
1889
+ agentApiKey: resolveAgentApiKey(flags, runMode),
1035
1890
  action: String(resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
1036
- params: parseJsonFlag(flags, 'params'),
1891
+ params: getPreparedParams(flags),
1037
1892
  };
1038
1893
  if (flags.chainId) payload.chainId = Number(flags.chainId);
1039
1894
  if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
@@ -1041,10 +1896,11 @@ function buildBuildTxPayload(flags, resolvedAction) {
1041
1896
  }
1042
1897
 
1043
1898
  function buildIntentPayload(flags, options) {
1899
+ const runMode = String(options.runMode || flags.runMode || '').trim().toLowerCase() || 'thirdfy';
1044
1900
  const payload = {
1045
- agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
1901
+ agentApiKey: resolveAgentApiKey(flags, runMode),
1046
1902
  action: String(options.resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
1047
- params: parseJsonFlag(flags, 'params'),
1903
+ params: getPreparedParams(flags),
1048
1904
  };
1049
1905
  if (flags.catalog) payload.catalog = String(flags.catalog).trim();
1050
1906
  if (flags.chainId) payload.chainId = Number(flags.chainId);
@@ -1065,6 +1921,22 @@ function buildIntentPayload(flags, options) {
1065
1921
  return payload;
1066
1922
  }
1067
1923
 
1924
+ function resolveAgentApiKey(flags, runMode) {
1925
+ const explicit = String(flags.agentApiKey || '').trim();
1926
+ if (explicit) return explicit;
1927
+ const envKey = String(process.env.THIRDFY_AGENT_API_KEY || '').trim();
1928
+ if (envKey) return envKey;
1929
+ const config = loadProfileConfig();
1930
+ const configKey = String(config?.agent?.apiKey || '').trim();
1931
+ if (configKey) return configKey;
1932
+ if (runMode === 'self') {
1933
+ throw new Error(
1934
+ 'SELF_OPEN_DISABLED: self mode without agent identity is not enabled on this API yet. Set --agent-api-key, THIRDFY_AGENT_API_KEY, or login/config default.'
1935
+ );
1936
+ }
1937
+ throw new Error('Missing --agent-api-key (or THIRDFY_AGENT_API_KEY / config.agent.apiKey)');
1938
+ }
1939
+
1068
1940
  function getActionKey(action) {
1069
1941
  return String(action?.action || action?.id || action?.key || action?.name || '')
1070
1942
  .trim();
@@ -1188,14 +2060,18 @@ async function resolveActionSelection(ctx, flags) {
1188
2060
  }
1189
2061
 
1190
2062
  function getAuthToken(flags, missingMessage) {
1191
- const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
2063
+ const config = loadProfileConfig();
2064
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || config?.auth?.authToken || '').trim();
1192
2065
  if (!authToken) throw new Error(missingMessage);
1193
2066
  return authToken;
1194
2067
  }
1195
2068
 
1196
2069
  function buildOwnerAuthHeaders(flags, missingMessage) {
1197
- const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
1198
- const ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
2070
+ const config = loadProfileConfig();
2071
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || config?.auth?.authToken || '').trim();
2072
+ const ownerSessionToken = String(
2073
+ flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || config?.auth?.ownerSessionToken || ''
2074
+ ).trim();
1199
2075
  if (!authToken && !ownerSessionToken) {
1200
2076
  throw new Error(missingMessage);
1201
2077
  }
@@ -1224,6 +2100,281 @@ async function negotiateCapabilities(ctx) {
1224
2100
  }
1225
2101
  }
1226
2102
 
2103
+ function resolveUnsignedTx(result) {
2104
+ if (result?.unsignedTx && typeof result.unsignedTx === 'object') return result.unsignedTx;
2105
+ if (result?.data?.unsignedTx && typeof result.data.unsignedTx === 'object') return result.data.unsignedTx;
2106
+ if (result?.details?.unsignedTx && typeof result.details.unsignedTx === 'object') return result.details.unsignedTx;
2107
+ return null;
2108
+ }
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
+
2275
+ async function loadEthers() {
2276
+ const candidates = ['ethers', path.join(process.cwd(), 'node_modules', 'ethers')];
2277
+ for (const candidate of candidates) {
2278
+ try {
2279
+ return require(candidate);
2280
+ } catch {
2281
+ // continue
2282
+ }
2283
+ }
2284
+ throw new Error('Could not load ethers dependency for self-exec');
2285
+ }
2286
+
2287
+ function resolveOwsBinary() {
2288
+ const explicit = String(process.env.OWS_CLI_PATH || '').trim();
2289
+ if (explicit && fs.existsSync(explicit)) return explicit;
2290
+ const localBin = path.join(process.cwd(), 'node_modules', '.bin', 'ows');
2291
+ if (fs.existsSync(localBin)) return localBin;
2292
+ const which = spawnSync('which', ['ows'], { encoding: 'utf-8' });
2293
+ const discovered = String(which.stdout || '').trim();
2294
+ if (discovered) return discovered;
2295
+ throw new Error('ows CLI not found. Install it or set OWS_CLI_PATH.');
2296
+ }
2297
+
2298
+ function signUnsignedTxWithOws(unsignedHex, walletName, chainId) {
2299
+ const passphrase = String(process.env.OWS_PASSPHRASE || process.env.OWS_API_KEY || '').trim();
2300
+ if (!passphrase) {
2301
+ throw new Error('Set OWS_PASSPHRASE (or OWS_API_KEY) for self-exec signing.');
2302
+ }
2303
+ const ows = resolveOwsBinary();
2304
+ const txHex = String(unsignedHex).startsWith('0x') ? String(unsignedHex).slice(2) : String(unsignedHex);
2305
+ const res = spawnSync(
2306
+ ows,
2307
+ ['sign', 'tx', '--wallet', walletName, '--chain', `eip155:${chainId}`, '--tx', txHex, '--json'],
2308
+ { env: { ...process.env, OWS_PASSPHRASE: passphrase }, encoding: 'utf-8' }
2309
+ );
2310
+ const out = String(res.stdout || '').trim();
2311
+ if (res.status !== 0 || !out) {
2312
+ throw new Error(`ows sign tx failed: ${String(res.stderr || out || res.status).slice(0, 300)}`);
2313
+ }
2314
+ let parsed;
2315
+ try {
2316
+ parsed = JSON.parse(out);
2317
+ } catch {
2318
+ throw new Error(`ows sign tx returned non-json payload: ${out.slice(0, 200)}`);
2319
+ }
2320
+ const signature = String(parsed.signature || '').replace(/^0x/i, '');
2321
+ if (signature.length < 128) {
2322
+ throw new Error('Invalid OWS signature payload');
2323
+ }
2324
+ return {
2325
+ recoveryId: Number(parsed.recovery_id),
2326
+ r: `0x${signature.slice(0, 64)}`,
2327
+ s: `0x${signature.slice(64, 128)}`,
2328
+ };
2329
+ }
2330
+
2331
+ async function signAndBroadcastWithOws({ txd, chainId, walletAddress, owsWalletName, rpcUrl }) {
2332
+ const ethers = await loadEthers();
2333
+ const { JsonRpcProvider, Transaction, Signature } = ethers;
2334
+ const provider = new JsonRpcProvider(rpcUrl, chainId);
2335
+ const nonce = await provider.getTransactionCount(walletAddress, 'pending');
2336
+ const fee = await provider.getFeeData();
2337
+ const txFields = {
2338
+ type: 2,
2339
+ chainId,
2340
+ nonce,
2341
+ to: String(txd.to).toLowerCase(),
2342
+ data: txd.data,
2343
+ value: txd.value ? BigInt(txd.value) : 0n,
2344
+ gasLimit: txd.gas ? BigInt(txd.gas) : txd.gasLimit ? BigInt(txd.gasLimit) : 500000n,
2345
+ maxFeePerGas: fee.maxFeePerGas ?? fee.gasPrice ?? 2_000_000_000n,
2346
+ maxPriorityFeePerGas: fee.maxPriorityFeePerGas ?? 1_000_000_000n,
2347
+ };
2348
+ const unsigned = Transaction.from(txFields);
2349
+ const sigParts = signUnsignedTxWithOws(unsigned.unsignedSerialized, owsWalletName, chainId);
2350
+ const sig = Signature.from({ r: sigParts.r, s: sigParts.s, yParity: sigParts.recoveryId });
2351
+ const tx = Transaction.from(unsigned.unsignedSerialized);
2352
+ tx.signature = sig;
2353
+ const sent = await provider.broadcastTransaction(tx.serialized);
2354
+ return { sent };
2355
+ }
2356
+
2357
+ async function readTokenBalanceRaw(rpcUrl, walletAddress, tokenAddress) {
2358
+ const ethers = await loadEthers();
2359
+ const { JsonRpcProvider, Interface } = ethers;
2360
+ const provider = new JsonRpcProvider(rpcUrl);
2361
+ const erc20 = new Interface(['function balanceOf(address owner) view returns (uint256)']);
2362
+ const response = await provider.call({
2363
+ to: tokenAddress,
2364
+ data: erc20.encodeFunctionData('balanceOf', [walletAddress]),
2365
+ });
2366
+ const decoded = erc20.decodeFunctionResult('balanceOf', response);
2367
+ return BigInt(decoded[0].toString()).toString();
2368
+ }
2369
+
2370
+ async function readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut) {
2371
+ const [inBal, outBal] = await Promise.all([
2372
+ readTokenBalanceRaw(rpcUrl, walletAddress, tokenIn),
2373
+ readTokenBalanceRaw(rpcUrl, walletAddress, tokenOut),
2374
+ ]);
2375
+ return { tokenIn: inBal, tokenOut: outBal };
2376
+ }
2377
+
1227
2378
  async function apiGet(ctx, route, headers = {}) {
1228
2379
  return requestJson(ctx, route, { method: 'GET', headers });
1229
2380
  }
@@ -1308,21 +2459,37 @@ Core commands:
1308
2459
  thirdfy-agent profile init [--profile personal|builder|network] [--run-mode thirdfy|self|hybrid] [--json]
1309
2460
  thirdfy-agent profile use --profile <name> [--run-mode thirdfy|self|hybrid] [--json]
1310
2461
  thirdfy-agent profile show [--json]
2462
+ thirdfy-agent config set --key <path> --value <value> [--json]
2463
+ thirdfy-agent login [--auth-token <token> | --owner-session-token <token>] [--agent-api-key <key>] [--json]
2464
+ thirdfy-agent logout [--all] [--clear-agent-key] [--json]
1311
2465
  thirdfy-agent whoami [--json]
1312
2466
  thirdfy-agent catalogs list [--json]
1313
2467
  thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--chain-id <id>] [--run-mode <mode>] [--json]
1314
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]
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]
1315
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]
1316
2472
  thirdfy-agent delegation status --auth-token <token> --agent-key <key> [--verify] [--json]
2473
+ thirdfy-agent delegation show --auth-token <token> --agent-key <key> [--verify] [--json]
2474
+ thirdfy-agent delegation inspect --auth-token <token> --agent-key <key> [--verify] [--json]
2475
+ thirdfy-agent delegation balance --auth-token <token> --agent-key <key> [--chain-id <id>] [--json]
2476
+ thirdfy-agent delegation revoke --auth-token <token> --agent-key <key> [--reason <text>] [--chain-id <id>] [--json]
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]
1317
2481
  thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
1318
2482
  thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
1319
2483
  thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
1320
2484
  thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
2485
+ thirdfy-agent developer bootstrap --agent-key <key> --name <name> [--owner-session-token <token> | --challenge-id <id> --signature <hex>] [--json]
1321
2486
  thirdfy-agent agent register --agent-key <key> --name <name> [--auth-token <token> | --owner-session-token <token>] [--json]
1322
2487
  thirdfy-agent agent key rotate [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
1323
2488
  thirdfy-agent agent key revoke [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
1324
2489
  thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1325
- thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--idempotency-key <key>] [--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]
1326
2493
  thirdfy-agent jeff preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1327
2494
  thirdfy-agent jeff trade --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1328
2495
  thirdfy-agent jeff status --intent-id <id> [--json]
@@ -1336,6 +2503,9 @@ Global flags:
1336
2503
  --profile <name> personal | builder | network
1337
2504
  --env <file> load env vars from file
1338
2505
  --timeout <ms> request timeout
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)
1339
2509
  --owner-session-token <token> owner session token (or THIRDFY_OWNER_SESSION_TOKEN)
1340
2510
  --json stable machine-readable output
1341
2511
  --verbose print adapter diagnostics