@thirdfy/agent-cli 0.1.2 → 0.1.4

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)
@@ -34,25 +36,83 @@ npx @thirdfy/agent-cli --help
34
36
  export THIRDFY_API_BASE="https://api.thirdfy.com"
35
37
  export AGENT_API_KEY="..."
36
38
  export THIRDFY_AUTH_TOKEN="..."
39
+ export THIRDFY_OWNER_SESSION_TOKEN="..."
37
40
 
38
41
  thirdfy-agent actions --api-base "$THIRDFY_API_BASE" --agent-api-key "$AGENT_API_KEY" --json
42
+ thirdfy-agent profile init --profile personal --json
39
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
40
- 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 --idempotency-key "swap-001" --json
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
41
45
  thirdfy-agent intent-status --api-base "$THIRDFY_API_BASE" --intent-id "<intentId>" --json
42
46
  ```
43
47
 
44
48
  ## Auth model
45
49
 
46
50
  - `AGENT_API_KEY`: execution identity and policy-aware action access (`actions --agent-api-key`, `preflight`, `run`)
47
- - `THIRDFY_AUTH_TOKEN`: user-scoped onboarding and account/governance operations (`agent register`, `agent key *`, `delegation *`, `credentials *`, `credits balance`)
51
+ - `THIRDFY_AUTH_TOKEN`: Privy-native owner/account auth (`agent register`, `agent key *`, `delegation *`, `credentials *`, `credits balance`)
52
+ - `THIRDFY_OWNER_SESSION_TOKEN`: wallet-sign owner auth session (`agent register`, `agent key *`)
48
53
 
49
54
  Execution-only workflows can run with only `AGENT_API_KEY` after onboarding is complete.
50
55
 
56
+ ### Wallet-sign onboarding path
57
+
58
+ ```bash
59
+ thirdfy-agent agent auth challenge --api-base "$THIRDFY_API_BASE" --agent-key "0xYOUR_AGENT_KEY" --json
60
+ # Sign challenge.message with your wallet, then:
61
+ thirdfy-agent agent auth verify --api-base "$THIRDFY_API_BASE" --challenge-id "<challengeId>" --signature "0x..." --agent-key "0xYOUR_AGENT_KEY" --json
62
+ ```
63
+
64
+ Use the returned session token:
65
+
66
+ ```bash
67
+ thirdfy-agent agent register --api-base "$THIRDFY_API_BASE" --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN" --agent-key "0xYOUR_AGENT_KEY" --name "my-agent" --json
68
+ ```
69
+
70
+ ### Idempotency behavior (`run`)
71
+
72
+ - `run` auto-generates `idempotencyKey` by default when not provided.
73
+ - For deterministic retries across workers/restarts, pass your own stable `--idempotency-key`.
74
+ - `preflight` does not force idempotency keys.
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
+
51
110
  ## Command groups
52
111
 
53
112
  - Discovery: `catalogs list`, `actions`
54
- - Execution: `preflight`, `run`, `intent-status`
55
- - Onboarding: `agent register`, `agent key rotate`, `agent key revoke`
113
+ - Execution: `preflight`, `run`, `intent-status`, `jeff preflight`, `jeff trade`, `jeff status`
114
+ - Profiles: `profile init`, `profile use`, `profile show`, `whoami`
115
+ - Onboarding: `agent auth challenge`, `agent auth verify`, `agent register`, `agent key rotate`, `agent key revoke`
56
116
  - Governance readiness: `delegation upsert`, `delegation status`, `credentials upsert`, `credentials status`
57
117
  - Account: `credits balance`
58
118
 
@@ -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;
@@ -113,9 +156,20 @@ async function main() {
113
156
  await commandAgentKeyRevoke(context, subFlags, capabilities);
114
157
  return;
115
158
  }
159
+ if (commandKey3 === 'agent auth challenge') {
160
+ await commandAgentAuthChallenge(context, subFlags, capabilities);
161
+ return;
162
+ }
163
+ if (commandKey3 === 'agent auth verify') {
164
+ await commandAgentAuthVerify(context, subFlags, capabilities);
165
+ return;
166
+ }
116
167
 
117
168
  const rootCommand = parsed.positionals[0];
118
169
  switch (rootCommand) {
170
+ case 'prompt':
171
+ await commandPrompt(context, subFlags, capabilities, parsed.positionals.slice(1));
172
+ return;
119
173
  case 'actions':
120
174
  await commandActions(context, subFlags, capabilities);
121
175
  return;
@@ -228,6 +282,9 @@ async function commandCatalogsList(ctx, flags, capabilities) {
228
282
  async function commandActions(ctx, flags, capabilities) {
229
283
  const policyAware = Boolean(flags.agentApiKey);
230
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);
231
288
  printEnvelope({
232
289
  success: true,
233
290
  code: 'OK',
@@ -236,6 +293,9 @@ async function commandActions(ctx, flags, capabilities) {
236
293
  meta: {
237
294
  apiBase: ctx.apiBase,
238
295
  policyAware,
296
+ runMode: effectiveRunMode,
297
+ profile: ctx.profile || null,
298
+ chainCompatibility: chainSupport,
239
299
  capabilitiesVersion: capabilities?.contractVersion || null,
240
300
  },
241
301
  });
@@ -243,23 +303,22 @@ async function commandActions(ctx, flags, capabilities) {
243
303
 
244
304
  async function commandPreflight(ctx, flags, capabilities) {
245
305
  const resolved = await resolveActionSelection(ctx, flags);
246
- const payload = buildIntentPayload(flags, {
247
- validationOnly: true,
248
- forceIdempotency: false,
249
- resolvedAction: resolved.resolvedAction,
250
- });
251
- const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
252
- 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;
253
310
  printEnvelope({
254
311
  success: normalized.success,
255
312
  code: normalized.success ? 'PREFLIGHT_OK' : 'PREFLIGHT_FAILED',
256
- message: normalized.success ? 'Preflight completed' : 'Preflight failed',
313
+ message: backendResult.message,
257
314
  data: normalized,
258
315
  meta: {
259
316
  apiBase: ctx.apiBase,
260
317
  catalog: flags.catalog || null,
261
318
  requestedAction: resolved.requestedAction,
262
319
  resolvedAction: resolved.resolvedAction,
320
+ runMode,
321
+ profile: ctx.profile || null,
263
322
  capabilitiesVersion: capabilities?.contractVersion || null,
264
323
  },
265
324
  });
@@ -268,52 +327,25 @@ async function commandPreflight(ctx, flags, capabilities) {
268
327
 
269
328
  async function commandRun(ctx, flags, capabilities) {
270
329
  const resolved = await resolveActionSelection(ctx, flags);
330
+ const runMode = getEffectiveRunMode(ctx, flags);
331
+ enforceChainCompatibility(capabilities, flags, runMode);
271
332
  const skipPreflight = Boolean(flags.skipPreflight);
272
- if (!skipPreflight) {
273
- const preflightPayload = buildIntentPayload(flags, {
274
- validationOnly: true,
275
- forceIdempotency: false,
276
- resolvedAction: resolved.resolvedAction,
277
- });
278
- const preflight = await apiPost(ctx, '/api/v1/agent/execute-intent', preflightPayload);
279
- const preflightNormalized = normalizeIntentResponse(preflight);
280
- const blockedCount = preflightNormalized.blocked || 0;
281
- if (!preflightNormalized.success || blockedCount > 0) {
282
- printEnvelope({
283
- success: false,
284
- code: 'PREFLIGHT_BLOCKED',
285
- message: 'Preflight blocked execution',
286
- data: preflightNormalized,
287
- meta: {
288
- apiBase: ctx.apiBase,
289
- catalog: flags.catalog || null,
290
- requestedAction: resolved.requestedAction,
291
- resolvedAction: resolved.resolvedAction,
292
- skippedPreflight: false,
293
- },
294
- });
295
- process.exit(1);
296
- return;
297
- }
298
- }
299
-
300
- const payload = buildIntentPayload(flags, {
301
- validationOnly: false,
302
- forceIdempotency: true,
303
- resolvedAction: resolved.resolvedAction,
333
+ const backendResult = await runExecutionByMode(runMode, ctx, flags, resolved, {
334
+ skipPreflight,
304
335
  });
305
- const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
306
- const normalized = normalizeIntentResponse(response);
336
+ const normalized = backendResult.normalized;
307
337
  printEnvelope({
308
338
  success: normalized.success,
309
- code: normalized.success ? 'RUN_QUEUED' : 'RUN_FAILED',
310
- message: normalized.success ? 'Execution intent queued' : 'Execution failed',
339
+ code: backendResult.code,
340
+ message: backendResult.message,
311
341
  data: normalized,
312
342
  meta: {
313
343
  apiBase: ctx.apiBase,
314
344
  requestedAction: resolved.requestedAction,
315
345
  resolvedAction: resolved.resolvedAction,
316
- idempotencyKey: payload.idempotencyKey || null,
346
+ idempotencyKey: backendResult.idempotencyKey || null,
347
+ runMode,
348
+ profile: ctx.profile || null,
317
349
  skippedPreflight: skipPreflight,
318
350
  capabilitiesVersion: capabilities?.contractVersion || null,
319
351
  },
@@ -483,7 +515,10 @@ async function commandCredentialsStatus(ctx, flags, capabilities) {
483
515
  }
484
516
 
485
517
  async function commandAgentRegister(ctx, flags, capabilities) {
486
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for agent register');
518
+ const ownerHeaders = buildOwnerAuthHeaders(
519
+ flags,
520
+ 'Missing owner auth for agent register: provide --auth-token (THIRDFY_AUTH_TOKEN) or --owner-session-token'
521
+ );
487
522
  const payload = {
488
523
  agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
489
524
  name: requireFlag(flags, 'name', 'Missing --name'),
@@ -501,9 +536,10 @@ async function commandAgentRegister(ctx, flags, capabilities) {
501
536
  if (flags.strategy) payload.strategy = parseJsonFlag(flags, 'strategy');
502
537
  if (flags.apiKeyEnvelopePublicKey) payload.apiKeyEnvelopePublicKey = String(flags.apiKeyEnvelopePublicKey);
503
538
 
504
- const response = await apiPost(ctx, '/api/v1/agent/onboarding/register-with-privy', payload, {
505
- Authorization: `Bearer ${authToken}`,
506
- });
539
+ const route = ownerHeaders['x-owner-session-token']
540
+ ? '/api/v1/agent/onboarding/register-with-wallet'
541
+ : '/api/v1/agent/onboarding/register-with-privy';
542
+ const response = await apiPost(ctx, route, payload, ownerHeaders);
507
543
  printEnvelope({
508
544
  success: true,
509
545
  code: 'AGENT_REGISTERED',
@@ -517,12 +553,13 @@ async function commandAgentRegister(ctx, flags, capabilities) {
517
553
  }
518
554
 
519
555
  async function commandAgentKeyRotate(ctx, flags, capabilities) {
520
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for agent key rotate');
556
+ const ownerHeaders = buildOwnerAuthHeaders(
557
+ flags,
558
+ 'Missing owner auth for agent key rotate: provide --auth-token (THIRDFY_AUTH_TOKEN) or --owner-session-token'
559
+ );
521
560
  const payload = {};
522
561
  if (flags.agentKey) payload.agentKey = String(flags.agentKey).trim();
523
- const response = await apiPost(ctx, '/api/v1/agent/onboarding/me/api-key/rotate', payload, {
524
- Authorization: `Bearer ${authToken}`,
525
- });
562
+ const response = await apiPost(ctx, '/api/v1/agent/onboarding/me/api-key/rotate', payload, ownerHeaders);
526
563
  printEnvelope({
527
564
  success: true,
528
565
  code: 'AGENT_KEY_ROTATED',
@@ -536,12 +573,13 @@ async function commandAgentKeyRotate(ctx, flags, capabilities) {
536
573
  }
537
574
 
538
575
  async function commandAgentKeyRevoke(ctx, flags, capabilities) {
539
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for agent key revoke');
576
+ const ownerHeaders = buildOwnerAuthHeaders(
577
+ flags,
578
+ 'Missing owner auth for agent key revoke: provide --auth-token (THIRDFY_AUTH_TOKEN) or --owner-session-token'
579
+ );
540
580
  const payload = {};
541
581
  if (flags.agentKey) payload.agentKey = String(flags.agentKey).trim();
542
- const response = await apiPost(ctx, '/api/v1/agent/onboarding/me/api-key/revoke', payload, {
543
- Authorization: `Bearer ${authToken}`,
544
- });
582
+ const response = await apiPost(ctx, '/api/v1/agent/onboarding/me/api-key/revoke', payload, ownerHeaders);
545
583
  printEnvelope({
546
584
  success: true,
547
585
  code: 'AGENT_KEY_REVOKED',
@@ -554,6 +592,407 @@ async function commandAgentKeyRevoke(ctx, flags, capabilities) {
554
592
  });
555
593
  }
556
594
 
595
+ async function commandAgentAuthChallenge(ctx, flags, capabilities) {
596
+ const payload = {
597
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
598
+ chainId: Number(flags.chainId || 8453),
599
+ };
600
+ if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
601
+
602
+ const response = await apiPost(ctx, '/api/v1/agent/onboarding/wallet/challenge', payload);
603
+ printEnvelope({
604
+ success: true,
605
+ code: 'OWNER_CHALLENGE_CREATED',
606
+ message: 'Wallet challenge created',
607
+ data: response,
608
+ meta: {
609
+ apiBase: ctx.apiBase,
610
+ capabilitiesVersion: capabilities?.contractVersion || null,
611
+ },
612
+ });
613
+ }
614
+
615
+ async function commandAgentAuthVerify(ctx, flags, capabilities) {
616
+ const payload = {
617
+ challengeId: requireFlag(flags, 'challengeId', 'Missing --challenge-id'),
618
+ signature: requireFlag(flags, 'signature', 'Missing --signature'),
619
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
620
+ };
621
+ if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
622
+
623
+ const response = await apiPost(ctx, '/api/v1/agent/onboarding/wallet/verify', payload);
624
+ printEnvelope({
625
+ success: true,
626
+ code: 'OWNER_VERIFIED',
627
+ message: 'Wallet challenge verified',
628
+ data: response,
629
+ meta: {
630
+ apiBase: ctx.apiBase,
631
+ capabilitiesVersion: capabilities?.contractVersion || null,
632
+ },
633
+ });
634
+ }
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
+
557
996
  function buildIntentPayload(flags, options) {
558
997
  const payload = {
559
998
  agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
@@ -565,6 +1004,12 @@ function buildIntentPayload(flags, options) {
565
1004
  if (flags.estimatedAmountUsd) payload.estimatedAmountUsd = Number(flags.estimatedAmountUsd);
566
1005
  if (flags.userDid) payload.userDid = String(flags.userDid);
567
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
+ }
568
1013
  if (options.forceIdempotency) {
569
1014
  payload.idempotencyKey = String(flags.idempotencyKey || `cli:${payload.action}:${Date.now()}:${randomUUID()}`);
570
1015
  } else if (flags.idempotencyKey) {
@@ -687,6 +1132,18 @@ function getAuthToken(flags, missingMessage) {
687
1132
  return authToken;
688
1133
  }
689
1134
 
1135
+ function buildOwnerAuthHeaders(flags, missingMessage) {
1136
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
1137
+ const ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
1138
+ if (!authToken && !ownerSessionToken) {
1139
+ throw new Error(missingMessage);
1140
+ }
1141
+ if (authToken) {
1142
+ return { Authorization: `Bearer ${authToken}` };
1143
+ }
1144
+ return { 'x-owner-session-token': ownerSessionToken };
1145
+ }
1146
+
690
1147
  function extractActions(response) {
691
1148
  if (Array.isArray(response)) return response;
692
1149
  if (Array.isArray(response.actions)) return response.actions;
@@ -787,24 +1244,37 @@ function printHelp() {
787
1244
  thirdfy-agent ${CLI_VERSION}
788
1245
 
789
1246
  Core commands:
1247
+ thirdfy-agent profile init [--profile personal|builder|network] [--run-mode thirdfy|self|hybrid] [--json]
1248
+ thirdfy-agent profile use --profile <name> [--run-mode thirdfy|self|hybrid] [--json]
1249
+ thirdfy-agent profile show [--json]
1250
+ thirdfy-agent whoami [--json]
790
1251
  thirdfy-agent catalogs list [--json]
791
- thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--json]
1252
+ thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--chain-id <id>] [--run-mode <mode>] [--json]
792
1253
  thirdfy-agent delegation upsert --user-did <did> --agent-key <key> --wallet-address <addr> [--json]
793
1254
  thirdfy-agent delegation status --auth-token <token> --agent-key <key> [--verify] [--json]
794
1255
  thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
795
1256
  thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
796
- thirdfy-agent agent register --auth-token <token> --agent-key <key> --name <name> [--json]
797
- thirdfy-agent agent key rotate --auth-token <token> [--agent-key <key>] [--json]
798
- thirdfy-agent agent key revoke --auth-token <token> [--agent-key <key>] [--json]
799
- thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--json]
800
- thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--idempotency-key <key>] [--json]
1257
+ thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
1258
+ thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
1259
+ thirdfy-agent agent register --agent-key <key> --name <name> [--auth-token <token> | --owner-session-token <token>] [--json]
1260
+ thirdfy-agent agent key rotate [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
1261
+ thirdfy-agent agent key revoke [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
1262
+ thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1263
+ thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--idempotency-key <key>] [--json]
1264
+ thirdfy-agent jeff preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1265
+ thirdfy-agent jeff trade --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1266
+ thirdfy-agent jeff status --intent-id <id> [--json]
1267
+ thirdfy-agent prompt "<natural language prompt>" [--json]
801
1268
  thirdfy-agent intent-status --intent-id <id> [--json]
802
1269
  thirdfy-agent credits balance --auth-token <token> [--json]
803
1270
 
804
1271
  Global flags:
805
1272
  --api-base <url> default: https://api.thirdfy.com
1273
+ --run-mode <mode> thirdfy | self | hybrid
1274
+ --profile <name> personal | builder | network
806
1275
  --env <file> load env vars from file
807
1276
  --timeout <ms> request timeout
1277
+ --owner-session-token <token> owner session token (or THIRDFY_OWNER_SESSION_TOKEN)
808
1278
  --json stable machine-readable output
809
1279
  --verbose print adapter diagnostics
810
1280
  --version print version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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