@thirdfy/agent-cli 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,8 @@ Use it to:
8
8
  - run governance preflight checks
9
9
  - queue execute-intent requests with idempotency
10
10
  - poll intent status with stable JSON output for automation
11
+ - choose execution topology with one switch: `thirdfy`, `self`, or `hybrid`
12
+ - configure profile-driven defaults (`personal`, `builder`, `network`)
11
13
 
12
14
  If you want the full developer docs, start here:
13
15
  - [Thirdfy Agent CLI docs](https://docs.thirdfy.com/api/thirdfy-agent-cli)
@@ -37,6 +39,7 @@ export THIRDFY_AUTH_TOKEN="..."
37
39
  export THIRDFY_OWNER_SESSION_TOKEN="..."
38
40
 
39
41
  thirdfy-agent actions --api-base "$THIRDFY_API_BASE" --agent-api-key "$AGENT_API_KEY" --json
42
+ thirdfy-agent profile init --profile personal --json
40
43
  thirdfy-agent preflight --api-base "$THIRDFY_API_BASE" --agent-api-key "$AGENT_API_KEY" --action swap --params '{"tokenIn":"0x...","tokenOut":"0x...","amountIn":"1000000"}' --estimated-amount-usd 25 --json
41
44
  thirdfy-agent run --api-base "$THIRDFY_API_BASE" --agent-api-key "$AGENT_API_KEY" --action swap --params '{"tokenIn":"0x...","tokenOut":"0x...","amountIn":"1000000"}' --estimated-amount-usd 25 --json
42
45
  thirdfy-agent intent-status --api-base "$THIRDFY_API_BASE" --intent-id "<intentId>" --json
@@ -70,10 +73,45 @@ thirdfy-agent agent register --api-base "$THIRDFY_API_BASE" --owner-session-toke
70
73
  - For deterministic retries across workers/restarts, pass your own stable `--idempotency-key`.
71
74
  - `preflight` does not force idempotency keys.
72
75
 
76
+ ## Run modes and profiles
77
+
78
+ `thirdfy-agent` supports one command surface with three run modes:
79
+
80
+ - `--run-mode thirdfy` -> Thirdfy-governed execute-intent rail (default for `profile=network`)
81
+ - `--run-mode self` -> self-custody rail (`build-tx` flow; default for `profile=personal`)
82
+ - `--run-mode hybrid` -> self-custody + governance mirror metadata (default for `profile=builder`)
83
+
84
+ Selection precedence:
85
+
86
+ 1. `--run-mode`
87
+ 2. `THIRDFY_RUN_MODE`
88
+ 3. saved profile config (`~/.thirdfy/config.json`)
89
+ 4. profile default
90
+
91
+ Profile commands:
92
+
93
+ ```bash
94
+ thirdfy-agent profile init --profile personal --json
95
+ thirdfy-agent profile use --profile builder --run-mode hybrid --json
96
+ thirdfy-agent whoami --json
97
+ ```
98
+
99
+ `profile use --profile <name>` resets run mode to that profile default unless `--run-mode` is explicitly provided.
100
+
101
+ ## Jeff shorthand rail
102
+
103
+ Jeff-friendly aliases map directly to core commands:
104
+
105
+ - `thirdfy-agent jeff preflight ...` -> alias of `preflight`
106
+ - `thirdfy-agent jeff trade ...` -> alias of `run`
107
+ - `thirdfy-agent jeff status --intent-id ...` -> alias of `intent-status`
108
+ - `thirdfy-agent prompt "<text>"` -> captures prompt intent and suggests deterministic execution commands
109
+
73
110
  ## Command groups
74
111
 
75
112
  - Discovery: `catalogs list`, `actions`
76
- - Execution: `preflight`, `run`, `intent-status`
113
+ - Execution: `preflight`, `run`, `intent-status`, `jeff preflight`, `jeff trade`, `jeff status`
114
+ - Profiles: `profile init`, `profile use`, `profile show`, `whoami`
77
115
  - Onboarding: `agent auth challenge`, `agent auth verify`, `agent register`, `agent key rotate`, `agent key revoke`
78
116
  - Governance readiness: `delegation upsert`, `delegation status`, `credentials upsert`, `credentials status`
79
117
  - Account: `credits balance`
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { randomUUID } from 'crypto';
4
4
  import fs from 'fs';
5
+ import os from 'os';
5
6
  import path from 'path';
6
7
  import { createRequire } from 'module';
7
8
 
@@ -16,6 +17,12 @@ const {
16
17
  const DEFAULT_API_BASE = 'https://api.thirdfy.com';
17
18
  const DEFAULT_TIMEOUT_MS = 30000;
18
19
  const DEFAULT_CATALOG_CACHE_TTL_MS = 60_000;
20
+ const RUN_MODES = ['thirdfy', 'self', 'hybrid'];
21
+ const PROFILE_DEFAULTS = {
22
+ personal: 'self',
23
+ builder: 'hybrid',
24
+ network: 'thirdfy',
25
+ };
19
26
  const catalogCache = new Map();
20
27
 
21
28
  const rawArgv = process.argv.slice(2);
@@ -59,6 +66,11 @@ main().catch((error) => {
59
66
  });
60
67
 
61
68
  async function main() {
69
+ const profileState = resolveProfileState(globalFlags);
70
+ context.profile = profileState.profile;
71
+ context.runMode = profileState.runMode;
72
+ context.profileConfigPath = profileState.configPath;
73
+
62
74
  if (globalFlags.version) {
63
75
  printEnvelope({
64
76
  success: true,
@@ -75,12 +87,43 @@ async function main() {
75
87
  return;
76
88
  }
77
89
 
78
- const capabilities = await negotiateCapabilities(context);
79
90
  const commandKey = parsed.positionals.slice(0, 2).join(' ');
80
91
  const commandKey3 = parsed.positionals.slice(0, 3).join(' ');
81
92
  const subFlags = parsed.flags;
82
93
 
94
+ // Pure local profile/config commands should work offline.
95
+ switch (commandKey) {
96
+ case 'profile init':
97
+ await commandProfileInit(context, subFlags);
98
+ return;
99
+ case 'profile use':
100
+ await commandProfileUse(context, subFlags);
101
+ return;
102
+ case 'profile show':
103
+ await commandProfileShow(context, subFlags);
104
+ return;
105
+ case 'config get':
106
+ await commandProfileShow(context, subFlags);
107
+ return;
108
+ case 'whoami':
109
+ await commandProfileShow(context, subFlags);
110
+ return;
111
+ default:
112
+ break;
113
+ }
114
+
115
+ const capabilities = await negotiateCapabilities(context);
116
+
83
117
  switch (commandKey) {
118
+ case 'jeff trade':
119
+ await commandRun(context, subFlags, capabilities);
120
+ return;
121
+ case 'jeff preflight':
122
+ await commandPreflight(context, subFlags, capabilities);
123
+ return;
124
+ case 'jeff status':
125
+ await commandIntentStatus(context, subFlags, capabilities);
126
+ return;
84
127
  case 'catalogs list':
85
128
  await commandCatalogsList(context, subFlags, capabilities);
86
129
  return;
@@ -124,6 +167,9 @@ async function main() {
124
167
 
125
168
  const rootCommand = parsed.positionals[0];
126
169
  switch (rootCommand) {
170
+ case 'prompt':
171
+ await commandPrompt(context, subFlags, capabilities, parsed.positionals.slice(1));
172
+ return;
127
173
  case 'actions':
128
174
  await commandActions(context, subFlags, capabilities);
129
175
  return;
@@ -236,6 +282,9 @@ async function commandCatalogsList(ctx, flags, capabilities) {
236
282
  async function commandActions(ctx, flags, capabilities) {
237
283
  const policyAware = Boolean(flags.agentApiKey);
238
284
  const actions = applyActionFilters(await getCachedActionsCatalog(ctx, flags), flags);
285
+ const effectiveRunMode = getEffectiveRunMode(ctx, flags);
286
+ const chainIdFilter = Number(flags.chainId || 0) || undefined;
287
+ const chainSupport = resolveChainSupport(capabilities, chainIdFilter, effectiveRunMode);
239
288
  printEnvelope({
240
289
  success: true,
241
290
  code: 'OK',
@@ -244,6 +293,9 @@ async function commandActions(ctx, flags, capabilities) {
244
293
  meta: {
245
294
  apiBase: ctx.apiBase,
246
295
  policyAware,
296
+ runMode: effectiveRunMode,
297
+ profile: ctx.profile || null,
298
+ chainCompatibility: chainSupport,
247
299
  capabilitiesVersion: capabilities?.contractVersion || null,
248
300
  },
249
301
  });
@@ -251,23 +303,22 @@ async function commandActions(ctx, flags, capabilities) {
251
303
 
252
304
  async function commandPreflight(ctx, flags, capabilities) {
253
305
  const resolved = await resolveActionSelection(ctx, flags);
254
- const payload = buildIntentPayload(flags, {
255
- validationOnly: true,
256
- forceIdempotency: false,
257
- resolvedAction: resolved.resolvedAction,
258
- });
259
- const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
260
- const normalized = normalizeIntentResponse(response);
306
+ const runMode = getEffectiveRunMode(ctx, flags);
307
+ enforceChainCompatibility(capabilities, flags, runMode);
308
+ const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved.resolvedAction);
309
+ const normalized = backendResult.normalized;
261
310
  printEnvelope({
262
311
  success: normalized.success,
263
312
  code: normalized.success ? 'PREFLIGHT_OK' : 'PREFLIGHT_FAILED',
264
- message: normalized.success ? 'Preflight completed' : 'Preflight failed',
313
+ message: backendResult.message,
265
314
  data: normalized,
266
315
  meta: {
267
316
  apiBase: ctx.apiBase,
268
317
  catalog: flags.catalog || null,
269
318
  requestedAction: resolved.requestedAction,
270
319
  resolvedAction: resolved.resolvedAction,
320
+ runMode,
321
+ profile: ctx.profile || null,
271
322
  capabilitiesVersion: capabilities?.contractVersion || null,
272
323
  },
273
324
  });
@@ -276,52 +327,25 @@ async function commandPreflight(ctx, flags, capabilities) {
276
327
 
277
328
  async function commandRun(ctx, flags, capabilities) {
278
329
  const resolved = await resolveActionSelection(ctx, flags);
330
+ const runMode = getEffectiveRunMode(ctx, flags);
331
+ enforceChainCompatibility(capabilities, flags, runMode);
279
332
  const skipPreflight = Boolean(flags.skipPreflight);
280
- if (!skipPreflight) {
281
- const preflightPayload = buildIntentPayload(flags, {
282
- validationOnly: true,
283
- forceIdempotency: false,
284
- resolvedAction: resolved.resolvedAction,
285
- });
286
- const preflight = await apiPost(ctx, '/api/v1/agent/execute-intent', preflightPayload);
287
- const preflightNormalized = normalizeIntentResponse(preflight);
288
- const blockedCount = preflightNormalized.blocked || 0;
289
- if (!preflightNormalized.success || blockedCount > 0) {
290
- printEnvelope({
291
- success: false,
292
- code: 'PREFLIGHT_BLOCKED',
293
- message: 'Preflight blocked execution',
294
- data: preflightNormalized,
295
- meta: {
296
- apiBase: ctx.apiBase,
297
- catalog: flags.catalog || null,
298
- requestedAction: resolved.requestedAction,
299
- resolvedAction: resolved.resolvedAction,
300
- skippedPreflight: false,
301
- },
302
- });
303
- process.exit(1);
304
- return;
305
- }
306
- }
307
-
308
- const payload = buildIntentPayload(flags, {
309
- validationOnly: false,
310
- forceIdempotency: true,
311
- resolvedAction: resolved.resolvedAction,
333
+ const backendResult = await runExecutionByMode(runMode, ctx, flags, resolved, {
334
+ skipPreflight,
312
335
  });
313
- const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
314
- const normalized = normalizeIntentResponse(response);
336
+ const normalized = backendResult.normalized;
315
337
  printEnvelope({
316
338
  success: normalized.success,
317
- code: normalized.success ? 'RUN_QUEUED' : 'RUN_FAILED',
318
- message: normalized.success ? 'Execution intent queued' : 'Execution failed',
339
+ code: backendResult.code,
340
+ message: backendResult.message,
319
341
  data: normalized,
320
342
  meta: {
321
343
  apiBase: ctx.apiBase,
322
344
  requestedAction: resolved.requestedAction,
323
345
  resolvedAction: resolved.resolvedAction,
324
- idempotencyKey: payload.idempotencyKey || null,
346
+ idempotencyKey: backendResult.idempotencyKey || null,
347
+ runMode,
348
+ profile: ctx.profile || null,
325
349
  skippedPreflight: skipPreflight,
326
350
  capabilitiesVersion: capabilities?.contractVersion || null,
327
351
  },
@@ -609,6 +633,366 @@ async function commandAgentAuthVerify(ctx, flags, capabilities) {
609
633
  });
610
634
  }
611
635
 
636
+ async function commandPrompt(ctx, flags, capabilities, promptTokens) {
637
+ const prompt = String(flags.prompt || promptTokens.join(' ') || '').trim();
638
+ if (!prompt) {
639
+ throw new Error('Missing prompt text. Use: thirdfy-agent prompt "<text>"');
640
+ }
641
+ // Jeff rail: deterministic wrapper around existing run contract.
642
+ printEnvelope({
643
+ success: true,
644
+ code: 'PROMPT_RECEIVED',
645
+ message: 'Prompt captured; execute with `jeff trade` or `run` using --run-mode.',
646
+ data: {
647
+ prompt,
648
+ suggestedCommands: [
649
+ 'thirdfy-agent jeff trade --run-mode self --agent-api-key ... --action ... --params ...',
650
+ 'thirdfy-agent jeff trade --run-mode hybrid --agent-api-key ... --action ... --params ...',
651
+ ],
652
+ },
653
+ meta: {
654
+ apiBase: ctx.apiBase,
655
+ profile: ctx.profile || null,
656
+ runMode: getEffectiveRunMode(ctx, flags),
657
+ capabilitiesVersion: capabilities?.contractVersion || null,
658
+ },
659
+ });
660
+ }
661
+
662
+ async function commandProfileInit(ctx, flags) {
663
+ const profile = normalizeProfileName(flags.profile || flags.name || 'personal');
664
+ const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
665
+ persistProfileConfig({
666
+ profile,
667
+ runMode,
668
+ });
669
+ ctx.profile = profile;
670
+ ctx.runMode = runMode;
671
+ printEnvelope({
672
+ success: true,
673
+ code: 'PROFILE_INITIALIZED',
674
+ message: 'Profile initialized',
675
+ data: {
676
+ profile,
677
+ runMode,
678
+ configPath: getProfileConfigPath(),
679
+ },
680
+ meta: { apiBase: ctx.apiBase },
681
+ });
682
+ }
683
+
684
+ async function commandProfileUse(ctx, flags) {
685
+ const current = loadProfileConfig();
686
+ const profile = normalizeProfileName(flags.profile || flags.name || current.profile || 'personal');
687
+ // `profile use` intentionally re-applies profile defaults unless caller pins a mode.
688
+ const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
689
+ persistProfileConfig({
690
+ profile,
691
+ runMode,
692
+ });
693
+ ctx.profile = profile;
694
+ ctx.runMode = runMode;
695
+ printEnvelope({
696
+ success: true,
697
+ code: 'PROFILE_UPDATED',
698
+ message: 'Profile updated',
699
+ data: {
700
+ profile,
701
+ runMode,
702
+ defaults: PROFILE_DEFAULTS,
703
+ configPath: getProfileConfigPath(),
704
+ },
705
+ meta: { apiBase: ctx.apiBase },
706
+ });
707
+ }
708
+
709
+ async function commandProfileShow(ctx, _flags) {
710
+ const config = loadProfileConfig();
711
+ printEnvelope({
712
+ success: true,
713
+ code: 'PROFILE_OK',
714
+ message: 'Profile configuration loaded',
715
+ data: {
716
+ profile: ctx.profile || config.profile || 'personal',
717
+ runMode: ctx.runMode || config.runMode || 'thirdfy',
718
+ profileDefaults: PROFILE_DEFAULTS,
719
+ configPath: getProfileConfigPath(),
720
+ config,
721
+ },
722
+ meta: { apiBase: ctx.apiBase },
723
+ });
724
+ }
725
+
726
+ function resolveProfileState(flags) {
727
+ const persisted = loadProfileConfig();
728
+ const profile = normalizeProfileName(flags.profile || process.env.THIRDFY_PROFILE || persisted.profile || 'personal');
729
+ const runMode = normalizeRunMode(
730
+ flags.runMode ||
731
+ process.env.THIRDFY_RUN_MODE ||
732
+ persisted.runMode ||
733
+ PROFILE_DEFAULTS[profile] ||
734
+ 'thirdfy'
735
+ );
736
+ return {
737
+ profile,
738
+ runMode,
739
+ configPath: getProfileConfigPath(),
740
+ };
741
+ }
742
+
743
+ function normalizeProfileName(value) {
744
+ const normalized = String(value || '').trim().toLowerCase();
745
+ if (normalized in PROFILE_DEFAULTS) return normalized;
746
+ throw new Error(`Unsupported profile "${value}". Supported: ${Object.keys(PROFILE_DEFAULTS).join(', ')}`);
747
+ }
748
+
749
+ function normalizeRunMode(value) {
750
+ const normalized = String(value || '').trim().toLowerCase();
751
+ if (!normalized) return 'thirdfy';
752
+ if (RUN_MODES.includes(normalized)) return normalized;
753
+ throw new Error(`Unsupported --run-mode "${value}". Supported: ${RUN_MODES.join(', ')}`);
754
+ }
755
+
756
+ function getProfileConfigPath() {
757
+ return path.join(os.homedir(), '.thirdfy', 'config.json');
758
+ }
759
+
760
+ function loadProfileConfig() {
761
+ try {
762
+ const raw = fs.readFileSync(getProfileConfigPath(), 'utf8');
763
+ const parsed = JSON.parse(raw);
764
+ if (!parsed || typeof parsed !== 'object') return {};
765
+ return parsed;
766
+ } catch {
767
+ return {};
768
+ }
769
+ }
770
+
771
+ function persistProfileConfig(config) {
772
+ const configPath = getProfileConfigPath();
773
+ const dirPath = path.dirname(configPath);
774
+ fs.mkdirSync(dirPath, { recursive: true });
775
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
776
+ }
777
+
778
+ function getEffectiveRunMode(ctx, flags) {
779
+ if (flags.runMode) return normalizeRunMode(flags.runMode);
780
+ return normalizeRunMode(ctx.runMode || 'thirdfy');
781
+ }
782
+
783
+ function resolveChainSupport(capabilities, chainId, runMode) {
784
+ const chains = Array.isArray(capabilities?.chains) ? capabilities.chains : [];
785
+ if (!chainId) return { chainId: null, supported: null };
786
+ const match = chains.find((entry) => Number(entry.chainId) === Number(chainId));
787
+ if (!match) {
788
+ return { chainId, supported: false, reason: 'chain_not_advertised_by_api' };
789
+ }
790
+ const lanes = Array.isArray(match.supportedExecutionLanes) ? match.supportedExecutionLanes : [];
791
+ return {
792
+ chainId,
793
+ supported: lanes.includes(runMode),
794
+ runMode,
795
+ chainKey: match.chainKey || null,
796
+ displayName: match.displayName || null,
797
+ };
798
+ }
799
+
800
+ function enforceChainCompatibility(capabilities, flags, runMode) {
801
+ const chainId = Number(flags.chainId || 0) || undefined;
802
+ if (!chainId) return;
803
+ if (!capabilities || !Array.isArray(capabilities.chains)) return;
804
+ const support = resolveChainSupport(capabilities, chainId, runMode);
805
+ if (support?.supported === false) {
806
+ throw new Error(
807
+ `Chain ${chainId} is not advertised as compatible with run-mode=${runMode}. Check \`thirdfy-agent actions --chain-id ${chainId}\`.`
808
+ );
809
+ }
810
+ }
811
+
812
+ async function runPreflightByMode(runMode, ctx, flags, resolvedAction) {
813
+ if (runMode === 'self') {
814
+ const normalized = await executeSelfRun(
815
+ ctx,
816
+ flags,
817
+ { resolvedAction },
818
+ { skipPreflight: false }
819
+ );
820
+ const blockedByGovernance = !normalized.success && Boolean(normalized.preflightBlocked);
821
+ return {
822
+ message: blockedByGovernance
823
+ ? 'Self preflight blocked by governance'
824
+ : normalized.success
825
+ ? 'Self preflight completed (unsigned transaction ready)'
826
+ : 'Self preflight failed',
827
+ normalized,
828
+ };
829
+ }
830
+ const payload = buildIntentPayload(flags, {
831
+ validationOnly: true,
832
+ forceIdempotency: false,
833
+ resolvedAction,
834
+ runMode,
835
+ mirrorOnly: runMode === 'hybrid',
836
+ });
837
+ const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
838
+ const normalized = normalizeIntentResponse(response);
839
+ return {
840
+ message: normalized.success
841
+ ? runMode === 'hybrid'
842
+ ? 'Hybrid mirror preflight completed'
843
+ : 'Preflight completed'
844
+ : runMode === 'hybrid'
845
+ ? 'Hybrid mirror preflight failed'
846
+ : 'Preflight failed',
847
+ normalized,
848
+ };
849
+ }
850
+
851
+ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
852
+ if (runMode === 'thirdfy') {
853
+ const normalized = await executeThirdfyRun(ctx, flags, resolved, options);
854
+ const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
855
+ return {
856
+ code: normalized.success ? 'RUN_QUEUED' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
857
+ message: normalized.success
858
+ ? 'Execution intent queued'
859
+ : preflightBlocked
860
+ ? 'Preflight blocked execution'
861
+ : 'Execution failed',
862
+ idempotencyKey: normalized.idempotencyKey || null,
863
+ normalized,
864
+ };
865
+ }
866
+ if (runMode === 'self') {
867
+ const normalized = await executeSelfRun(ctx, flags, resolved, options);
868
+ const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
869
+ return {
870
+ code: normalized.success
871
+ ? 'SELF_UNSIGNED_TX_READY'
872
+ : preflightBlocked
873
+ ? 'PREFLIGHT_BLOCKED'
874
+ : 'SELF_EXECUTION_FAILED',
875
+ message: normalized.success
876
+ ? 'Unsigned transaction prepared for self-custody execution'
877
+ : preflightBlocked
878
+ ? 'Preflight blocked self execution'
879
+ : 'Self-custody execution failed',
880
+ idempotencyKey: null,
881
+ normalized,
882
+ };
883
+ }
884
+ const normalized = await executeHybridRun(ctx, flags, resolved, options);
885
+ const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
886
+ return {
887
+ code: normalized.success ? 'HYBRID_READY' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'HYBRID_FAILED',
888
+ message: normalized.success
889
+ ? 'Hybrid execution prepared (self tx + mirror preflight)'
890
+ : preflightBlocked
891
+ ? 'Preflight blocked hybrid execution'
892
+ : 'Hybrid execution failed',
893
+ idempotencyKey: normalized.idempotencyKey || null,
894
+ normalized,
895
+ };
896
+ }
897
+
898
+ async function executeThirdfyRun(ctx, flags, resolved, options) {
899
+ const skipPreflight = Boolean(options?.skipPreflight);
900
+ if (!skipPreflight) {
901
+ const preflightPayload = buildIntentPayload(flags, {
902
+ validationOnly: true,
903
+ forceIdempotency: false,
904
+ resolvedAction: resolved.resolvedAction,
905
+ runMode: 'thirdfy',
906
+ mirrorOnly: false,
907
+ });
908
+ const preflight = await apiPost(ctx, '/api/v1/agent/execute-intent', preflightPayload);
909
+ const preflightNormalized = normalizeIntentResponse(preflight);
910
+ const blockedCount = preflightNormalized.blocked || 0;
911
+ if (!preflightNormalized.success || blockedCount > 0) {
912
+ preflightNormalized.success = false;
913
+ preflightNormalized.error = preflightNormalized.error || 'Preflight blocked execution';
914
+ preflightNormalized.preflightBlocked = true;
915
+ return preflightNormalized;
916
+ }
917
+ }
918
+ const payload = buildIntentPayload(flags, {
919
+ validationOnly: false,
920
+ forceIdempotency: true,
921
+ resolvedAction: resolved.resolvedAction,
922
+ runMode: 'thirdfy',
923
+ mirrorOnly: false,
924
+ });
925
+ const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
926
+ const normalized = normalizeIntentResponse(response);
927
+ normalized.idempotencyKey = payload.idempotencyKey;
928
+ return normalized;
929
+ }
930
+
931
+ async function executeSelfRun(ctx, flags, resolved, options) {
932
+ const skipPreflight = Boolean(options?.skipPreflight);
933
+ if (!skipPreflight) {
934
+ const preflightPayload = buildIntentPayload(flags, {
935
+ validationOnly: true,
936
+ forceIdempotency: false,
937
+ resolvedAction: resolved.resolvedAction,
938
+ runMode: 'self',
939
+ mirrorOnly: false,
940
+ });
941
+ const preflight = await apiPost(ctx, '/api/v1/agent/execute-intent', preflightPayload);
942
+ const preflightNormalized = normalizeIntentResponse(preflight);
943
+ const blockedCount = preflightNormalized.blocked || 0;
944
+ if (!preflightNormalized.success || blockedCount > 0) {
945
+ preflightNormalized.success = false;
946
+ preflightNormalized.error = preflightNormalized.error || 'Preflight blocked self execution';
947
+ preflightNormalized.preflightBlocked = true;
948
+ return preflightNormalized;
949
+ }
950
+ }
951
+ const buildTxPayload = buildBuildTxPayload(flags, resolved.resolvedAction);
952
+ const buildTx = await apiPost(ctx, '/api/v1/agent/build-tx', buildTxPayload);
953
+ return normalizeIntentResponse({
954
+ ...buildTx,
955
+ status: buildTx?.status || 'ready',
956
+ mode: buildTx?.mode || 'self',
957
+ unsignedTx: buildTx?.unsignedTx || null,
958
+ estimatedGas: buildTx?.estimatedGas || null,
959
+ });
960
+ }
961
+
962
+ async function executeHybridRun(ctx, flags, resolved, options) {
963
+ const selfResult = await executeSelfRun(ctx, flags, resolved, options);
964
+ if (!selfResult.success) return selfResult;
965
+
966
+ const mirrorPayload = buildIntentPayload(flags, {
967
+ validationOnly: true,
968
+ forceIdempotency: true,
969
+ resolvedAction: resolved.resolvedAction,
970
+ runMode: 'hybrid',
971
+ mirrorOnly: true,
972
+ });
973
+ const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
974
+ const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
975
+ return normalizeIntentResponse({
976
+ success: Boolean(selfResult.success && mirrorNormalized.success),
977
+ status: mirrorNormalized.status || 'ready',
978
+ mode: 'hybrid',
979
+ idempotencyKey: mirrorPayload.idempotencyKey,
980
+ self: selfResult,
981
+ mirror: mirrorNormalized,
982
+ });
983
+ }
984
+
985
+ function buildBuildTxPayload(flags, resolvedAction) {
986
+ const payload = {
987
+ agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
988
+ action: String(resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
989
+ params: parseJsonFlag(flags, 'params'),
990
+ };
991
+ if (flags.chainId) payload.chainId = Number(flags.chainId);
992
+ if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
993
+ return payload;
994
+ }
995
+
612
996
  function buildIntentPayload(flags, options) {
613
997
  const payload = {
614
998
  agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
@@ -620,6 +1004,12 @@ function buildIntentPayload(flags, options) {
620
1004
  if (flags.estimatedAmountUsd) payload.estimatedAmountUsd = Number(flags.estimatedAmountUsd);
621
1005
  if (flags.userDid) payload.userDid = String(flags.userDid);
622
1006
  if (options.validationOnly) payload.executionLane = 'validation_only';
1007
+ if (options.runMode) {
1008
+ payload.executionStrategy = {
1009
+ runMode: String(options.runMode),
1010
+ mirrorOnly: Boolean(options.mirrorOnly),
1011
+ };
1012
+ }
623
1013
  if (options.forceIdempotency) {
624
1014
  payload.idempotencyKey = String(flags.idempotencyKey || `cli:${payload.action}:${Date.now()}:${randomUUID()}`);
625
1015
  } else if (flags.idempotencyKey) {
@@ -704,15 +1094,29 @@ async function resolveActionSelection(ctx, flags) {
704
1094
  return { requestedAction, resolvedAction: requestedAction };
705
1095
  }
706
1096
 
707
- const exact = keyed.filter(
708
- (entry) => entry.key.toLowerCase() === requestedLower || entry.aliases.some((a) => a.toLowerCase() === requestedLower)
709
- );
710
- if (exact.length === 1) {
711
- return { requestedAction, resolvedAction: exact[0].key };
1097
+ // Prefer direct action id matches over alias matches.
1098
+ // This keeps UX simple for commands like `--action swap` when another action aliases to "swap".
1099
+ const exactKey = keyed.filter((entry) => entry.key.toLowerCase() === requestedLower);
1100
+ if (exactKey.length === 1) {
1101
+ return { requestedAction, resolvedAction: exactKey[0].key };
1102
+ }
1103
+ if (exactKey.length > 1) {
1104
+ throw new Error(
1105
+ `Ambiguous --action "${requestedAction}". Multiple exact matches found: ${formatActionList(
1106
+ exactKey.map((v) => v.key)
1107
+ )}.`
1108
+ );
1109
+ }
1110
+
1111
+ const exactAlias = keyed.filter((entry) => entry.aliases.some((a) => a.toLowerCase() === requestedLower));
1112
+ if (exactAlias.length === 1) {
1113
+ return { requestedAction, resolvedAction: exactAlias[0].key };
712
1114
  }
713
- if (exact.length > 1) {
1115
+ if (exactAlias.length > 1) {
714
1116
  throw new Error(
715
- `Ambiguous --action "${requestedAction}". Multiple exact matches found: ${formatActionList(exact.map((v) => v.key))}.`
1117
+ `Ambiguous --action "${requestedAction}". Alias matches multiple actions: ${formatActionList(
1118
+ exactAlias.map((v) => v.key)
1119
+ )}.`
716
1120
  );
717
1121
  }
718
1122
 
@@ -854,8 +1258,12 @@ function printHelp() {
854
1258
  thirdfy-agent ${CLI_VERSION}
855
1259
 
856
1260
  Core commands:
1261
+ thirdfy-agent profile init [--profile personal|builder|network] [--run-mode thirdfy|self|hybrid] [--json]
1262
+ thirdfy-agent profile use --profile <name> [--run-mode thirdfy|self|hybrid] [--json]
1263
+ thirdfy-agent profile show [--json]
1264
+ thirdfy-agent whoami [--json]
857
1265
  thirdfy-agent catalogs list [--json]
858
- thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--json]
1266
+ thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--chain-id <id>] [--run-mode <mode>] [--json]
859
1267
  thirdfy-agent delegation upsert --user-did <did> --agent-key <key> --wallet-address <addr> [--json]
860
1268
  thirdfy-agent delegation status --auth-token <token> --agent-key <key> [--verify] [--json]
861
1269
  thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
@@ -865,13 +1273,19 @@ Core commands:
865
1273
  thirdfy-agent agent register --agent-key <key> --name <name> [--auth-token <token> | --owner-session-token <token>] [--json]
866
1274
  thirdfy-agent agent key rotate [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
867
1275
  thirdfy-agent agent key revoke [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
868
- thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--json]
869
- thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--idempotency-key <key>] [--json]
1276
+ thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1277
+ thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--idempotency-key <key>] [--json]
1278
+ thirdfy-agent jeff preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1279
+ thirdfy-agent jeff trade --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1280
+ thirdfy-agent jeff status --intent-id <id> [--json]
1281
+ thirdfy-agent prompt "<natural language prompt>" [--json]
870
1282
  thirdfy-agent intent-status --intent-id <id> [--json]
871
1283
  thirdfy-agent credits balance --auth-token <token> [--json]
872
1284
 
873
1285
  Global flags:
874
1286
  --api-base <url> default: https://api.thirdfy.com
1287
+ --run-mode <mode> thirdfy | self | hybrid
1288
+ --profile <name> personal | builder | network
875
1289
  --env <file> load env vars from file
876
1290
  --timeout <ms> request timeout
877
1291
  --owner-session-token <token> owner session token (or THIRDFY_OWNER_SESSION_TOKEN)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,12 @@ function normalizeIntentResponse(response) {
30
30
  results,
31
31
  blockedByReason: summarizeBlockedReasons(results),
32
32
  error: response?.error || null,
33
+ mode: response?.mode || null,
34
+ unsignedTx: response?.unsignedTx || null,
35
+ estimatedGas: response?.estimatedGas || null,
36
+ idempotencyKey: response?.idempotencyKey || null,
37
+ mirror: response?.mirror || null,
38
+ self: response?.self || null,
33
39
  };
34
40
  }
35
41