@thirdfy/agent-cli 0.1.0
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/LICENSE +15 -0
- package/README.md +79 -0
- package/bin/thirdfy-agent.mjs +825 -0
- package/package.json +43 -0
- package/scripts/validate-cli-contract-schemas.mjs +49 -0
- package/src/contracts/cli/schemas/actions.response.schema.json +50 -0
- package/src/contracts/cli/schemas/execute-intent.response.schema.json +54 -0
- package/src/contracts/cli/schemas/intent-status.response.schema.json +36 -0
- package/src/contracts/cli/schemas/preflight.response.schema.json +60 -0
- package/src/utils/cli/delegationNormalization.cjs +40 -0
- package/src/utils/cli/intentNormalization.cjs +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thirdfy
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @thirdfy/agent-cli
|
|
2
|
+
|
|
3
|
+
`@thirdfy/agent-cli` is the standalone command-line product for Thirdfy agent operations.
|
|
4
|
+
Use it to:
|
|
5
|
+
|
|
6
|
+
- onboard and manage agent keys
|
|
7
|
+
- discover allowed actions (including policy-aware discovery)
|
|
8
|
+
- run governance preflight checks
|
|
9
|
+
- queue execute-intent requests with idempotency
|
|
10
|
+
- poll intent status with stable JSON output for automation
|
|
11
|
+
|
|
12
|
+
If you want the full developer docs, start here:
|
|
13
|
+
- [Thirdfy Agent CLI docs](https://docs.thirdfy.com/api/thirdfy-agent-cli)
|
|
14
|
+
- [API Reference home](https://docs.thirdfy.com/api)
|
|
15
|
+
- [LLM docs map (llms.txt)](https://docs.thirdfy.com/llms.txt)
|
|
16
|
+
- [LLM full API snapshot (llms-full.txt)](https://docs.thirdfy.com/llms-full.txt)
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @thirdfy/agent-cli
|
|
22
|
+
thirdfy-agent --version --json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Run without global install:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @thirdfy/agent-cli --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export THIRDFY_API_BASE="https://api.thirdfy.com"
|
|
35
|
+
export AGENT_API_KEY="..."
|
|
36
|
+
export THIRDFY_AUTH_TOKEN="..."
|
|
37
|
+
|
|
38
|
+
thirdfy-agent actions --api-base "$THIRDFY_API_BASE" --agent-api-key "$AGENT_API_KEY" --json
|
|
39
|
+
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
|
|
41
|
+
thirdfy-agent intent-status --api-base "$THIRDFY_API_BASE" --intent-id "<intentId>" --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Auth model
|
|
45
|
+
|
|
46
|
+
- `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`)
|
|
48
|
+
|
|
49
|
+
Execution-only workflows can run with only `AGENT_API_KEY` after onboarding is complete.
|
|
50
|
+
|
|
51
|
+
## Command groups
|
|
52
|
+
|
|
53
|
+
- Discovery: `catalogs list`, `actions`
|
|
54
|
+
- Execution: `preflight`, `run`, `intent-status`
|
|
55
|
+
- Onboarding: `agent register`, `agent key rotate`, `agent key revoke`
|
|
56
|
+
- Governance readiness: `delegation upsert`, `delegation status`, `credentials upsert`, `credentials status`
|
|
57
|
+
- Account: `credits balance`
|
|
58
|
+
|
|
59
|
+
## More docs
|
|
60
|
+
|
|
61
|
+
- OpenClaw CLI onboarding flow:
|
|
62
|
+
[docs.thirdfy.com/api/thirdfy-agent-cli#create-an-openclaw-agent-with-the-cli](https://docs.thirdfy.com/api/thirdfy-agent-cli#create-an-openclaw-agent-with-the-cli)
|
|
63
|
+
- Creator platform (agent registration and keys):
|
|
64
|
+
[thirdfy.com/creator](https://thirdfy.com/creator)
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm run validate:cli-contract-schemas
|
|
70
|
+
npm test
|
|
71
|
+
npm run smoke:cli
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Release
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm version patch
|
|
78
|
+
npm publish --access public
|
|
79
|
+
```
|
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
import { pathToFileURL } from 'url';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { normalizeIntentResponse } = require('../src/utils/cli/intentNormalization.cjs');
|
|
11
|
+
const {
|
|
12
|
+
normalizeDelegationStatus,
|
|
13
|
+
normalizeCredentialStatus,
|
|
14
|
+
} = require('../src/utils/cli/delegationNormalization.cjs');
|
|
15
|
+
|
|
16
|
+
const CLI_VERSION = '0.1.0';
|
|
17
|
+
const DEFAULT_API_BASE = 'https://api.thirdfy.com';
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
19
|
+
const DEFAULT_CATALOG_CACHE_TTL_MS = 60_000;
|
|
20
|
+
const catalogCache = new Map();
|
|
21
|
+
|
|
22
|
+
const rawArgv = process.argv.slice(2);
|
|
23
|
+
const parsed = parseArgs(rawArgv);
|
|
24
|
+
const globalFlags = parsed.flags;
|
|
25
|
+
const jsonMode = Boolean(globalFlags.json);
|
|
26
|
+
|
|
27
|
+
if (globalFlags.env) {
|
|
28
|
+
loadEnvFile(globalFlags.env);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const apiBase = String(globalFlags.apiBase || process.env.THIRDFY_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
32
|
+
const timeoutMs = Number(globalFlags.timeout || process.env.THIRDFY_CLI_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
|
|
33
|
+
const verbose = Boolean(globalFlags.verbose);
|
|
34
|
+
|
|
35
|
+
const context = {
|
|
36
|
+
apiBase,
|
|
37
|
+
timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : DEFAULT_TIMEOUT_MS,
|
|
38
|
+
jsonMode,
|
|
39
|
+
verbose,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const isEntrypoint = (() => {
|
|
43
|
+
try {
|
|
44
|
+
if (!process.argv[1]) return false;
|
|
45
|
+
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
if (isEntrypoint) {
|
|
52
|
+
main().catch((error) => {
|
|
53
|
+
const statusCode = Number(error?.statusCode || 0) || undefined;
|
|
54
|
+
const payload = error?.payload && typeof error.payload === 'object' ? error.payload : {};
|
|
55
|
+
const blockedReason = String(payload.blockedReason || payload.error || '').trim() || undefined;
|
|
56
|
+
const blockedStage = String(payload.blockedStage || '').trim() || undefined;
|
|
57
|
+
printEnvelope({
|
|
58
|
+
success: false,
|
|
59
|
+
code: statusCode ? 'API_REQUEST_FAILED' : 'CLI_UNHANDLED_ERROR',
|
|
60
|
+
message: error?.message || 'Unhandled CLI error',
|
|
61
|
+
data: {
|
|
62
|
+
statusCode,
|
|
63
|
+
blockedReason,
|
|
64
|
+
blockedStage,
|
|
65
|
+
payload,
|
|
66
|
+
},
|
|
67
|
+
meta: { version: CLI_VERSION, apiBase: context.apiBase },
|
|
68
|
+
});
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main() {
|
|
74
|
+
if (globalFlags.version) {
|
|
75
|
+
printEnvelope({
|
|
76
|
+
success: true,
|
|
77
|
+
code: 'OK',
|
|
78
|
+
message: 'thirdfy-agent version',
|
|
79
|
+
data: { version: CLI_VERSION },
|
|
80
|
+
meta: { apiBase: context.apiBase },
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (globalFlags.help || parsed.positionals.length === 0) {
|
|
86
|
+
printHelp();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const capabilities = await negotiateCapabilities(context);
|
|
91
|
+
const commandKey = parsed.positionals.slice(0, 2).join(' ');
|
|
92
|
+
const commandKey3 = parsed.positionals.slice(0, 3).join(' ');
|
|
93
|
+
const subFlags = parsed.flags;
|
|
94
|
+
|
|
95
|
+
switch (commandKey) {
|
|
96
|
+
case 'catalogs list':
|
|
97
|
+
await commandCatalogsList(context, subFlags, capabilities);
|
|
98
|
+
return;
|
|
99
|
+
case 'credits balance':
|
|
100
|
+
await commandCreditsBalance(context, subFlags, capabilities);
|
|
101
|
+
return;
|
|
102
|
+
case 'delegation upsert':
|
|
103
|
+
await commandDelegationUpsert(context, subFlags, capabilities);
|
|
104
|
+
return;
|
|
105
|
+
case 'delegation status':
|
|
106
|
+
await commandDelegationStatus(context, subFlags, capabilities);
|
|
107
|
+
return;
|
|
108
|
+
case 'credentials upsert':
|
|
109
|
+
await commandCredentialsUpsert(context, subFlags, capabilities);
|
|
110
|
+
return;
|
|
111
|
+
case 'credentials status':
|
|
112
|
+
await commandCredentialsStatus(context, subFlags, capabilities);
|
|
113
|
+
return;
|
|
114
|
+
case 'agent register':
|
|
115
|
+
await commandAgentRegister(context, subFlags, capabilities);
|
|
116
|
+
return;
|
|
117
|
+
default:
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (commandKey3 === 'agent key rotate') {
|
|
121
|
+
await commandAgentKeyRotate(context, subFlags, capabilities);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (commandKey3 === 'agent key revoke') {
|
|
125
|
+
await commandAgentKeyRevoke(context, subFlags, capabilities);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rootCommand = parsed.positionals[0];
|
|
130
|
+
switch (rootCommand) {
|
|
131
|
+
case 'actions':
|
|
132
|
+
await commandActions(context, subFlags, capabilities);
|
|
133
|
+
return;
|
|
134
|
+
case 'preflight':
|
|
135
|
+
await commandPreflight(context, subFlags, capabilities);
|
|
136
|
+
return;
|
|
137
|
+
case 'run':
|
|
138
|
+
await commandRun(context, subFlags, capabilities);
|
|
139
|
+
return;
|
|
140
|
+
case 'intent-status':
|
|
141
|
+
await commandIntentStatus(context, subFlags, capabilities);
|
|
142
|
+
return;
|
|
143
|
+
default:
|
|
144
|
+
printEnvelope({
|
|
145
|
+
success: false,
|
|
146
|
+
code: 'UNKNOWN_COMMAND',
|
|
147
|
+
message: `Unknown command: ${parsed.positionals.join(' ')}`,
|
|
148
|
+
data: {},
|
|
149
|
+
meta: { version: CLI_VERSION },
|
|
150
|
+
});
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseArgs(argv) {
|
|
156
|
+
const flags = {};
|
|
157
|
+
const positionals = [];
|
|
158
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
159
|
+
const token = argv[i];
|
|
160
|
+
if (!token.startsWith('--')) {
|
|
161
|
+
positionals.push(token);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const key = toCamel(token.slice(2));
|
|
165
|
+
const next = argv[i + 1];
|
|
166
|
+
if (!next || next.startsWith('--')) {
|
|
167
|
+
flags[key] = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
flags[key] = next;
|
|
171
|
+
i += 1;
|
|
172
|
+
}
|
|
173
|
+
return { flags, positionals };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function toCamel(input) {
|
|
177
|
+
return String(input || '')
|
|
178
|
+
.trim()
|
|
179
|
+
.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function loadEnvFile(filePath) {
|
|
183
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
184
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
185
|
+
for (const line of content.split('\n')) {
|
|
186
|
+
const trimmed = line.trim();
|
|
187
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
188
|
+
const idx = trimmed.indexOf('=');
|
|
189
|
+
if (idx <= 0) continue;
|
|
190
|
+
const key = trimmed.slice(0, idx).trim();
|
|
191
|
+
const value = trimmed.slice(idx + 1).trim().replace(/^"|"$/g, '');
|
|
192
|
+
if (!(key in process.env)) {
|
|
193
|
+
process.env[key] = value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function requireFlag(flags, key, message) {
|
|
199
|
+
if (!flags[key]) {
|
|
200
|
+
throw new Error(message);
|
|
201
|
+
}
|
|
202
|
+
return String(flags[key]).trim();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseJsonFlag(flags, key) {
|
|
206
|
+
const raw = requireFlag(flags, key, `Missing --${key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`);
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(raw);
|
|
209
|
+
} catch {
|
|
210
|
+
throw new Error(`Invalid JSON for --${key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function commandCatalogsList(ctx, flags, capabilities) {
|
|
215
|
+
const response = await apiGet(ctx, '/api/v1/agent/actions/catalog');
|
|
216
|
+
const actions = extractActions(response);
|
|
217
|
+
const catalogs = [];
|
|
218
|
+
const seen = new Set();
|
|
219
|
+
for (const action of actions) {
|
|
220
|
+
const catalog = String(action.catalog || action.catalogId || action.provider || 'default').trim();
|
|
221
|
+
if (!catalog || seen.has(catalog)) continue;
|
|
222
|
+
seen.add(catalog);
|
|
223
|
+
catalogs.push({
|
|
224
|
+
id: catalog,
|
|
225
|
+
provider: String(action.provider || '').trim() || undefined,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
printEnvelope({
|
|
229
|
+
success: true,
|
|
230
|
+
code: 'OK',
|
|
231
|
+
message: 'Catalogs loaded',
|
|
232
|
+
data: { catalogs },
|
|
233
|
+
meta: {
|
|
234
|
+
apiBase: ctx.apiBase,
|
|
235
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function commandActions(ctx, flags, capabilities) {
|
|
241
|
+
const policyAware = Boolean(flags.agentApiKey);
|
|
242
|
+
const actions = applyActionFilters(await getCachedActionsCatalog(ctx, flags), flags);
|
|
243
|
+
printEnvelope({
|
|
244
|
+
success: true,
|
|
245
|
+
code: 'OK',
|
|
246
|
+
message: 'Actions loaded',
|
|
247
|
+
data: { actions },
|
|
248
|
+
meta: {
|
|
249
|
+
apiBase: ctx.apiBase,
|
|
250
|
+
policyAware,
|
|
251
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function commandPreflight(ctx, flags, capabilities) {
|
|
257
|
+
const resolved = await resolveActionSelection(ctx, flags);
|
|
258
|
+
const payload = buildIntentPayload(flags, {
|
|
259
|
+
validationOnly: true,
|
|
260
|
+
forceIdempotency: false,
|
|
261
|
+
resolvedAction: resolved.resolvedAction,
|
|
262
|
+
});
|
|
263
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
264
|
+
const normalized = normalizeIntentResponse(response);
|
|
265
|
+
printEnvelope({
|
|
266
|
+
success: normalized.success,
|
|
267
|
+
code: normalized.success ? 'PREFLIGHT_OK' : 'PREFLIGHT_FAILED',
|
|
268
|
+
message: normalized.success ? 'Preflight completed' : 'Preflight failed',
|
|
269
|
+
data: normalized,
|
|
270
|
+
meta: {
|
|
271
|
+
apiBase: ctx.apiBase,
|
|
272
|
+
catalog: flags.catalog || null,
|
|
273
|
+
requestedAction: resolved.requestedAction,
|
|
274
|
+
resolvedAction: resolved.resolvedAction,
|
|
275
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
if (!normalized.success) process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function commandRun(ctx, flags, capabilities) {
|
|
282
|
+
const resolved = await resolveActionSelection(ctx, flags);
|
|
283
|
+
const skipPreflight = Boolean(flags.skipPreflight);
|
|
284
|
+
if (!skipPreflight) {
|
|
285
|
+
const preflightPayload = buildIntentPayload(flags, {
|
|
286
|
+
validationOnly: true,
|
|
287
|
+
forceIdempotency: false,
|
|
288
|
+
resolvedAction: resolved.resolvedAction,
|
|
289
|
+
});
|
|
290
|
+
const preflight = await apiPost(ctx, '/api/v1/agent/execute-intent', preflightPayload);
|
|
291
|
+
const preflightNormalized = normalizeIntentResponse(preflight);
|
|
292
|
+
const blockedCount = preflightNormalized.blocked || 0;
|
|
293
|
+
if (!preflightNormalized.success || blockedCount > 0) {
|
|
294
|
+
printEnvelope({
|
|
295
|
+
success: false,
|
|
296
|
+
code: 'PREFLIGHT_BLOCKED',
|
|
297
|
+
message: 'Preflight blocked execution',
|
|
298
|
+
data: preflightNormalized,
|
|
299
|
+
meta: {
|
|
300
|
+
apiBase: ctx.apiBase,
|
|
301
|
+
catalog: flags.catalog || null,
|
|
302
|
+
requestedAction: resolved.requestedAction,
|
|
303
|
+
resolvedAction: resolved.resolvedAction,
|
|
304
|
+
skippedPreflight: false,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
process.exit(1);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const payload = buildIntentPayload(flags, {
|
|
313
|
+
validationOnly: false,
|
|
314
|
+
forceIdempotency: true,
|
|
315
|
+
resolvedAction: resolved.resolvedAction,
|
|
316
|
+
});
|
|
317
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
318
|
+
const normalized = normalizeIntentResponse(response);
|
|
319
|
+
printEnvelope({
|
|
320
|
+
success: normalized.success,
|
|
321
|
+
code: normalized.success ? 'RUN_QUEUED' : 'RUN_FAILED',
|
|
322
|
+
message: normalized.success ? 'Execution intent queued' : 'Execution failed',
|
|
323
|
+
data: normalized,
|
|
324
|
+
meta: {
|
|
325
|
+
apiBase: ctx.apiBase,
|
|
326
|
+
requestedAction: resolved.requestedAction,
|
|
327
|
+
resolvedAction: resolved.resolvedAction,
|
|
328
|
+
idempotencyKey: payload.idempotencyKey || null,
|
|
329
|
+
skippedPreflight: skipPreflight,
|
|
330
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
if (!normalized.success) process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function commandIntentStatus(ctx, flags, capabilities) {
|
|
337
|
+
const intentId = requireFlag(flags, 'intentId', 'Missing --intent-id');
|
|
338
|
+
const response = await apiGet(ctx, `/api/v1/agent/execute-intent/status?intentId=${encodeURIComponent(intentId)}`);
|
|
339
|
+
const normalized = normalizeIntentResponse(response);
|
|
340
|
+
printEnvelope({
|
|
341
|
+
success: normalized.success,
|
|
342
|
+
code: normalized.success ? 'OK' : 'INTENT_STATUS_FAILED',
|
|
343
|
+
message: normalized.success ? 'Intent status loaded' : 'Intent status failed',
|
|
344
|
+
data: normalized,
|
|
345
|
+
meta: {
|
|
346
|
+
apiBase: ctx.apiBase,
|
|
347
|
+
intentId,
|
|
348
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
if (!normalized.success) process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function commandCreditsBalance(ctx, flags, capabilities) {
|
|
355
|
+
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credits balance');
|
|
356
|
+
const response = await apiGet(ctx, '/api/v1/credits/balance', {
|
|
357
|
+
Authorization: `Bearer ${authToken}`,
|
|
358
|
+
});
|
|
359
|
+
printEnvelope({
|
|
360
|
+
success: true,
|
|
361
|
+
code: 'OK',
|
|
362
|
+
message: 'Credits balance loaded',
|
|
363
|
+
data: response,
|
|
364
|
+
meta: {
|
|
365
|
+
apiBase: ctx.apiBase,
|
|
366
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function commandDelegationUpsert(ctx, flags, capabilities) {
|
|
372
|
+
const payload = {
|
|
373
|
+
userDid: requireFlag(flags, 'userDid', 'Missing --user-did'),
|
|
374
|
+
agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
|
|
375
|
+
walletAddress: requireFlag(flags, 'walletAddress', 'Missing --wallet-address'),
|
|
376
|
+
chainId: Number(flags.chainId || 8453),
|
|
377
|
+
executionMode: String(flags.executionMode || 'delegated_execution_user'),
|
|
378
|
+
};
|
|
379
|
+
if (flags.sessionAccountAddress) payload.sessionAccountAddress = String(flags.sessionAccountAddress).trim();
|
|
380
|
+
if (flags.sessionAccountType) payload.sessionAccountType = String(flags.sessionAccountType).trim();
|
|
381
|
+
if (flags.permissionsContext) payload.permissionsContext = String(flags.permissionsContext).trim();
|
|
382
|
+
if (flags.delegationManager) payload.delegationManager = String(flags.delegationManager).trim();
|
|
383
|
+
if (flags.permissionsExpiry) payload.permissionsExpiry = String(flags.permissionsExpiry).trim();
|
|
384
|
+
if (flags.strategyManifestVersion) payload.strategyManifestVersion = String(flags.strategyManifestVersion).trim();
|
|
385
|
+
if (flags.strategyManifestHash) payload.strategyManifestHash = String(flags.strategyManifestHash).trim();
|
|
386
|
+
if (flags.modelAConstraints) payload.modelAConstraints = parseJsonFlag(flags, 'modelAConstraints');
|
|
387
|
+
|
|
388
|
+
const response = await requestWithRouteFallback(ctx, {
|
|
389
|
+
method: 'POST',
|
|
390
|
+
routes: ['/api/v1/agent/delegation/upsert', '/api/v1/agent/agent/delegation/upsert'],
|
|
391
|
+
body: payload,
|
|
392
|
+
});
|
|
393
|
+
printEnvelope({
|
|
394
|
+
success: Boolean(response?.success !== false),
|
|
395
|
+
code: response?.updated ? 'DELEGATION_UPDATED' : 'DELEGATION_UPSERTED',
|
|
396
|
+
message: response?.message || 'Delegation upsert completed',
|
|
397
|
+
data: response,
|
|
398
|
+
meta: {
|
|
399
|
+
apiBase: ctx.apiBase,
|
|
400
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function commandDelegationStatus(ctx, flags, capabilities) {
|
|
406
|
+
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for delegation status');
|
|
407
|
+
const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
|
|
408
|
+
const chainId = Number(flags.chainId || 8453);
|
|
409
|
+
const verify = Boolean(flags.verify);
|
|
410
|
+
const route = verify
|
|
411
|
+
? `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`
|
|
412
|
+
: `/api/v1/agent/delegation/check?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
|
|
413
|
+
const response = await apiGet(ctx, route, {
|
|
414
|
+
Authorization: `Bearer ${authToken}`,
|
|
415
|
+
});
|
|
416
|
+
const normalized = normalizeDelegationStatus(response);
|
|
417
|
+
printEnvelope({
|
|
418
|
+
success: true,
|
|
419
|
+
code: 'DELEGATION_STATUS_OK',
|
|
420
|
+
message: 'Delegation status loaded',
|
|
421
|
+
data: normalized,
|
|
422
|
+
meta: {
|
|
423
|
+
apiBase: ctx.apiBase,
|
|
424
|
+
verify,
|
|
425
|
+
governanceState: normalized.state || null,
|
|
426
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function commandCredentialsUpsert(ctx, flags, capabilities) {
|
|
432
|
+
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials upsert');
|
|
433
|
+
const payload = {
|
|
434
|
+
venue: requireFlag(flags, 'venue', 'Missing --venue'),
|
|
435
|
+
apiKey: requireFlag(flags, 'apiKey', 'Missing --api-key'),
|
|
436
|
+
apiSecret: requireFlag(flags, 'apiSecret', 'Missing --api-secret'),
|
|
437
|
+
credentialType: String(flags.credentialType || 'api_key'),
|
|
438
|
+
credentialRef: String(flags.credentialRef || ''),
|
|
439
|
+
};
|
|
440
|
+
if (flags.passphrase) payload.passphrase = String(flags.passphrase);
|
|
441
|
+
if (flags.metadata) payload.metadata = parseJsonFlag(flags, 'metadata');
|
|
442
|
+
if (flags.policy) payload.policy = parseJsonFlag(flags, 'policy');
|
|
443
|
+
if (flags.agentKey) payload.agentKey = String(flags.agentKey).trim();
|
|
444
|
+
if (flags.chainId) payload.chainId = Number(flags.chainId);
|
|
445
|
+
|
|
446
|
+
const response = await requestWithRouteFallback(ctx, {
|
|
447
|
+
method: 'POST',
|
|
448
|
+
routes: ['/api/v1/agent/delegation/credentials/upsert', '/api/v1/agent/agent/delegation/credentials/upsert'],
|
|
449
|
+
body: payload,
|
|
450
|
+
headers: {
|
|
451
|
+
Authorization: `Bearer ${authToken}`,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
printEnvelope({
|
|
455
|
+
success: Boolean(response?.success !== false),
|
|
456
|
+
code: 'CREDENTIALS_UPSERTED',
|
|
457
|
+
message: 'Delegation credential upsert completed',
|
|
458
|
+
data: response,
|
|
459
|
+
meta: {
|
|
460
|
+
apiBase: ctx.apiBase,
|
|
461
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function commandCredentialsStatus(ctx, flags, capabilities) {
|
|
467
|
+
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials status');
|
|
468
|
+
const query = new URLSearchParams();
|
|
469
|
+
if (flags.venue) query.set('venue', String(flags.venue));
|
|
470
|
+
if (flags.credentialType) query.set('credentialType', String(flags.credentialType));
|
|
471
|
+
if (flags.credentialRef) query.set('credentialRef', String(flags.credentialRef));
|
|
472
|
+
const suffix = query.toString() ? `?${query.toString()}` : '';
|
|
473
|
+
const response = await requestWithRouteFallback(ctx, {
|
|
474
|
+
method: 'GET',
|
|
475
|
+
routes: [
|
|
476
|
+
`/api/v1/agent/delegation/credentials/status${suffix}`,
|
|
477
|
+
`/api/v1/agent/agent/delegation/credentials/status${suffix}`,
|
|
478
|
+
],
|
|
479
|
+
headers: {
|
|
480
|
+
Authorization: `Bearer ${authToken}`,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
const normalized = normalizeCredentialStatus(response);
|
|
484
|
+
printEnvelope({
|
|
485
|
+
success: true,
|
|
486
|
+
code: 'CREDENTIALS_STATUS_OK',
|
|
487
|
+
message: 'Delegation credential status loaded',
|
|
488
|
+
data: normalized,
|
|
489
|
+
meta: {
|
|
490
|
+
apiBase: ctx.apiBase,
|
|
491
|
+
governanceState: normalized.state || null,
|
|
492
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function commandAgentRegister(ctx, flags, capabilities) {
|
|
498
|
+
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for agent register');
|
|
499
|
+
const payload = {
|
|
500
|
+
agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
|
|
501
|
+
name: requireFlag(flags, 'name', 'Missing --name'),
|
|
502
|
+
};
|
|
503
|
+
if (flags.email) payload.email = String(flags.email);
|
|
504
|
+
if (flags.bio) payload.bio = String(flags.bio);
|
|
505
|
+
if (flags.website) payload.website = String(flags.website);
|
|
506
|
+
if (flags.github) payload.github = String(flags.github);
|
|
507
|
+
if (flags.tags) {
|
|
508
|
+
payload.tags = String(flags.tags)
|
|
509
|
+
.split(',')
|
|
510
|
+
.map((s) => s.trim())
|
|
511
|
+
.filter(Boolean);
|
|
512
|
+
}
|
|
513
|
+
if (flags.strategy) payload.strategy = parseJsonFlag(flags, 'strategy');
|
|
514
|
+
if (flags.apiKeyEnvelopePublicKey) payload.apiKeyEnvelopePublicKey = String(flags.apiKeyEnvelopePublicKey);
|
|
515
|
+
|
|
516
|
+
const response = await apiPost(ctx, '/api/v1/agent/onboarding/register-with-privy', payload, {
|
|
517
|
+
Authorization: `Bearer ${authToken}`,
|
|
518
|
+
});
|
|
519
|
+
printEnvelope({
|
|
520
|
+
success: true,
|
|
521
|
+
code: 'AGENT_REGISTERED',
|
|
522
|
+
message: 'Agent registered',
|
|
523
|
+
data: response,
|
|
524
|
+
meta: {
|
|
525
|
+
apiBase: ctx.apiBase,
|
|
526
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function commandAgentKeyRotate(ctx, flags, capabilities) {
|
|
532
|
+
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for agent key rotate');
|
|
533
|
+
const payload = {};
|
|
534
|
+
if (flags.agentKey) payload.agentKey = String(flags.agentKey).trim();
|
|
535
|
+
const response = await apiPost(ctx, '/api/v1/agent/onboarding/me/api-key/rotate', payload, {
|
|
536
|
+
Authorization: `Bearer ${authToken}`,
|
|
537
|
+
});
|
|
538
|
+
printEnvelope({
|
|
539
|
+
success: true,
|
|
540
|
+
code: 'AGENT_KEY_ROTATED',
|
|
541
|
+
message: 'Agent API key rotated',
|
|
542
|
+
data: response,
|
|
543
|
+
meta: {
|
|
544
|
+
apiBase: ctx.apiBase,
|
|
545
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function commandAgentKeyRevoke(ctx, flags, capabilities) {
|
|
551
|
+
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for agent key revoke');
|
|
552
|
+
const payload = {};
|
|
553
|
+
if (flags.agentKey) payload.agentKey = String(flags.agentKey).trim();
|
|
554
|
+
const response = await apiPost(ctx, '/api/v1/agent/onboarding/me/api-key/revoke', payload, {
|
|
555
|
+
Authorization: `Bearer ${authToken}`,
|
|
556
|
+
});
|
|
557
|
+
printEnvelope({
|
|
558
|
+
success: true,
|
|
559
|
+
code: 'AGENT_KEY_REVOKED',
|
|
560
|
+
message: 'Agent API key revoked',
|
|
561
|
+
data: response,
|
|
562
|
+
meta: {
|
|
563
|
+
apiBase: ctx.apiBase,
|
|
564
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function buildIntentPayload(flags, options) {
|
|
570
|
+
const payload = {
|
|
571
|
+
agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
|
|
572
|
+
action: String(options.resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
|
|
573
|
+
params: parseJsonFlag(flags, 'params'),
|
|
574
|
+
};
|
|
575
|
+
if (flags.catalog) payload.catalog = String(flags.catalog).trim();
|
|
576
|
+
if (flags.chainId) payload.chainId = Number(flags.chainId);
|
|
577
|
+
if (flags.estimatedAmountUsd) payload.estimatedAmountUsd = Number(flags.estimatedAmountUsd);
|
|
578
|
+
if (flags.userDid) payload.userDid = String(flags.userDid);
|
|
579
|
+
if (options.validationOnly) payload.executionLane = 'validation_only';
|
|
580
|
+
if (options.forceIdempotency) {
|
|
581
|
+
payload.idempotencyKey = String(flags.idempotencyKey || `cli:${payload.action}:${Date.now()}:${randomUUID()}`);
|
|
582
|
+
} else if (flags.idempotencyKey) {
|
|
583
|
+
payload.idempotencyKey = String(flags.idempotencyKey);
|
|
584
|
+
}
|
|
585
|
+
return payload;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function getActionKey(action) {
|
|
589
|
+
return String(action?.action || action?.id || action?.key || action?.name || '')
|
|
590
|
+
.trim();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function getActionAliases(action) {
|
|
594
|
+
if (!Array.isArray(action?.aliases)) return [];
|
|
595
|
+
return action.aliases
|
|
596
|
+
.map((v) => String(v || '').trim())
|
|
597
|
+
.filter(Boolean);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function getCachedActionsCatalogTtlMs() {
|
|
601
|
+
const raw = Number(process.env.THIRDFY_CLI_CATALOG_CACHE_TTL_MS || DEFAULT_CATALOG_CACHE_TTL_MS);
|
|
602
|
+
if (!Number.isFinite(raw) || raw < 0) return DEFAULT_CATALOG_CACHE_TTL_MS;
|
|
603
|
+
return raw;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function getCachedActionsCatalog(ctx, flags) {
|
|
607
|
+
const policyAware = Boolean(flags.agentApiKey);
|
|
608
|
+
const cacheKey = policyAware
|
|
609
|
+
? `policy:${ctx.apiBase}:${String(flags.agentApiKey)}`
|
|
610
|
+
: `public:${ctx.apiBase}`;
|
|
611
|
+
const ttlMs = getCachedActionsCatalogTtlMs();
|
|
612
|
+
const now = Date.now();
|
|
613
|
+
const hit = catalogCache.get(cacheKey);
|
|
614
|
+
if (hit && hit.expiresAt > now) {
|
|
615
|
+
return hit.actions;
|
|
616
|
+
}
|
|
617
|
+
let endpoint = '/api/v1/agent/actions/catalog';
|
|
618
|
+
if (policyAware) {
|
|
619
|
+
endpoint = `/api/v1/agent/actions?agentApiKey=${encodeURIComponent(String(flags.agentApiKey))}`;
|
|
620
|
+
}
|
|
621
|
+
const response = await apiGet(ctx, endpoint);
|
|
622
|
+
const actions = extractActions(response);
|
|
623
|
+
catalogCache.set(cacheKey, {
|
|
624
|
+
actions,
|
|
625
|
+
expiresAt: now + ttlMs,
|
|
626
|
+
});
|
|
627
|
+
return actions;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function formatActionList(items) {
|
|
631
|
+
return items
|
|
632
|
+
.map((v) => `\`${v}\``)
|
|
633
|
+
.join(', ');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function applyActionFilters(actions, flags) {
|
|
637
|
+
let filtered = actions;
|
|
638
|
+
if (flags.catalog) {
|
|
639
|
+
const expectedCatalog = String(flags.catalog).trim().toLowerCase();
|
|
640
|
+
filtered = filtered.filter((a) => String(a.catalog || a.catalogId || 'default').trim().toLowerCase() === expectedCatalog);
|
|
641
|
+
}
|
|
642
|
+
if (flags.provider) {
|
|
643
|
+
const expectedProvider = String(flags.provider).trim().toLowerCase();
|
|
644
|
+
filtered = filtered.filter((a) => String(a.provider || '').trim().toLowerCase() === expectedProvider);
|
|
645
|
+
}
|
|
646
|
+
return filtered;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function resolveActionSelection(ctx, flags) {
|
|
650
|
+
const requestedAction = requireFlag(flags, 'action', 'Missing --action');
|
|
651
|
+
const requestedLower = requestedAction.toLowerCase();
|
|
652
|
+
const actions = applyActionFilters(await getCachedActionsCatalog(ctx, flags), flags);
|
|
653
|
+
const keyed = actions
|
|
654
|
+
.map((action) => ({
|
|
655
|
+
key: getActionKey(action),
|
|
656
|
+
aliases: getActionAliases(action),
|
|
657
|
+
}))
|
|
658
|
+
.filter((entry) => entry.key);
|
|
659
|
+
|
|
660
|
+
if (!keyed.length) {
|
|
661
|
+
return { requestedAction, resolvedAction: requestedAction };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const exact = keyed.filter(
|
|
665
|
+
(entry) => entry.key.toLowerCase() === requestedLower || entry.aliases.some((a) => a.toLowerCase() === requestedLower)
|
|
666
|
+
);
|
|
667
|
+
if (exact.length === 1) {
|
|
668
|
+
return { requestedAction, resolvedAction: exact[0].key };
|
|
669
|
+
}
|
|
670
|
+
if (exact.length > 1) {
|
|
671
|
+
throw new Error(
|
|
672
|
+
`Ambiguous --action "${requestedAction}". Multiple exact matches found: ${formatActionList(exact.map((v) => v.key))}.`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const fuzzy = keyed.filter(
|
|
677
|
+
(entry) =>
|
|
678
|
+
entry.key.toLowerCase().includes(requestedLower) ||
|
|
679
|
+
entry.aliases.some((a) => a.toLowerCase().includes(requestedLower))
|
|
680
|
+
);
|
|
681
|
+
if (fuzzy.length === 1) {
|
|
682
|
+
return { requestedAction, resolvedAction: fuzzy[0].key };
|
|
683
|
+
}
|
|
684
|
+
if (fuzzy.length > 1) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Ambiguous --action "${requestedAction}". Did you mean one of: ${formatActionList(
|
|
687
|
+
fuzzy.slice(0, 10).map((v) => v.key)
|
|
688
|
+
)}?`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
throw new Error(
|
|
692
|
+
`Unknown --action "${requestedAction}". No compatible action found in current catalog/provider scope.`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function getAuthToken(flags, missingMessage) {
|
|
697
|
+
const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
|
|
698
|
+
if (!authToken) throw new Error(missingMessage);
|
|
699
|
+
return authToken;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function extractActions(response) {
|
|
703
|
+
if (Array.isArray(response)) return response;
|
|
704
|
+
if (Array.isArray(response.actions)) return response.actions;
|
|
705
|
+
if (Array.isArray(response.data?.actions)) return response.data.actions;
|
|
706
|
+
return [];
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function negotiateCapabilities(ctx) {
|
|
710
|
+
try {
|
|
711
|
+
const response = await apiGet(ctx, '/api/v1/agent/cli/capabilities');
|
|
712
|
+
return response;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
if (ctx.verbose) {
|
|
715
|
+
process.stderr.write(`capabilities negotiation skipped: ${error.message}\n`);
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function apiGet(ctx, route, headers = {}) {
|
|
722
|
+
return requestJson(ctx, route, { method: 'GET', headers });
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function apiPost(ctx, route, body, headers = {}) {
|
|
726
|
+
return requestJson(ctx, route, {
|
|
727
|
+
method: 'POST',
|
|
728
|
+
headers: {
|
|
729
|
+
'content-type': 'application/json',
|
|
730
|
+
...headers,
|
|
731
|
+
},
|
|
732
|
+
body: JSON.stringify(body),
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function requestJson(ctx, route, init) {
|
|
737
|
+
const controller = new AbortController();
|
|
738
|
+
const timeout = setTimeout(() => controller.abort(), ctx.timeoutMs);
|
|
739
|
+
try {
|
|
740
|
+
const response = await fetch(`${ctx.apiBase}${route}`, {
|
|
741
|
+
...init,
|
|
742
|
+
signal: controller.signal,
|
|
743
|
+
});
|
|
744
|
+
const text = await response.text();
|
|
745
|
+
const data = text ? safeJsonParse(text) : {};
|
|
746
|
+
if (!response.ok) {
|
|
747
|
+
const error = new Error(data?.error || data?.message || `HTTP ${response.status}`);
|
|
748
|
+
error.statusCode = response.status;
|
|
749
|
+
error.payload = data;
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
752
|
+
return data;
|
|
753
|
+
} finally {
|
|
754
|
+
clearTimeout(timeout);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function requestWithRouteFallback(ctx, options) {
|
|
759
|
+
const routes = Array.isArray(options.routes) ? options.routes : [];
|
|
760
|
+
let lastError = null;
|
|
761
|
+
for (const route of routes) {
|
|
762
|
+
try {
|
|
763
|
+
if (options.method === 'GET') {
|
|
764
|
+
return await apiGet(ctx, route, options.headers || {});
|
|
765
|
+
}
|
|
766
|
+
if (options.method === 'POST') {
|
|
767
|
+
return await apiPost(ctx, route, options.body || {}, options.headers || {});
|
|
768
|
+
}
|
|
769
|
+
throw new Error(`Unsupported method for fallback request: ${options.method}`);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
lastError = error;
|
|
772
|
+
const statusCode = Number(error?.statusCode || 0);
|
|
773
|
+
if (statusCode !== 404) throw error;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
throw lastError || new Error('No route variants succeeded');
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function safeJsonParse(text) {
|
|
780
|
+
try {
|
|
781
|
+
return JSON.parse(text);
|
|
782
|
+
} catch {
|
|
783
|
+
return { raw: text };
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function printEnvelope(payload) {
|
|
788
|
+
if (jsonMode) {
|
|
789
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const status = payload.success ? 'OK' : 'ERROR';
|
|
793
|
+
process.stdout.write(`[${status}] ${payload.code}: ${payload.message}\n`);
|
|
794
|
+
process.stdout.write(`${JSON.stringify(payload.data || {}, null, 2)}\n`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function printHelp() {
|
|
798
|
+
const help = `
|
|
799
|
+
thirdfy-agent ${CLI_VERSION}
|
|
800
|
+
|
|
801
|
+
Core commands:
|
|
802
|
+
thirdfy-agent catalogs list [--json]
|
|
803
|
+
thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--json]
|
|
804
|
+
thirdfy-agent delegation upsert --user-did <did> --agent-key <key> --wallet-address <addr> [--json]
|
|
805
|
+
thirdfy-agent delegation status --auth-token <token> --agent-key <key> [--verify] [--json]
|
|
806
|
+
thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
|
|
807
|
+
thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
|
|
808
|
+
thirdfy-agent agent register --auth-token <token> --agent-key <key> --name <name> [--json]
|
|
809
|
+
thirdfy-agent agent key rotate --auth-token <token> [--agent-key <key>] [--json]
|
|
810
|
+
thirdfy-agent agent key revoke --auth-token <token> [--agent-key <key>] [--json]
|
|
811
|
+
thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--json]
|
|
812
|
+
thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--idempotency-key <key>] [--json]
|
|
813
|
+
thirdfy-agent intent-status --intent-id <id> [--json]
|
|
814
|
+
thirdfy-agent credits balance --auth-token <token> [--json]
|
|
815
|
+
|
|
816
|
+
Global flags:
|
|
817
|
+
--api-base <url> default: https://api.thirdfy.com
|
|
818
|
+
--env <file> load env vars from file
|
|
819
|
+
--timeout <ms> request timeout
|
|
820
|
+
--json stable machine-readable output
|
|
821
|
+
--verbose print adapter diagnostics
|
|
822
|
+
--version print version
|
|
823
|
+
`.trim();
|
|
824
|
+
process.stdout.write(`${help}\n`);
|
|
825
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thirdfy/agent-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"thirdfy-agent": "./bin/thirdfy-agent.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/utils/cli/",
|
|
12
|
+
"src/contracts/cli/schemas/",
|
|
13
|
+
"scripts/validate-cli-contract-schemas.mjs",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test \"test/**/*.test.cjs\"",
|
|
19
|
+
"lint": "node --check ./bin/thirdfy-agent.mjs",
|
|
20
|
+
"validate:cli-contract-schemas": "node ./scripts/validate-cli-contract-schemas.mjs",
|
|
21
|
+
"smoke:cli": "node ./bin/thirdfy-agent.mjs --help && node ./bin/thirdfy-agent.mjs --version --json",
|
|
22
|
+
"prepublishOnly": "npm run validate:cli-contract-schemas && npm run test && npm run smoke:cli"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"thirdfy",
|
|
26
|
+
"cli",
|
|
27
|
+
"agent",
|
|
28
|
+
"defi"
|
|
29
|
+
],
|
|
30
|
+
"author": "Thirdfy",
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/thirdfy/agent-cli.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/thirdfy/agent-cli/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/thirdfy/agent-cli#readme"
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const SCHEMA_DIR = path.resolve(process.cwd(), 'src/contracts/cli/schemas');
|
|
7
|
+
const REQUIRED_FILES = [
|
|
8
|
+
'actions.response.schema.json',
|
|
9
|
+
'preflight.response.schema.json',
|
|
10
|
+
'execute-intent.response.schema.json',
|
|
11
|
+
'intent-status.response.schema.json',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const REQUIRED_KEYS = ['$schema', 'title', 'type'];
|
|
15
|
+
|
|
16
|
+
function fail(message) {
|
|
17
|
+
process.stderr.write(`${message}\n`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(SCHEMA_DIR)) {
|
|
22
|
+
fail(`Schema directory missing: ${SCHEMA_DIR}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const fileName of REQUIRED_FILES) {
|
|
26
|
+
const filePath = path.join(SCHEMA_DIR, fileName);
|
|
27
|
+
if (!fs.existsSync(filePath)) {
|
|
28
|
+
fail(`Missing schema file: ${fileName}`);
|
|
29
|
+
}
|
|
30
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(raw);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
fail(`Invalid JSON in ${fileName}: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const key of REQUIRED_KEYS) {
|
|
39
|
+
if (!(key in parsed)) {
|
|
40
|
+
fail(`Missing key "${key}" in ${fileName}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!parsed.properties || typeof parsed.properties !== 'object') {
|
|
45
|
+
fail(`Schema ${fileName} must include top-level "properties" object`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
process.stdout.write('CLI schema snapshots are valid.\n');
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "CLI Critical Endpoint: actions",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"actions"
|
|
7
|
+
],
|
|
8
|
+
"properties": {
|
|
9
|
+
"actions": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"required": [
|
|
14
|
+
"action"
|
|
15
|
+
],
|
|
16
|
+
"properties": {
|
|
17
|
+
"action": {
|
|
18
|
+
"type": "string"
|
|
19
|
+
},
|
|
20
|
+
"description": {
|
|
21
|
+
"type": "string"
|
|
22
|
+
},
|
|
23
|
+
"requiredParams": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"paramsSchema": {
|
|
30
|
+
"type": [
|
|
31
|
+
"object",
|
|
32
|
+
"null"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"catalog": {
|
|
36
|
+
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
"catalogId": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
},
|
|
41
|
+
"provider": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"additionalProperties": true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"additionalProperties": true
|
|
50
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "CLI Critical Endpoint: execute-intent",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"success",
|
|
7
|
+
"results"
|
|
8
|
+
],
|
|
9
|
+
"properties": {
|
|
10
|
+
"success": {
|
|
11
|
+
"type": "boolean"
|
|
12
|
+
},
|
|
13
|
+
"intentId": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"status": {
|
|
17
|
+
"type": "string"
|
|
18
|
+
},
|
|
19
|
+
"total": {
|
|
20
|
+
"type": "number"
|
|
21
|
+
},
|
|
22
|
+
"executed": {
|
|
23
|
+
"type": "number"
|
|
24
|
+
},
|
|
25
|
+
"results": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"required": [
|
|
30
|
+
"success"
|
|
31
|
+
],
|
|
32
|
+
"properties": {
|
|
33
|
+
"success": {
|
|
34
|
+
"type": "boolean"
|
|
35
|
+
},
|
|
36
|
+
"txHash": {
|
|
37
|
+
"type": "string"
|
|
38
|
+
},
|
|
39
|
+
"resultType": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
"blockedReason": {
|
|
43
|
+
"type": "string"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"additionalProperties": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"error": {
|
|
50
|
+
"type": "string"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"additionalProperties": true
|
|
54
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "CLI Critical Endpoint: intent-status",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"success"
|
|
7
|
+
],
|
|
8
|
+
"properties": {
|
|
9
|
+
"success": {
|
|
10
|
+
"type": "boolean"
|
|
11
|
+
},
|
|
12
|
+
"intentId": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"status": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"total": {
|
|
19
|
+
"type": "number"
|
|
20
|
+
},
|
|
21
|
+
"executed": {
|
|
22
|
+
"type": "number"
|
|
23
|
+
},
|
|
24
|
+
"results": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"error": {
|
|
32
|
+
"type": "string"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"additionalProperties": true
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "CLI Critical Endpoint: preflight (execute-intent validation_only)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"success",
|
|
7
|
+
"results"
|
|
8
|
+
],
|
|
9
|
+
"properties": {
|
|
10
|
+
"success": {
|
|
11
|
+
"type": "boolean"
|
|
12
|
+
},
|
|
13
|
+
"intentId": {
|
|
14
|
+
"type": [
|
|
15
|
+
"string",
|
|
16
|
+
"null"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"status": {
|
|
20
|
+
"type": "string"
|
|
21
|
+
},
|
|
22
|
+
"total": {
|
|
23
|
+
"type": "number"
|
|
24
|
+
},
|
|
25
|
+
"executed": {
|
|
26
|
+
"type": "number"
|
|
27
|
+
},
|
|
28
|
+
"results": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"required": [
|
|
33
|
+
"success"
|
|
34
|
+
],
|
|
35
|
+
"properties": {
|
|
36
|
+
"success": {
|
|
37
|
+
"type": "boolean"
|
|
38
|
+
},
|
|
39
|
+
"resultType": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
"blockedReason": {
|
|
43
|
+
"type": "string"
|
|
44
|
+
},
|
|
45
|
+
"blockedStage": {
|
|
46
|
+
"type": "string"
|
|
47
|
+
},
|
|
48
|
+
"userDid": {
|
|
49
|
+
"type": "string"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"additionalProperties": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"error": {
|
|
56
|
+
"type": "string"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"additionalProperties": true
|
|
60
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function normalizeDelegationStatus(response) {
|
|
2
|
+
const data = response?.data && typeof response.data === 'object' ? response.data : response || {};
|
|
3
|
+
const raw = String(data?.status || data?.dbStatus || '').trim().toUpperCase();
|
|
4
|
+
const state = mapDelegationState(raw);
|
|
5
|
+
return {
|
|
6
|
+
state,
|
|
7
|
+
rawStatus: raw || null,
|
|
8
|
+
data,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function mapDelegationState(raw) {
|
|
13
|
+
if (!raw) return 'UNKNOWN';
|
|
14
|
+
if (raw === 'ACTIVE') return 'ACTIVE';
|
|
15
|
+
if (raw.includes('MISSING') && raw.includes('DELEGATION')) return 'MISSING_DELEGATION';
|
|
16
|
+
if (raw.includes('EXPIRED') && raw.includes('DELEGATION')) return 'EXPIRED_DELEGATION';
|
|
17
|
+
if (raw.includes('REVOKED') && raw.includes('DELEGATION')) return 'REVOKED_DELEGATION';
|
|
18
|
+
if (raw.includes('MISSING') && raw.includes('CREDENTIAL')) return 'MISSING_CREDENTIAL';
|
|
19
|
+
if (raw.includes('POLICY') && raw.includes('DEN')) return 'POLICY_DENIED';
|
|
20
|
+
if (raw.includes('CREDIT') && raw.includes('BLOCK')) return 'CREDITS_BLOCKED';
|
|
21
|
+
if (raw.includes('ACTION') && raw.includes('RAIL')) return 'ACTION_UNSUPPORTED_ON_RAIL';
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeCredentialStatus(response) {
|
|
26
|
+
const container = response?.data && typeof response.data === 'object' ? response.data : response || {};
|
|
27
|
+
const items = Array.isArray(container.items) ? container.items : [];
|
|
28
|
+
const active = items.filter((item) => String(item?.status || '').trim().toLowerCase() === 'active').length;
|
|
29
|
+
return {
|
|
30
|
+
state: active > 0 ? 'ACTIVE' : 'MISSING_CREDENTIAL',
|
|
31
|
+
total: items.length,
|
|
32
|
+
active,
|
|
33
|
+
items,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
normalizeDelegationStatus,
|
|
39
|
+
normalizeCredentialStatus,
|
|
40
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function summarizeBlockedReasons(results) {
|
|
2
|
+
const grouped = {};
|
|
3
|
+
for (const row of results) {
|
|
4
|
+
const reason = String(row?.blockedReason || '').trim();
|
|
5
|
+
if (!reason) continue;
|
|
6
|
+
grouped[reason] = (grouped[reason] || 0) + 1;
|
|
7
|
+
}
|
|
8
|
+
return grouped;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeIntentResponse(response) {
|
|
12
|
+
const results = Array.isArray(response?.results) ? response.results : [];
|
|
13
|
+
const isBlockedResult = (r) => {
|
|
14
|
+
const hasBlockedReason = String(r?.blockedReason || '').trim().length > 0;
|
|
15
|
+
return r?.resultType === 'blocked' || hasBlockedReason;
|
|
16
|
+
};
|
|
17
|
+
const blocked = results.filter(isBlockedResult).length;
|
|
18
|
+
const failed = results.filter((r) => !isBlockedResult(r) && (r?.resultType === 'failed' || r?.success === false)).length;
|
|
19
|
+
const executed = Number(response?.executed || 0);
|
|
20
|
+
const total = Number(response?.total || results.length || 0);
|
|
21
|
+
const success = Boolean(response?.success);
|
|
22
|
+
return {
|
|
23
|
+
success,
|
|
24
|
+
status: response?.status || (success ? 'queued' : 'failed'),
|
|
25
|
+
intentId: response?.intentId || null,
|
|
26
|
+
total,
|
|
27
|
+
executed,
|
|
28
|
+
blocked,
|
|
29
|
+
failed,
|
|
30
|
+
results,
|
|
31
|
+
blockedByReason: summarizeBlockedReasons(results),
|
|
32
|
+
error: response?.error || null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
normalizeIntentResponse,
|
|
38
|
+
summarizeBlockedReasons,
|
|
39
|
+
};
|