@thirdfy/agent-cli 0.1.5 → 0.1.7

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';
@@ -105,6 +106,15 @@ async function main() {
105
106
  case 'config get':
106
107
  await commandProfileShow(context, subFlags);
107
108
  return;
109
+ case 'config set':
110
+ await commandConfigSet(context, subFlags);
111
+ return;
112
+ case 'login':
113
+ await commandLogin(context, subFlags);
114
+ return;
115
+ case 'logout':
116
+ await commandLogout(context, subFlags);
117
+ return;
108
118
  case 'whoami':
109
119
  await commandProfileShow(context, subFlags);
110
120
  return;
@@ -130,12 +140,36 @@ async function main() {
130
140
  case 'credits balance':
131
141
  await commandCreditsBalance(context, subFlags, capabilities);
132
142
  return;
133
- case 'delegation upsert':
134
- await commandDelegationUpsert(context, subFlags, capabilities);
143
+ case 'delegation create':
144
+ await commandDelegationCreate(context, subFlags, capabilities);
145
+ return;
146
+ case 'delegation init-custodial':
147
+ await commandDelegationInitCustodial(context, subFlags, capabilities);
148
+ return;
149
+ case 'delegation custodial-grant':
150
+ await commandDelegationCustodialGrant(context, subFlags, capabilities);
151
+ return;
152
+ case 'delegation activate':
153
+ await commandDelegationActivate(context, subFlags, capabilities);
135
154
  return;
136
155
  case 'delegation status':
137
156
  await commandDelegationStatus(context, subFlags, capabilities);
138
157
  return;
158
+ case 'delegation show':
159
+ await commandDelegationStatus(context, subFlags, capabilities);
160
+ return;
161
+ case 'delegation inspect':
162
+ await commandDelegationInspect(context, subFlags, capabilities);
163
+ return;
164
+ case 'delegation balance':
165
+ await commandDelegationBalance(context, subFlags, capabilities);
166
+ return;
167
+ case 'delegation revoke':
168
+ await commandDelegationRevoke(context, subFlags, capabilities);
169
+ return;
170
+ case 'delegation redeem':
171
+ await commandDelegationRedeem(context, subFlags, capabilities);
172
+ return;
139
173
  case 'credentials upsert':
140
174
  await commandCredentialsUpsert(context, subFlags, capabilities);
141
175
  return;
@@ -145,6 +179,9 @@ async function main() {
145
179
  case 'agent register':
146
180
  await commandAgentRegister(context, subFlags, capabilities);
147
181
  return;
182
+ case 'developer bootstrap':
183
+ await commandDeveloperBootstrap(context, subFlags, capabilities);
184
+ return;
148
185
  default:
149
186
  break;
150
187
  }
@@ -179,6 +216,9 @@ async function main() {
179
216
  case 'run':
180
217
  await commandRun(context, subFlags, capabilities);
181
218
  return;
219
+ case 'self-exec':
220
+ await commandSelfExec(context, subFlags, capabilities);
221
+ return;
182
222
  case 'intent-status':
183
223
  await commandIntentStatus(context, subFlags, capabilities);
184
224
  return;
@@ -253,6 +293,162 @@ function parseJsonFlag(flags, key) {
253
293
  }
254
294
  }
255
295
 
296
+ function shouldApplySwapAmountContract(actionName) {
297
+ const action = String(actionName || '').trim().toLowerCase();
298
+ return action === 'swap' || /(^|:|-)swap$/.test(action);
299
+ }
300
+
301
+ function createCliError(code, message, payload = {}) {
302
+ const error = new Error(message);
303
+ error.payload = {
304
+ error: code,
305
+ blockedReason: code,
306
+ blockedStage: 'client_validation',
307
+ ...payload,
308
+ };
309
+ return error;
310
+ }
311
+
312
+ function normalizeIntegerRawAmount(input, code = 'AMOUNT_UNIT_INVALID') {
313
+ const raw = String(input ?? '').trim();
314
+ if (!/^\d+$/.test(raw)) {
315
+ throw createCliError(code, 'amountInRaw must be an integer base-unit string (example: "1000000").');
316
+ }
317
+ return raw;
318
+ }
319
+
320
+ function decimalToRawUnits(amountHuman, decimals) {
321
+ const s = String(amountHuman ?? '').trim();
322
+ if (!/^\d+(\.\d+)?$/.test(s)) {
323
+ throw createCliError('AMOUNT_UNIT_INVALID', 'amountInHuman must be a decimal string (example: "100.5").');
324
+ }
325
+ const [whole, frac = ''] = s.split('.');
326
+ if (decimals === 0) {
327
+ if (frac && /[1-9]/.test(frac)) {
328
+ throw createCliError(
329
+ 'AMOUNT_PRECISION_EXCESS',
330
+ 'amountInHuman has fractional digits but tokenInDecimals is 0 (example: use "2" not "2.5").'
331
+ );
332
+ }
333
+ } else if (frac.length > decimals && /[1-9]/.test(frac.slice(decimals))) {
334
+ throw createCliError(
335
+ 'AMOUNT_PRECISION_EXCESS',
336
+ `amountInHuman has more than ${decimals} fractional digit(s) for tokenInDecimals; round or use amountInRaw.`
337
+ );
338
+ }
339
+ const fracNorm = (frac + '0'.repeat(decimals)).slice(0, decimals);
340
+ const wholePart = BigInt(whole || '0') * 10n ** BigInt(decimals);
341
+ const fracPart = BigInt(fracNorm || '0');
342
+ return (wholePart + fracPart).toString();
343
+ }
344
+
345
+ function normalizeSwapAmountContract(params) {
346
+ const hasRaw = params.amountInRaw !== undefined && params.amountInRaw !== null && String(params.amountInRaw).trim() !== '';
347
+ const hasHuman = params.amountInHuman !== undefined && params.amountInHuman !== null && String(params.amountInHuman).trim() !== '';
348
+ const hasLegacy = params.amountIn !== undefined && params.amountIn !== null && String(params.amountIn).trim() !== '';
349
+ const populated = [hasRaw, hasHuman, hasLegacy].filter(Boolean).length;
350
+ if (populated === 0) {
351
+ throw createCliError(
352
+ 'AMOUNT_UNIT_MISSING',
353
+ 'Missing swap amount: provide one of amountInRaw, amountInHuman, or legacy amountIn.'
354
+ );
355
+ }
356
+ if (populated > 1) {
357
+ throw createCliError(
358
+ 'AMOUNT_UNIT_AMBIGUOUS',
359
+ 'Provide exactly one of amountInRaw, amountInHuman, or legacy amountIn.'
360
+ );
361
+ }
362
+
363
+ if (hasRaw) {
364
+ const amountInRaw = normalizeIntegerRawAmount(params.amountInRaw);
365
+ return {
366
+ params: {
367
+ ...params,
368
+ amountInRaw,
369
+ amountIn: amountInRaw,
370
+ },
371
+ swapInput: {
372
+ amountInRaw,
373
+ amountInHuman: null,
374
+ tokenInDecimals: null,
375
+ unitSource: 'raw',
376
+ },
377
+ };
378
+ }
379
+
380
+ if (hasLegacy) {
381
+ const amountInRaw = normalizeIntegerRawAmount(params.amountIn, 'AMOUNT_UNIT_INVALID');
382
+ return {
383
+ params: {
384
+ ...params,
385
+ amountInRaw,
386
+ amountIn: amountInRaw,
387
+ },
388
+ swapInput: {
389
+ amountInRaw,
390
+ amountInHuman: null,
391
+ tokenInDecimals: null,
392
+ unitSource: 'legacy_raw',
393
+ },
394
+ };
395
+ }
396
+
397
+ const hasDecimals =
398
+ params.tokenInDecimals !== undefined &&
399
+ params.tokenInDecimals !== null &&
400
+ String(params.tokenInDecimals).trim() !== '';
401
+ if (!hasDecimals) {
402
+ throw createCliError(
403
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
404
+ 'amountInHuman requires tokenInDecimals (integer 0-36).'
405
+ );
406
+ }
407
+ const decimals = Number(params.tokenInDecimals);
408
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
409
+ throw createCliError(
410
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
411
+ 'amountInHuman requires tokenInDecimals (integer 0-36).'
412
+ );
413
+ }
414
+ const amountInHuman = String(params.amountInHuman).trim();
415
+ const amountInRaw = decimalToRawUnits(amountInHuman, decimals);
416
+ return {
417
+ params: {
418
+ ...params,
419
+ amountInHuman,
420
+ amountInRaw,
421
+ amountIn: amountInRaw,
422
+ tokenInDecimals: decimals,
423
+ },
424
+ swapInput: {
425
+ amountInRaw,
426
+ amountInHuman,
427
+ tokenInDecimals: decimals,
428
+ unitSource: 'human',
429
+ },
430
+ };
431
+ }
432
+
433
+ function prepareActionParamsForFlags(flags, resolvedAction) {
434
+ const parsedParams = parseJsonFlag(flags, 'params');
435
+ if (!shouldApplySwapAmountContract(resolvedAction)) {
436
+ flags.__preparedParams = parsedParams;
437
+ flags.__swapInput = null;
438
+ return;
439
+ }
440
+ const normalized = normalizeSwapAmountContract(parsedParams);
441
+ flags.__preparedParams = normalized.params;
442
+ flags.__swapInput = normalized.swapInput;
443
+ }
444
+
445
+ function getPreparedParams(flags) {
446
+ if (flags.__preparedParams && typeof flags.__preparedParams === 'object') {
447
+ return flags.__preparedParams;
448
+ }
449
+ return parseJsonFlag(flags, 'params');
450
+ }
451
+
256
452
  async function commandCatalogsList(ctx, flags, capabilities) {
257
453
  const response = await apiGet(ctx, '/api/v1/agent/actions/catalog');
258
454
  const actions = extractActions(response);
@@ -303,6 +499,7 @@ async function commandActions(ctx, flags, capabilities) {
303
499
 
304
500
  async function commandPreflight(ctx, flags, capabilities) {
305
501
  const resolved = await resolveActionSelection(ctx, flags);
502
+ prepareActionParamsForFlags(flags, resolved.resolvedAction);
306
503
  const runMode = getEffectiveRunMode(ctx, flags);
307
504
  enforceChainCompatibility(capabilities, flags, runMode);
308
505
  const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved.resolvedAction);
@@ -319,6 +516,7 @@ async function commandPreflight(ctx, flags, capabilities) {
319
516
  resolvedAction: resolved.resolvedAction,
320
517
  runMode,
321
518
  profile: ctx.profile || null,
519
+ swapInput: flags.__swapInput || null,
322
520
  capabilitiesVersion: capabilities?.contractVersion || null,
323
521
  },
324
522
  });
@@ -327,6 +525,7 @@ async function commandPreflight(ctx, flags, capabilities) {
327
525
 
328
526
  async function commandRun(ctx, flags, capabilities) {
329
527
  const resolved = await resolveActionSelection(ctx, flags);
528
+ prepareActionParamsForFlags(flags, resolved.resolvedAction);
330
529
  const runMode = getEffectiveRunMode(ctx, flags);
331
530
  enforceChainCompatibility(capabilities, flags, runMode);
332
531
  const skipPreflight = Boolean(flags.skipPreflight);
@@ -347,6 +546,7 @@ async function commandRun(ctx, flags, capabilities) {
347
546
  runMode,
348
547
  profile: ctx.profile || null,
349
548
  skippedPreflight: skipPreflight,
549
+ swapInput: flags.__swapInput || null,
350
550
  capabilitiesVersion: capabilities?.contractVersion || null,
351
551
  },
352
552
  });
@@ -388,32 +588,145 @@ async function commandCreditsBalance(ctx, flags, capabilities) {
388
588
  });
