@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 +39 -1
- package/bin/thirdfy-agent.mjs +470 -56
- package/package.json +1 -1
- package/src/utils/cli/intentNormalization.cjs +6 -0
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`
|
package/bin/thirdfy-agent.mjs
CHANGED
|
@@ -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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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:
|
|
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
|
-
|
|
281
|
-
|
|
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
|
|
314
|
-
const normalized = normalizeIntentResponse(response);
|
|
336
|
+
const normalized = backendResult.normalized;
|
|
315
337
|
printEnvelope({
|
|
316
338
|
success: normalized.success,
|
|
317
|
-
code:
|
|
318
|
-
message:
|
|
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:
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
);
|
|
710
|
-
if (
|
|
711
|
-
return { requestedAction, resolvedAction:
|
|
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 (
|
|
1115
|
+
if (exactAlias.length > 1) {
|
|
714
1116
|
throw new Error(
|
|
715
|
-
`Ambiguous --action "${requestedAction}".
|
|
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
|
@@ -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
|
|