389
589
  }
390
590
 
391
- async function commandDelegationUpsert(ctx, flags, capabilities) {
591
+ async function commandDelegationCreate(ctx, flags, capabilities) {
592
+ const authHeaders = buildOwnerAuthHeaders(
593
+ flags,
594
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation create'
595
+ );
596
+ let sessionAccountAddress = String(flags.sessionAccountAddress || '').trim();
597
+ if (!sessionAccountAddress) {
598
+ try {
599
+ const executorAddressResponse = await apiGet(ctx, '/api/v1/agent/delegation/executor-address', {
600
+ ...authHeaders,
601
+ });
602
+ sessionAccountAddress = String(executorAddressResponse?.data?.executorAddress || '').trim();
603
+ } catch (error) {
604
+ if (ctx.verbose) {
605
+ process.stderr.write(
606
+ `delegation create: executor-address discovery failed, falling back to --session-account-address (${error?.message || 'unknown error'})\n`
607
+ );
608
+ }
609
+ }
610
+ }
392
611
  const payload = {
393
- userDid: requireFlag(flags, 'userDid', 'Missing --user-did'),
394
612
  agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
395
- walletAddress: requireFlag(flags, 'walletAddress', 'Missing --wallet-address'),
613
+ ownerAddress: String(flags.ownerAddress || flags.walletAddress || '').trim() || requireFlag(flags, 'ownerAddress', 'Missing --owner-address'),
614
+ sessionAccountAddress: sessionAccountAddress || requireFlag(flags, 'sessionAccountAddress', 'Missing --session-account-address'),
396
615
  chainId: Number(flags.chainId || 8453),
397
- executionMode: String(flags.executionMode || 'delegated_execution_user'),
616
+ tokenAddress: requireFlag(flags, 'tokenAddress', 'Missing --token-address'),
617
+ maxUsdPerDay: Number(requireFlag(flags, 'maxUsdPerDay', 'Missing --max-usd-per-day')),
618
+ periodDuration: Number(flags.periodDuration || 86_400),
619
+ expirySeconds: Number(flags.expirySeconds || 86_400),
398
620
  };
399
- if (flags.sessionAccountAddress) payload.sessionAccountAddress = String(flags.sessionAccountAddress).trim();
400
- if (flags.sessionAccountType) payload.sessionAccountType = String(flags.sessionAccountType).trim();
401
- if (flags.permissionsContext) payload.permissionsContext = String(flags.permissionsContext).trim();
402
- if (flags.delegationManager) payload.delegationManager = String(flags.delegationManager).trim();
403
- if (flags.permissionsExpiry) payload.permissionsExpiry = String(flags.permissionsExpiry).trim();
404
- if (flags.strategyManifestVersion) payload.strategyManifestVersion = String(flags.strategyManifestVersion).trim();
405
- if (flags.strategyManifestHash) payload.strategyManifestHash = String(flags.strategyManifestHash).trim();
406
- if (flags.modelAConstraints) payload.modelAConstraints = parseJsonFlag(flags, 'modelAConstraints');
621
+ const response = await apiPost(ctx, '/api/v1/agent/delegation/create', payload, {
622
+ ...authHeaders,
623
+ });
624
+ const success = Boolean(response?.success !== false);
625
+ printEnvelope({
626
+ success,
627
+ code: success ? 'DELEGATION_CREATED' : 'DELEGATION_CREATE_FAILED',
628
+ message: response?.message || (success ? 'Delegation payload created' : 'Delegation create failed'),
629
+ data: response,
630
+ meta: {
631
+ apiBase: ctx.apiBase,
632
+ capabilitiesVersion: capabilities?.contractVersion || null,
633
+ },
634
+ });
635
+ }
407
636
 
637
+ async function commandDelegationInitCustodial(ctx, flags, capabilities) {
638
+ const authHeaders = buildOwnerAuthHeaders(
639
+ flags,
640
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation init-custodial'
641
+ );
642
+ const payload = {
643
+ chainId: Number(flags.chainId || 8453),
644
+ };
408
645
  const response = await requestWithRouteFallback(ctx, {
409
646
  method: 'POST',
410
- routes: ['/api/v1/agent/delegation/upsert', '/api/v1/agent/agent/delegation/upsert'],
647
+ routes: ['/api/v1/agent/delegation/init-custodial'],
411
648
  body: payload,
649
+ headers: {
650
+ ...authHeaders,
651
+ },
412
652
  });
653
+ const success = Boolean(response?.success !== false);
413
654
  printEnvelope({
414
- success: Boolean(response?.success !== false),
415
- code: response?.updated ? 'DELEGATION_UPDATED' : 'DELEGATION_UPSERTED',
416
- message: response?.message || 'Delegation upsert completed',
655
+ success,
656
+ code: success ? 'DELEGATION_CUSTODIAL_INITIALIZED' : 'DELEGATION_CUSTODIAL_INIT_FAILED',
657
+ message: response?.message || (success ? 'Custodial wallet initialized' : 'Custodial wallet init failed'),
658
+ data: response,
659
+ meta: {
660
+ apiBase: ctx.apiBase,
661
+ capabilitiesVersion: capabilities?.contractVersion || null,
662
+ },
663
+ });
664
+ }
665
+
666
+ async function commandDelegationCustodialGrant(ctx, flags, capabilities) {
667
+ const authHeaders = buildOwnerAuthHeaders(
668
+ flags,
669
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation custodial-grant'
670
+ );
671
+ const payload = {
672
+ chainId: Number(flags.chainId || 8453),
673
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
674
+ tokenAddress: requireFlag(flags, 'tokenAddress', 'Missing --token-address'),
675
+ maxUsdPerDay: Number(requireFlag(flags, 'maxUsdPerDay', 'Missing --max-usd-per-day')),
676
+ periodDuration: Number(flags.periodDuration || 86_400),
677
+ expirySeconds: Number(flags.expirySeconds || 86_400),
678
+ };
679
+ const response = await requestWithRouteFallback(ctx, {
680
+ method: 'POST',
681
+ routes: ['/api/v1/agent/delegation/custodial-grant'],
682
+ body: payload,
683
+ headers: {
684
+ ...authHeaders,
685
+ },
686
+ });
687
+ const success = Boolean(response?.success !== false);
688
+ printEnvelope({
689
+ success,
690
+ code: success ? 'DELEGATION_CUSTODIAL_GRANTED' : 'DELEGATION_CUSTODIAL_GRANT_FAILED',
691
+ message: response?.message || (success ? 'Custodial delegation granted' : 'Custodial delegation grant failed'),
692
+ data: response,
693
+ meta: {
694
+ apiBase: ctx.apiBase,
695
+ capabilitiesVersion: capabilities?.contractVersion || null,
696
+ },
697
+ });
698
+ }
699
+
700
+ async function commandDelegationActivate(ctx, flags, capabilities) {
701
+ const authHeaders = buildOwnerAuthHeaders(
702
+ flags,
703
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation activate'
704
+ );
705
+ const payload = {
706
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
707
+ walletAddress: requireFlag(flags, 'walletAddress', 'Missing --wallet-address'),
708
+ sessionAccountAddress: requireFlag(flags, 'sessionAccountAddress', 'Missing --session-account-address'),
709
+ sessionAccountType: String(flags.sessionAccountType || 'smart').trim(),
710
+ chainId: Number(flags.chainId || 8453),
711
+ delegationManager: requireFlag(flags, 'delegationManager', 'Missing --delegation-manager'),
712
+ signature: requireFlag(flags, 'signature', 'Missing --signature'),
713
+ };
714
+ const delegation = parseJsonFlag(flags, 'delegation');
715
+ payload.delegation = delegation;
716
+ if (flags.permissionsExpiry) payload.permissionsExpiry = String(flags.permissionsExpiry).trim();
717
+ if (flags.permissionsContext) payload.permissionsContext = String(flags.permissionsContext).trim();
718
+ if (flags.permission) payload.permission = parseJsonFlag(flags, 'permission');
719
+ if (flags.permissionType) payload.permissionType = String(flags.permissionType).trim();
720
+ if (flags.permissionData) payload.permissionData = parseJsonFlag(flags, 'permissionData');
721
+ if (flags.modelAConstraints) payload.modelAConstraints = parseJsonFlag(flags, 'modelAConstraints');
722
+ const response = await apiPost(ctx, '/api/v1/agent/delegation/activate', payload, {
723
+ ...authHeaders,
724
+ });
725
+ const success = Boolean(response?.success !== false);
726
+ printEnvelope({
727
+ success,
728
+ code: success ? 'DELEGATION_ACTIVATED' : 'DELEGATION_ACTIVATE_FAILED',
729
+ message: response?.message || (success ? 'Delegation activated' : 'Delegation activate failed'),
417
730
  data: response,
418
731
  meta: {
419
732
  apiBase: ctx.apiBase,
@@ -423,7 +736,10 @@ async function commandDelegationUpsert(ctx, flags, capabilities) {
423
736
  }
424
737
 
425
738
  async function commandDelegationStatus(ctx, flags, capabilities) {
426
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for delegation status');
739
+ const authHeaders = buildOwnerAuthHeaders(
740
+ flags,
741
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation status'
742
+ );
427
743
  const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
428
744
  const chainId = Number(flags.chainId || 8453);
429
745
  const verify = Boolean(flags.verify);
@@ -431,7 +747,7 @@ async function commandDelegationStatus(ctx, flags, capabilities) {
431
747
  ? `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`
432
748
  : `/api/v1/agent/delegation/check?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
433
749
  const response = await apiGet(ctx, route, {
434
- Authorization: `Bearer ${authToken}`,
750
+ ...authHeaders,
435
751
  });
436
752
  const normalized = normalizeDelegationStatus(response);
437
753
  printEnvelope({
@@ -448,6 +764,129 @@ async function commandDelegationStatus(ctx, flags, capabilities) {
448
764
  });
449
765
  }
450
766
 
767
+ async function commandDelegationInspect(ctx, flags, capabilities) {
768
+ const authHeaders = buildOwnerAuthHeaders(
769
+ flags,
770
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation inspect'
771
+ );
772
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
773
+ const chainId = Number(flags.chainId || 8453);
774
+ const includeOnchain = Boolean(flags.verify || flags.includeOnchain || flags.onchain);
775
+ const checkRoute = `/api/v1/agent/delegation/check?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
776
+ const checkResponse = await apiGet(ctx, checkRoute, {
777
+ ...authHeaders,
778
+ });
779
+ const checkNormalized = normalizeDelegationStatus(checkResponse);
780
+
781
+ let verifyNormalized = null;
782
+ if (includeOnchain) {
783
+ const verifyRoute = `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
784
+ const verifyResponse = await apiGet(ctx, verifyRoute, {
785
+ ...authHeaders,
786
+ });
787
+ verifyNormalized = normalizeDelegationStatus(verifyResponse);
788
+ }
789
+
790
+ printEnvelope({
791
+ success: true,
792
+ code: 'DELEGATION_INSPECT_OK',
793
+ message: includeOnchain ? 'Delegation inspect loaded (with onchain verification)' : 'Delegation inspect loaded',
794
+ data: {
795
+ check: checkNormalized,
796
+ verify: verifyNormalized,
797
+ state: verifyNormalized?.state || checkNormalized.state,
798
+ rawStatus: verifyNormalized?.rawStatus || checkNormalized.rawStatus,
799
+ },
800
+ meta: {
801
+ apiBase: ctx.apiBase,
802
+ includeOnchain,
803
+ governanceState: verifyNormalized?.state || checkNormalized.state || null,
804
+ capabilitiesVersion: capabilities?.contractVersion || null,
805
+ },
806
+ });
807
+ }
808
+
809
+ async function commandDelegationRevoke(ctx, flags, capabilities) {
810
+ const authHeaders = buildOwnerAuthHeaders(
811
+ flags,
812
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation revoke'
813
+ );
814
+ const payload = {
815
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
816
+ reason: String(flags.reason || 'user_revoked').trim(),
817
+ chainId: Number(flags.chainId || 8453),
818
+ };
819
+ const response = await requestWithRouteFallback(ctx, {
820
+ method: 'POST',
821
+ routes: ['/api/v1/agent/delegation/revoke'],
822
+ body: payload,
823
+ headers: {
824
+ ...authHeaders,
825
+ },
826
+ });
827
+ const success = Boolean(response?.success !== false);
828
+ printEnvelope({
829
+ success,
830
+ code: success ? 'DELEGATION_REVOKED' : 'DELEGATION_REVOKE_FAILED',
831
+ message: response?.message || (success ? 'Delegation revoked' : 'Delegation revoke failed'),
832
+ data: response,
833
+ meta: {
834
+ apiBase: ctx.apiBase,
835
+ capabilitiesVersion: capabilities?.contractVersion || null,
836
+ },
837
+ });
838
+ }
839
+
840
+ async function commandDelegationBalance(ctx, flags, capabilities) {
841
+ const authHeaders = buildOwnerAuthHeaders(
842
+ flags,
843
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation balance'
844
+ );
845
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
846
+ const chainId = Number(flags.chainId || 8453);
847
+ const route = `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
848
+ const response = await apiGet(ctx, route, {
849
+ ...authHeaders,
850
+ });
851
+ const normalized = normalizeDelegationStatus(response);
852
+ const verifyData = normalized?.data?.onChainVerification || {};
853
+ printEnvelope({
854
+ success: true,
855
+ code: 'DELEGATION_BALANCE_OK',
856
+ message: 'Delegation allowance balance loaded',
857
+ data: {
858
+ state: normalized.state,
859
+ rawStatus: normalized.rawStatus,
860
+ delegatedWalletAddress: normalized?.data?.delegatedWalletAddress || null,
861
+ periodAvailableUsd: verifyData?.periodAvailableUsd ?? null,
862
+ periodAvailableBaseUnits: verifyData?.periodAvailableBaseUnits ?? null,
863
+ verified: verifyData?.verified ?? null,
864
+ isRevoked: verifyData?.isRevoked ?? null,
865
+ onChainVerification: verifyData || null,
866
+ },
867
+ meta: {
868
+ apiBase: ctx.apiBase,
869
+ governanceState: normalized.state || null,
870
+ capabilitiesVersion: capabilities?.contractVersion || null,
871
+ },
872
+ });
873
+ }
874
+
875
+ async function commandDelegationRedeem(ctx, flags, capabilities) {
876
+ const selectedRunMode = getEffectiveRunMode(ctx, flags);
877
+ if (!['thirdfy', 'hybrid'].includes(selectedRunMode)) {
878
+ throw createCliError('DELEGATION_REDEEM_MODE_INVALID', 'delegation redeem supports only --run-mode thirdfy|hybrid');
879
+ }
880
+ const delegatedFlags = {
881
+ ...flags,
882
+ runMode: selectedRunMode,
883
+ };
884
+ if (!delegatedFlags.estimatedAmountUsd) {
885
+ delegatedFlags.estimatedAmountUsd = '5';
886
+ }
887
+ await commandRun(ctx, delegatedFlags, capabilities);
888
+ }
889
+
451
890
  async function commandCredentialsUpsert(ctx, flags, capabilities) {
452
891
  const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials upsert');
453
892
  const payload = {
@@ -540,11 +979,83 @@ async function commandAgentRegister(ctx, flags, capabilities) {
540
979
  ? '/api/v1/agent/onboarding/register-with-wallet'
541
980
  : '/api/v1/agent/onboarding/register-with-privy';
542
981
  const response = await apiPost(ctx, route, payload, ownerHeaders);
982
+ if (!flags.__suppressOutput) {
983
+ printEnvelope({
984
+ success: true,
985
+ code: 'AGENT_REGISTERED',
986
+ message: 'Agent registered',
987
+ data: response,
988
+ meta: {
989
+ apiBase: ctx.apiBase,
990
+ capabilitiesVersion: capabilities?.contractVersion || null,
991
+ },
992
+ });
993
+ }
994
+ return response;
995
+ }
996
+
997
+ async function commandDeveloperBootstrap(ctx, flags, capabilities) {
998
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
999
+ const name = requireFlag(flags, 'name', 'Missing --name');
1000
+ let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
1001
+ let usedWalletFlow = false;
1002
+ const challengeId = String(flags.challengeId || '').trim();
1003
+ const signature = String(flags.signature || '').trim();
1004
+
1005
+ if (!ownerSessionToken && signature) {
1006
+ if (!challengeId) {
1007
+ throw new Error('Missing --challenge-id for wallet bootstrap. Run `agent auth challenge` first.');
1008
+ }
1009
+ const verify = await apiPost(ctx, '/api/v1/agent/onboarding/wallet/verify', {
1010
+ challengeId,
1011
+ signature,
1012
+ agentKey,
1013
+ ...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
1014
+ });
1015
+ ownerSessionToken = String(verify?.ownerSessionToken || verify?.data?.ownerSessionToken || '').trim();
1016
+ usedWalletFlow = true;
1017
+ }
1018
+
1019
+ const registerFlags = {
1020
+ ...flags,
1021
+ agentKey,
1022
+ name,
1023
+ ...(ownerSessionToken ? { ownerSessionToken } : {}),
1024
+ };
1025
+ const registerResponse = await commandAgentRegister(
1026
+ ctx,
1027
+ { ...registerFlags, __suppressOutput: true },
1028
+ capabilities
1029
+ );
1030
+
1031
+ const config = loadProfileConfig();
1032
+ const next = {
1033
+ ...config,
1034
+ auth: {
1035
+ ...(config.auth && typeof config.auth === 'object' ? config.auth : {}),
1036
+ ...(ownerSessionToken ? { ownerSessionToken } : {}),
1037
+ lastLoginAt: new Date().toISOString(),
1038
+ source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
1039
+ },
1040
+ developer: {
1041
+ ...(config.developer && typeof config.developer === 'object' ? config.developer : {}),
1042
+ agentKey,
1043
+ bootstrapCompletedAt: new Date().toISOString(),
1044
+ },
1045
+ };
1046
+ persistProfileConfig(next);
543
1047
  printEnvelope({
544
1048
  success: true,
545
- code: 'AGENT_REGISTERED',
546
- message: 'Agent registered',
547
- data: response,
1049
+ code: 'DEVELOPER_BOOTSTRAP_OK',
1050
+ message: 'Developer bootstrap completed',
1051
+ data: {
1052
+ registration: registerResponse,
1053
+ persisted: {
1054
+ ownerSessionTokenSaved: Boolean(ownerSessionToken),
1055
+ source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
1056
+ configPath: getProfileConfigPath(),
1057
+ },
1058
+ },
548
1059
  meta: {
549
1060
  apiBase: ctx.apiBase,
550
1061
  capabilitiesVersion: capabilities?.contractVersion || null,
@@ -659,10 +1170,140 @@ async function commandPrompt(ctx, flags, capabilities, promptTokens) {
659
1170
  });
660
1171
  }
661
1172
 
1173
+ async function commandSelfExec(ctx, flags, capabilities) {
1174
+ const resolved = await resolveActionSelection(ctx, flags);
1175
+ prepareActionParamsForFlags(flags, resolved.resolvedAction);
1176
+ enforceChainCompatibility(capabilities, flags, 'self');
1177
+ const selfResult = await executeSelfRun(ctx, flags, resolved, {
1178
+ skipPreflight: Boolean(flags.skipPreflight),
1179
+ });
1180
+
1181
+ if (!selfResult?.success) {
1182
+ const blocked = Boolean(selfResult?.preflightBlocked);
1183
+ printEnvelope({
1184
+ success: false,
1185
+ code: blocked ? 'PREFLIGHT_BLOCKED' : 'SELF_EXECUTION_FAILED',
1186
+ message: blocked ? 'Preflight blocked self execution' : 'Self execution preparation failed',
1187
+ data: selfResult || {},
1188
+ meta: {
1189
+ apiBase: ctx.apiBase,
1190
+ runMode: 'self',
1191
+ resolvedAction: resolved.resolvedAction,
1192
+ },
1193
+ });
1194
+ process.exit(1);
1195
+ }
1196
+
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
+ }
1212
+
1213
+ const chainId = Number(txd.chainId || flags.chainId || 8453);
1214
+ const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
1215
+ 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();
1227
+
1228
+ let balancesBefore = null;
1229
+ if (effectTokenIn && effectTokenOut) {
1230
+ balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
1231
+ }
1232
+
1233
+ let submission;
1234
+ try {
1235
+ submission = await signAndBroadcastWithOws({
1236
+ txd,
1237
+ chainId,
1238
+ walletAddress,
1239
+ owsWalletName,
1240
+ rpcUrl,
1241
+ });
1242
+ } catch (error) {
1243
+ throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
1244
+ }
1245
+
1246
+ let receipt;
1247
+ try {
1248
+ receipt = await submission.sent.wait();
1249
+ } 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
+ });
1258
+ }
1259
+
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
+ }
1278
+ }
1279
+
1280
+ printEnvelope({
1281
+ success: true,
1282
+ code: 'SELF_EXECUTED',
1283
+ message: 'Self-custody execution completed',
1284
+ data: {
1285
+ txHash: submission.sent.hash,
1286
+ chainId,
1287
+ blockNumber: receipt?.blockNumber ?? null,
1288
+ status: receipt?.status ?? null,
1289
+ unsignedTx: txd,
1290
+ effectCheck,
1291
+ },
1292
+ meta: {
1293
+ apiBase: ctx.apiBase,
1294
+ runMode: 'self',
1295
+ resolvedAction: resolved.resolvedAction,
1296
+ swapInput: flags.__swapInput || null,
1297
+ },
1298
+ });
1299
+ }
1300
+
662
1301
  async function commandProfileInit(ctx, flags) {
1302
+ const current = loadProfileConfig();
663
1303
  const profile = normalizeProfileName(flags.profile || flags.name || 'personal');
664
1304
  const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
665
1305
  persistProfileConfig({
1306
+ ...current,
666
1307
  profile,
667
1308
  runMode,
668
1309
  });
@@ -687,6 +1328,7 @@ async function commandProfileUse(ctx, flags) {
687
1328
  // `profile use` intentionally re-applies profile defaults unless caller pins a mode.
688
1329
  const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
689
1330
  persistProfileConfig({
1331
+ ...current,
690
1332
  profile,
691
1333
  runMode,
692
1334
  });
@@ -723,6 +1365,154 @@ async function commandProfileShow(ctx, _flags) {
723
1365
  });
724
1366
  }
725
1367
 
1368
+ async function commandConfigSet(ctx, flags) {
1369
+ const key = String(flags.key || '').trim();
1370
+ const value = flags.value;
1371
+ if (!key) {
1372
+ throw new Error('Missing --key for config set');
1373
+ }
1374
+ const canonicalKey = key.toLowerCase();
1375
+ const hasValue = !(value === undefined || String(value).trim() === '');
1376
+ if (canonicalKey === 'profile' && hasValue) {
1377
+ normalizeProfileName(value);
1378
+ }
1379
+ if (canonicalKey === 'runmode' && hasValue) {
1380
+ normalizeRunMode(value);
1381
+ }
1382
+
1383
+ const current = loadProfileConfig();
1384
+ const next = setNestedConfigValue(current, key, value);
1385
+ persistProfileConfig(next);
1386
+ printEnvelope({
1387
+ success: true,
1388
+ code: 'CONFIG_UPDATED',
1389
+ message: 'Configuration updated',
1390
+ data: {
1391
+ key,
1392
+ value: value === undefined ? null : String(value),
1393
+ configPath: getProfileConfigPath(),
1394
+ },
1395
+ meta: { apiBase: ctx.apiBase },
1396
+ });
1397
+ }
1398
+
1399
+ async function commandLogin(ctx, flags) {
1400
+ const current = loadProfileConfig();
1401
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
1402
+ let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
1403
+ let loginSource = 'token';
1404
+
1405
+ if (!authToken && !ownerSessionToken) {
1406
+ const challengeId = String(flags.challengeId || '').trim();
1407
+ const signature = String(flags.signature || '').trim();
1408
+ const agentKey = String(flags.agentKey || '').trim();
1409
+ if (challengeId && signature && agentKey) {
1410
+ const verifyResponse = await requestWithRouteFallback(ctx, {
1411
+ method: 'POST',
1412
+ routes: ['/api/v1/agent/onboarding/wallet/verify'],
1413
+ body: {
1414
+ challengeId,
1415
+ signature,
1416
+ agentKey,
1417
+ ...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
1418
+ },
1419
+ });
1420
+ ownerSessionToken = String(
1421
+ verifyResponse?.ownerSessionToken || verifyResponse?.data?.ownerSessionToken || ''
1422
+ ).trim();
1423
+ loginSource = 'wallet-verify';
1424
+ }
1425
+ }
1426
+
1427
+ if (!authToken && !ownerSessionToken) {
1428
+ throw new Error(
1429
+ 'Missing login credentials: provide --auth-token, --owner-session-token, or challenge verify inputs (--challenge-id/--signature/--agent-key).'
1430
+ );
1431
+ }
1432
+
1433
+ const next = {
1434
+ ...current,
1435
+ auth: {
1436
+ ...(current.auth && typeof current.auth === 'object' ? current.auth : {}),
1437
+ lastLoginAt: new Date().toISOString(),
1438
+ source: loginSource,
1439
+ },
1440
+ };
1441
+ if (authToken) {
1442
+ next.auth.authToken = authToken;
1443
+ delete next.auth.ownerSessionToken;
1444
+ } else if (ownerSessionToken) {
1445
+ next.auth.ownerSessionToken = ownerSessionToken;
1446
+ delete next.auth.authToken;
1447
+ }
1448
+ if (flags.agentApiKey) {
1449
+ next.agent = {
1450
+ ...(current.agent && typeof current.agent === 'object' ? current.agent : {}),
1451
+ apiKey: String(flags.agentApiKey).trim(),
1452
+ };
1453
+ }
1454
+ persistProfileConfig(next);
1455
+ printEnvelope({
1456
+ success: true,
1457
+ code: 'LOGIN_OK',
1458
+ message: 'Login credentials stored',
1459
+ data: {
1460
+ hasAuthToken: Boolean(authToken),
1461
+ hasOwnerSessionToken: Boolean(ownerSessionToken),
1462
+ hasAgentApiKey: Boolean(flags.agentApiKey),
1463
+ configPath: getProfileConfigPath(),
1464
+ },
1465
+ meta: { apiBase: ctx.apiBase },
1466
+ });
1467
+ }
1468
+
1469
+ async function commandLogout(ctx, flags) {
1470
+ const current = loadProfileConfig();
1471
+ const localOnly = flags.all ? false : true;
1472
+ const auth = current.auth && typeof current.auth === 'object' ? current.auth : {};
1473
+ const ownerSessionToken = String(auth.ownerSessionToken || '').trim();
1474
+ let revokeAttempted = false;
1475
+ let revokeSuccess = false;
1476
+ let revokeError = null;
1477
+
1478
+ if (!localOnly && ownerSessionToken) {
1479
+ revokeAttempted = true;
1480
+ try {
1481
+ await requestWithRouteFallback(ctx, {
1482
+ method: 'POST',
1483
+ routes: ['/api/v1/agent/onboarding/wallet/logout', '/api/v1/agent/onboarding/wallet/revoke-session'],
1484
+ body: {},
1485
+ headers: {
1486
+ 'x-owner-session-token': ownerSessionToken,
1487
+ },
1488
+ });
1489
+ revokeSuccess = true;
1490
+ } catch (error) {
1491
+ revokeError = error?.message || 'OWNER_SESSION_REVOKE_FAILED';
1492
+ }
1493
+ }
1494
+
1495
+ const next = { ...current };
1496
+ delete next.auth;
1497
+ if (flags.clearAgentKey) {
1498
+ delete next.agent;
1499
+ }
1500
+ persistProfileConfig(next);
1501
+ printEnvelope({
1502
+ success: true,
1503
+ code: 'LOGOUT_OK',
1504
+ message: 'Local credentials cleared',
1505
+ data: {
1506
+ localOnly,
1507
+ revokeAttempted,
1508
+ revokeSuccess,
1509
+ revokeError,
1510
+ configPath: getProfileConfigPath(),
1511
+ },
1512
+ meta: { apiBase: ctx.apiBase },
1513
+ });
1514
+ }
1515
+
726
1516
  function resolveProfileState(flags) {
727
1517
  const persisted = loadProfileConfig();
728
1518
  const profile = normalizeProfileName(flags.profile || process.env.THIRDFY_PROFILE || persisted.profile || 'personal');
@@ -771,8 +1561,60 @@ function loadProfileConfig() {
771
1561
  function persistProfileConfig(config) {
772
1562
  const configPath = getProfileConfigPath();
773
1563
  const dirPath = path.dirname(configPath);
774
- fs.mkdirSync(dirPath, { recursive: true });
775
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
1564
+ fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
1565
+ const payload = `${JSON.stringify(config, null, 2)}\n`;
1566
+ fs.writeFileSync(configPath, payload, { encoding: 'utf8', mode: 0o600 });
1567
+ try {
1568
+ fs.chmodSync(configPath, 0o600);
1569
+ } catch {
1570
+ /* chmod unsupported or restricted (e.g. some Windows FS) */
1571
+ }
1572
+ try {
1573
+ fs.chmodSync(dirPath, 0o700);
1574
+ } catch {
1575
+ /* ignore */
1576
+ }
1577
+ }
1578
+
1579
+ function clonePlainConfigTree(input) {
1580
+ const obj = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
1581
+ try {
1582
+ return structuredClone(obj);
1583
+ } catch {
1584
+ return JSON.parse(JSON.stringify(obj));
1585
+ }
1586
+ }
1587
+
1588
+ function setNestedConfigValue(config, keyPath, value) {
1589
+ const next = clonePlainConfigTree(config || {});
1590
+ const parts = String(keyPath || '')
1591
+ .split('.')
1592
+ .map((v) => v.trim())
1593
+ .filter(Boolean);
1594
+ const blocked = new Set(['__proto__', 'prototype', 'constructor']);
1595
+ if (parts.some((part) => blocked.has(part))) {
1596
+ throw createCliError(
1597
+ 'CONFIG_KEY_INVALID',
1598
+ 'Invalid config key path: reserved segments (__proto__, prototype, constructor) are not allowed.'
1599
+ );
1600
+ }
1601
+ if (!parts.length) return next;
1602
+ let cursor = next;
1603
+ for (let i = 0; i < parts.length - 1; i += 1) {
1604
+ const key = parts[i];
1605
+ const existing = cursor[key];
1606
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
1607
+ cursor[key] = {};
1608
+ }
1609
+ cursor = cursor[key];
1610
+ }
1611
+ const finalKey = parts[parts.length - 1];
1612
+ if (value === undefined || String(value).trim() === '') {
1613
+ delete cursor[finalKey];
1614
+ } else {
1615
+ cursor[finalKey] = String(value);
1616
+ }
1617
+ return next;
776
1618
  }
777
1619
 
778
1620
  function getEffectiveRunMode(ctx, flags) {
@@ -983,10 +1825,11 @@ async function executeHybridRun(ctx, flags, resolved, options) {
983
1825
  }
984
1826
 
985
1827
  function buildBuildTxPayload(flags, resolvedAction) {
1828
+ const runMode = String(flags.runMode || '').trim().toLowerCase() || 'self';
986
1829
  const payload = {
987
- agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
1830
+ agentApiKey: resolveAgentApiKey(flags, runMode),
988
1831
  action: String(resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
989
- params: parseJsonFlag(flags, 'params'),
1832
+ params: getPreparedParams(flags),
990
1833
  };
991
1834
  if (flags.chainId) payload.chainId = Number(flags.chainId);
992
1835
  if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
@@ -994,10 +1837,11 @@ function buildBuildTxPayload(flags, resolvedAction) {
994
1837
  }
995
1838
 
996
1839
  function buildIntentPayload(flags, options) {
1840
+ const runMode = String(options.runMode || flags.runMode || '').trim().toLowerCase() || 'thirdfy';
997
1841
  const payload = {
998
- agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
1842
+ agentApiKey: resolveAgentApiKey(flags, runMode),
999
1843
  action: String(options.resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
1000
- params: parseJsonFlag(flags, 'params'),
1844
+ params: getPreparedParams(flags),
1001
1845
  };
1002
1846
  if (flags.catalog) payload.catalog = String(flags.catalog).trim();
1003
1847
  if (flags.chainId) payload.chainId = Number(flags.chainId);
@@ -1018,6 +1862,22 @@ function buildIntentPayload(flags, options) {
1018
1862
  return payload;
1019
1863
  }
1020
1864
 
1865
+ function resolveAgentApiKey(flags, runMode) {
1866
+ const explicit = String(flags.agentApiKey || '').trim();
1867
+ if (explicit) return explicit;
1868
+ const envKey = String(process.env.THIRDFY_AGENT_API_KEY || '').trim();
1869
+ if (envKey) return envKey;
1870
+ const config = loadProfileConfig();
1871
+ const configKey = String(config?.agent?.apiKey || '').trim();
1872
+ if (configKey) return configKey;
1873
+ if (runMode === 'self') {
1874
+ throw new Error(
1875
+ '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.'
1876
+ );
1877
+ }
1878
+ throw new Error('Missing --agent-api-key (or THIRDFY_AGENT_API_KEY / config.agent.apiKey)');
1879
+ }
1880
+
1021
1881
  function getActionKey(action) {
1022
1882
  return String(action?.action || action?.id || action?.key || action?.name || '')
1023
1883
  .trim();
@@ -1141,14 +2001,18 @@ async function resolveActionSelection(ctx, flags) {
1141
2001
  }
1142
2002
 
1143
2003
  function getAuthToken(flags, missingMessage) {
1144
- const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
2004
+ const config = loadProfileConfig();
2005
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || config?.auth?.authToken || '').trim();
1145
2006
  if (!authToken) throw new Error(missingMessage);
1146
2007
  return authToken;
1147
2008
  }
1148
2009
 
1149
2010
  function buildOwnerAuthHeaders(flags, missingMessage) {
1150
- const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
1151
- const ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
2011
+ const config = loadProfileConfig();
2012
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || config?.auth?.authToken || '').trim();
2013
+ const ownerSessionToken = String(
2014
+ flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || config?.auth?.ownerSessionToken || ''
2015
+ ).trim();
1152
2016
  if (!authToken && !ownerSessionToken) {
1153
2017
  throw new Error(missingMessage);
1154
2018
  }
@@ -1177,6 +2041,116 @@ async function negotiateCapabilities(ctx) {
1177
2041
  }
1178
2042
  }
1179
2043
 
2044
+ function resolveUnsignedTx(result) {
2045
+ if (result?.unsignedTx && typeof result.unsignedTx === 'object') return result.unsignedTx;
2046
+ if (result?.data?.unsignedTx && typeof result.data.unsignedTx === 'object') return result.data.unsignedTx;
2047
+ if (result?.details?.unsignedTx && typeof result.details.unsignedTx === 'object') return result.details.unsignedTx;
2048
+ return null;
2049
+ }
2050
+
2051
+ async function loadEthers() {
2052
+ const candidates = ['ethers', path.join(process.cwd(), 'node_modules', 'ethers')];
2053
+ for (const candidate of candidates) {
2054
+ try {
2055
+ return require(candidate);
2056
+ } catch {
2057
+ // continue
2058
+ }
2059
+ }
2060
+ throw new Error('Could not load ethers dependency for self-exec');
2061
+ }
2062
+
2063
+ function resolveOwsBinary() {
2064
+ const explicit = String(process.env.OWS_CLI_PATH || '').trim();
2065
+ if (explicit && fs.existsSync(explicit)) return explicit;
2066
+ const localBin = path.join(process.cwd(), 'node_modules', '.bin', 'ows');
2067
+ if (fs.existsSync(localBin)) return localBin;
2068
+ const which = spawnSync('which', ['ows'], { encoding: 'utf-8' });
2069
+ const discovered = String(which.stdout || '').trim();
2070
+ if (discovered) return discovered;
2071
+ throw new Error('ows CLI not found. Install it or set OWS_CLI_PATH.');
2072
+ }
2073
+
2074
+ function signUnsignedTxWithOws(unsignedHex, walletName, chainId) {
2075
+ const passphrase = String(process.env.OWS_PASSPHRASE || process.env.OWS_API_KEY || '').trim();
2076
+ if (!passphrase) {
2077
+ throw new Error('Set OWS_PASSPHRASE (or OWS_API_KEY) for self-exec signing.');
2078
+ }
2079
+ const ows = resolveOwsBinary();
2080
+ const txHex = String(unsignedHex).startsWith('0x') ? String(unsignedHex).slice(2) : String(unsignedHex);
2081
+ const res = spawnSync(
2082
+ ows,
2083
+ ['sign', 'tx', '--wallet', walletName, '--chain', `eip155:${chainId}`, '--tx', txHex, '--json'],
2084
+ { env: { ...process.env, OWS_PASSPHRASE: passphrase }, encoding: 'utf-8' }
2085
+ );
2086
+ const out = String(res.stdout || '').trim();
2087
+ if (res.status !== 0 || !out) {
2088
+ throw new Error(`ows sign tx failed: ${String(res.stderr || out || res.status).slice(0, 300)}`);
2089
+ }
2090
+ let parsed;
2091
+ try {
2092
+ parsed = JSON.parse(out);
2093
+ } catch {
2094
+ throw new Error(`ows sign tx returned non-json payload: ${out.slice(0, 200)}`);
2095
+ }
2096
+ const signature = String(parsed.signature || '').replace(/^0x/i, '');
2097
+ if (signature.length < 128) {
2098
+ throw new Error('Invalid OWS signature payload');
2099
+ }
2100
+ return {
2101
+ recoveryId: Number(parsed.recovery_id),
2102
+ r: `0x${signature.slice(0, 64)}`,
2103
+ s: `0x${signature.slice(64, 128)}`,
2104
+ };
2105
+ }
2106
+
2107
+ async function signAndBroadcastWithOws({ txd, chainId, walletAddress, owsWalletName, rpcUrl }) {
2108
+ const ethers = await loadEthers();
2109
+ const { JsonRpcProvider, Transaction, Signature } = ethers;
2110
+ const provider = new JsonRpcProvider(rpcUrl, chainId);
2111
+ const nonce = await provider.getTransactionCount(walletAddress, 'pending');
2112
+ const fee = await provider.getFeeData();
2113
+ const txFields = {
2114
+ type: 2,
2115
+ chainId,
2116
+ nonce,
2117
+ to: String(txd.to).toLowerCase(),
2118
+ data: txd.data,
2119
+ value: txd.value ? BigInt(txd.value) : 0n,
2120
+ gasLimit: txd.gas ? BigInt(txd.gas) : txd.gasLimit ? BigInt(txd.gasLimit) : 500000n,
2121
+ maxFeePerGas: fee.maxFeePerGas ?? fee.gasPrice ?? 2_000_000_000n,
2122
+ maxPriorityFeePerGas: fee.maxPriorityFeePerGas ?? 1_000_000_000n,
2123
+ };
2124
+ const unsigned = Transaction.from(txFields);
2125
+ const sigParts = signUnsignedTxWithOws(unsigned.unsignedSerialized, owsWalletName, chainId);
2126
+ const sig = Signature.from({ r: sigParts.r, s: sigParts.s, yParity: sigParts.recoveryId });
2127
+ const tx = Transaction.from(unsigned.unsignedSerialized);
2128
+ tx.signature = sig;
2129
+ const sent = await provider.broadcastTransaction(tx.serialized);
2130
+ return { sent };
2131
+ }
2132
+
2133
+ async function readTokenBalanceRaw(rpcUrl, walletAddress, tokenAddress) {
2134
+ const ethers = await loadEthers();
2135
+ const { JsonRpcProvider, Interface } = ethers;
2136
+ const provider = new JsonRpcProvider(rpcUrl);
2137
+ const erc20 = new Interface(['function balanceOf(address owner) view returns (uint256)']);
2138
+ const response = await provider.call({
2139
+ to: tokenAddress,
2140
+ data: erc20.encodeFunctionData('balanceOf', [walletAddress]),
2141
+ });
2142
+ const decoded = erc20.decodeFunctionResult('balanceOf', response);
2143
+ return BigInt(decoded[0].toString()).toString();
2144
+ }
2145
+
2146
+ async function readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut) {
2147
+ const [inBal, outBal] = await Promise.all([
2148
+ readTokenBalanceRaw(rpcUrl, walletAddress, tokenIn),
2149
+ readTokenBalanceRaw(rpcUrl, walletAddress, tokenOut),
2150
+ ]);
2151
+ return { tokenIn: inBal, tokenOut: outBal };
2152
+ }
2153
+
1180
2154
  async function apiGet(ctx, route, headers = {}) {
1181
2155
  return requestJson(ctx, route, { method: 'GET', headers });
1182
2156
  }
@@ -1261,20 +2235,33 @@ Core commands:
1261
2235
  thirdfy-agent profile init [--profile personal|builder|network] [--run-mode thirdfy|self|hybrid] [--json]
1262
2236
  thirdfy-agent profile use --profile <name> [--run-mode thirdfy|self|hybrid] [--json]
1263
2237
  thirdfy-agent profile show [--json]
2238
+ thirdfy-agent config set --key <path> --value <value> [--json]
2239
+ thirdfy-agent login [--auth-token <token> | --owner-session-token <token>] [--agent-api-key <key>] [--json]
2240
+ thirdfy-agent logout [--all] [--clear-agent-key] [--json]
1264
2241
  thirdfy-agent whoami [--json]
1265
2242
  thirdfy-agent catalogs list [--json]
1266
2243
  thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--chain-id <id>] [--run-mode <mode>] [--json]
1267
- thirdfy-agent delegation upsert --user-did <did> --agent-key <key> --wallet-address <addr> [--json]
2244
+ 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]
2247
+ 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]
1268
2248
  thirdfy-agent delegation status --auth-token <token> --agent-key <key> [--verify] [--json]
2249
+ thirdfy-agent delegation show --auth-token <token> --agent-key <key> [--verify] [--json]
2250
+ thirdfy-agent delegation inspect --auth-token <token> --agent-key <key> [--verify] [--json]
2251
+ thirdfy-agent delegation balance --auth-token <token> --agent-key <key> [--chain-id <id>] [--json]
2252
+ thirdfy-agent delegation revoke --auth-token <token> --agent-key <key> [--reason <text>] [--chain-id <id>] [--json]
2253
+ thirdfy-agent delegation redeem --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--estimated-amount-usd <usd>] [--json]
1269
2254
  thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
1270
2255
  thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
1271
2256
  thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
1272
2257
  thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
2258
+ thirdfy-agent developer bootstrap --agent-key <key> --name <name> [--owner-session-token <token> | --challenge-id <id> --signature <hex>] [--json]
1273
2259
  thirdfy-agent agent register --agent-key <key> --name <name> [--auth-token <token> | --owner-session-token <token>] [--json]
1274
2260
  thirdfy-agent agent key rotate [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
1275
2261
  thirdfy-agent agent key revoke [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
1276
2262
  thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1277
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]
1278
2265
  thirdfy-agent jeff preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1279
2266
  thirdfy-agent jeff trade --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1280
2267
  thirdfy-agent jeff status --intent-id <id> [--json]
@@ -1288,6 +2275,7 @@ Global flags:
1288
2275
  --profile <name> personal | builder | network
1289
2276
  --env <file> load env vars from file
1290
2277
  --timeout <ms> request timeout
2278
+ --params <json> for swap: exactly one of {amountInRaw} or {amountInHuman + tokenInDecimals}
1291
2279
  --owner-session-token <token> owner session token (or THIRDFY_OWNER_SESSION_TOKEN)
1292
2280
  --json stable machine-readable output
1293
2281
  --verbose print adapter diagnostics