agentex-creator-sdk 1.0.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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/packages/contracts/src/deployer-investigation.d.ts +1015 -0
- package/dist/packages/contracts/src/deployer-investigation.js +101 -0
- package/dist/packages/contracts/src/index.d.ts +1701 -0
- package/dist/packages/contracts/src/index.js +380 -0
- package/dist/packages/contracts/src/indexed-activity.d.ts +684 -0
- package/dist/packages/contracts/src/indexed-activity.js +71 -0
- package/dist/packages/contracts/src/indexed-agents.d.ts +299 -0
- package/dist/packages/contracts/src/indexed-agents.js +131 -0
- package/dist/packages/contracts/src/inspection.d.ts +906 -0
- package/dist/packages/contracts/src/inspection.js +114 -0
- package/dist/packages/contracts/src/kinds.d.ts +5398 -0
- package/dist/packages/contracts/src/kinds.js +156 -0
- package/dist/packages/contracts/src/report-presentation.d.ts +346 -0
- package/dist/packages/contracts/src/report-presentation.js +120 -0
- package/dist/packages/contracts/src/solana-inspection.d.ts +451 -0
- package/dist/packages/contracts/src/solana-inspection.js +94 -0
- package/dist/packages/contracts/src/token-market.d.ts +193 -0
- package/dist/packages/contracts/src/token-market.js +335 -0
- package/dist/packages/contracts/src/wallet-analysis.d.ts +866 -0
- package/dist/packages/contracts/src/wallet-analysis.js +89 -0
- package/dist/packages/contracts/src/watchtower.d.ts +1141 -0
- package/dist/packages/contracts/src/watchtower.js +196 -0
- package/dist/packages/contracts/src/workflow.d.ts +1568 -0
- package/dist/packages/contracts/src/workflow.js +651 -0
- package/dist/packages/inspector/src/decode.d.ts +23 -0
- package/dist/packages/inspector/src/decode.js +150 -0
- package/dist/packages/inspector/src/scope.d.ts +87 -0
- package/dist/packages/inspector/src/scope.js +64 -0
- package/dist/packages/model/src/analysis.d.ts +149 -0
- package/dist/packages/model/src/analysis.js +387 -0
- package/dist/packages/model/src/pricing.d.ts +38 -0
- package/dist/packages/model/src/pricing.js +49 -0
- package/dist/packages/model/src/retry.d.ts +20 -0
- package/dist/packages/model/src/retry.js +31 -0
- package/dist/packages/model/src/schema.d.ts +10 -0
- package/dist/packages/model/src/schema.js +51 -0
- package/dist/packages/model/src/summary.d.ts +91 -0
- package/dist/packages/model/src/summary.js +177 -0
- package/dist/packages/model/src/types.d.ts +81 -0
- package/dist/packages/model/src/types.js +19 -0
- package/dist/packages/monitoring/src/delivery.d.ts +32 -0
- package/dist/packages/monitoring/src/delivery.js +53 -0
- package/dist/packages/providers/src/chain-transport.d.ts +42 -0
- package/dist/packages/providers/src/chain-transport.js +57 -0
- package/dist/packages/providers/src/coverage.d.ts +105 -0
- package/dist/packages/providers/src/coverage.js +260 -0
- package/dist/packages/providers/src/health.d.ts +273 -0
- package/dist/packages/providers/src/health.js +505 -0
- package/dist/packages/providers/src/keyed.d.ts +96 -0
- package/dist/packages/providers/src/keyed.js +240 -0
- package/dist/packages/providers/src/snapshot.d.ts +61 -0
- package/dist/packages/providers/src/snapshot.js +77 -0
- package/dist/packages/publication/src/fixtures.d.ts +45 -0
- package/dist/packages/publication/src/fixtures.js +350 -0
- package/dist/packages/research/src/index.d.ts +188 -0
- package/dist/packages/research/src/index.js +829 -0
- package/dist/packages/runtime/src/checkpoints.d.ts +65 -0
- package/dist/packages/runtime/src/checkpoints.js +214 -0
- package/dist/packages/runtime/src/policy.d.ts +57 -0
- package/dist/packages/runtime/src/policy.js +296 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.js +4 -0
- package/dist/packages/sdk/src/bin/agentex.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex.js +3 -0
- package/dist/packages/sdk/src/buyer-cli.d.ts +10 -0
- package/dist/packages/sdk/src/buyer-cli.js +210 -0
- package/dist/packages/sdk/src/buyer.d.ts +534 -0
- package/dist/packages/sdk/src/buyer.js +441 -0
- package/dist/packages/sdk/src/cli.d.ts +14 -0
- package/dist/packages/sdk/src/cli.js +149 -0
- package/dist/packages/sdk/src/errors.d.ts +31 -0
- package/dist/packages/sdk/src/errors.js +24 -0
- package/dist/packages/sdk/src/index.d.ts +236 -0
- package/dist/packages/sdk/src/index.js +150 -0
- package/dist/packages/sdk/src/local.d.ts +11 -0
- package/dist/packages/sdk/src/local.js +110 -0
- package/dist/packages/sdk/src/rails.d.ts +59 -0
- package/dist/packages/sdk/src/rails.js +96 -0
- package/dist/packages/sdk/src/report.d.ts +121 -0
- package/dist/packages/sdk/src/report.js +114 -0
- package/dist/packages/sdk/src/version.d.ts +2 -0
- package/dist/packages/sdk/src/version.js +2 -0
- package/dist/packages/watchtower/src/index.d.ts +150 -0
- package/dist/packages/watchtower/src/index.js +786 -0
- package/dist/packages/workflow/src/registry.d.ts +61 -0
- package/dist/packages/workflow/src/registry.js +76 -0
- package/examples/README.md +34 -0
- package/examples/cli-usage.sh +30 -0
- package/examples/fixtures/base-weth-input.json +4 -0
- package/examples/focused-researcher.json +127 -0
- package/examples/pay-with-eth-robinhood.mts +37 -0
- package/examples/pay-with-usdc.mts +41 -0
- package/examples/quickstart.mts +73 -0
- package/package.json +50 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { verifyWebhookSignature, WEBHOOK_EVENT_ID_HEADER } from '../../monitoring/src/delivery.js';
|
|
4
|
+
import { AgentexApiError, isAgentexApiError } from './errors.js';
|
|
5
|
+
import { assetIdOf, describeAsset, formatAmount, profileForAsset, railPath, resolveAssetId } from './rails.js';
|
|
6
|
+
import { readReport } from './report.js';
|
|
7
|
+
import { SDK_VERSION } from './version.js';
|
|
8
|
+
export { AgentexApiError, isAgentexApiError } from './errors.js';
|
|
9
|
+
export * from './rails.js';
|
|
10
|
+
export * from './report.js';
|
|
11
|
+
/**
|
|
12
|
+
* Typed buyer SDK for the AGENTEX buyer API (https://agentex.sh). It sends only `Authorization: Bearer <key>`, never cookies, never follows
|
|
13
|
+
* redirects and never includes the API key in an error. Plain HTTP is allowed only to loopback origins. Payment rails are chosen by asset id
|
|
14
|
+
* (SOL first; USDC on Solana; ETH on Robinhood Chain), and an API key is bound to one of them.
|
|
15
|
+
*/
|
|
16
|
+
export const BUYER_SDK_VERSION = SDK_VERSION;
|
|
17
|
+
/** The production API origin. The buyer API is served from the site's own origin. */
|
|
18
|
+
export const DEFAULT_API_ORIGIN = 'https://agentex.sh';
|
|
19
|
+
export const API_KEY_PATTERN = /^agx_(test|live)_[A-Za-z0-9_-]{43}$/;
|
|
20
|
+
export const IdempotencyKeySchema = z.string().min(8).max(128).regex(/^[a-zA-Z0-9_-]+$/);
|
|
21
|
+
const Hash = z.string().regex(/^[a-f0-9]{64}$/);
|
|
22
|
+
const Uuid = z.uuid();
|
|
23
|
+
const ATOMIC = /^(0|[1-9][0-9]{0,77})$/;
|
|
24
|
+
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
25
|
+
const MAX_RETRY_AFTER_MS = 30_000;
|
|
26
|
+
/** Marketplace topics (the catalog's controlled vocabulary; `topic` filter). */
|
|
27
|
+
export const CATALOG_TOPICS = Object.freeze(['token-intelligence', 'contract-forensics', 'wallet-intelligence', 'transaction-safety', 'monitoring', 'defi', 'liquidity', 'lending-yield',
|
|
28
|
+
'staking', 'stablecoins', 'governance', 'treasuries', 'rwa', 'bridges', 'chain-activity', 'launches', 'nfts', 'security']);
|
|
29
|
+
/** Builds a research-workflow input: `workflowInput('solana', { mint })`. */
|
|
30
|
+
export function workflowInput(chain, values) { return { workflow: 'agentex.workflow.v1', chain, values }; }
|
|
31
|
+
const statusKinds = { 400: 'invalid_input', 401: 'unauthorized', 403: 'forbidden', 404: 'not_found', 408: 'network', 409: 'conflict', 410: 'gone', 413: 'invalid_input', 429: 'rate_limited', 503: 'unavailable' };
|
|
32
|
+
const TERMINAL = new Set(['succeeded', 'partial', 'failed', 'cancelled']);
|
|
33
|
+
/** An exact origin. HTTPS anywhere; plain HTTP only on loopback, so a bearer key never crosses a network in clear text. */
|
|
34
|
+
export function buyerApiOrigin(raw) {
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new AgentexApiError('invalid_input', null, 'invalid_origin', 'The API origin is not a valid URL.', false);
|
|
41
|
+
}
|
|
42
|
+
const loopback = ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname);
|
|
43
|
+
if (!['http:', 'https:'].includes(url.protocol) || (url.protocol === 'http:' && !loopback) || url.username || url.password || (url.pathname !== '/' && url.pathname !== '') || url.search || url.hash)
|
|
44
|
+
throw new AgentexApiError('invalid_input', null, 'invalid_origin', 'The API origin must be an exact HTTPS origin, or HTTP on loopback.', false);
|
|
45
|
+
return url.origin;
|
|
46
|
+
}
|
|
47
|
+
/** The offer a listing makes in one asset (an asset id or `sol`, `usdc`, `eth`), or undefined when it does not accept that asset. */
|
|
48
|
+
export function offerFor(agent, assetId) {
|
|
49
|
+
const id = resolveAssetId(assetId);
|
|
50
|
+
return agent.offers.find(offer => offer.pricing.assetId === id);
|
|
51
|
+
}
|
|
52
|
+
const aborted = () => new AgentexApiError('aborted', null, 'aborted', 'The request was aborted by the caller.', false);
|
|
53
|
+
function sleep(ms, signal) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
if (signal?.aborted) {
|
|
56
|
+
reject(aborted());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, ms);
|
|
60
|
+
const onAbort = () => { clearTimeout(timer); reject(aborted()); };
|
|
61
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function retryAfter(headers) {
|
|
65
|
+
const raw = headers.get('retry-after');
|
|
66
|
+
if (!raw)
|
|
67
|
+
return null;
|
|
68
|
+
const seconds = Number(raw);
|
|
69
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
70
|
+
return Math.round(seconds * 1000);
|
|
71
|
+
const date = Date.parse(raw);
|
|
72
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : null;
|
|
73
|
+
}
|
|
74
|
+
function errorBody(data) {
|
|
75
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
76
|
+
return null;
|
|
77
|
+
const value = data;
|
|
78
|
+
const nested = value.error;
|
|
79
|
+
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
80
|
+
const error = nested;
|
|
81
|
+
if (typeof error.code === 'string' && typeof error.message === 'string')
|
|
82
|
+
return { code: error.code.slice(0, 100), message: error.message.slice(0, 2000) };
|
|
83
|
+
}
|
|
84
|
+
// The site's edge answers some failures (unavailable service, oversized or slow bodies) as { error?: string, message: string }.
|
|
85
|
+
if (typeof value.message === 'string')
|
|
86
|
+
return { code: typeof nested === 'string' ? nested.slice(0, 100) : 'http_error', message: value.message.slice(0, 2000) };
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
export function createBuyerClient(options) {
|
|
90
|
+
const origin = buyerApiOrigin(options.origin ?? DEFAULT_API_ORIGIN);
|
|
91
|
+
if (typeof options.apiKey !== 'string' || !API_KEY_PATTERN.test(options.apiKey))
|
|
92
|
+
throw new AgentexApiError('unauthorized', null, 'invalid_api_key', 'An AGENTEX API key (agx_live_... or agx_test_...) is required. Create one at https://agentex.sh/developers.', false);
|
|
93
|
+
const apiKey = options.apiKey;
|
|
94
|
+
const doFetch = options.fetch ?? fetch;
|
|
95
|
+
const defaultAsset = options.assetId === undefined ? undefined : resolveAssetId(options.assetId);
|
|
96
|
+
if (options.profile && defaultAsset && profileForAsset(defaultAsset) !== options.profile)
|
|
97
|
+
throw new AgentexApiError('invalid_input', null, 'profile_asset_mismatch', `Profile ${options.profile} does not pay in ${defaultAsset}.`, false);
|
|
98
|
+
let profile = options.profile ?? (defaultAsset ? profileForAsset(defaultAsset) : undefined);
|
|
99
|
+
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
100
|
+
const maxRetries = Math.max(0, Math.min(options.maxRetries ?? 2, 5));
|
|
101
|
+
const retryDelayMs = Math.max(0, options.retryDelayMs ?? 250);
|
|
102
|
+
const id = (value, label = 'ID') => { if (typeof value !== 'string' || !Uuid.safeParse(value).success)
|
|
103
|
+
throw new AgentexApiError('invalid_input', null, 'invalid_input', `${label} must be a UUID.`, false); return value; };
|
|
104
|
+
const assetOption = (value) => value === undefined ? defaultAsset : resolveAssetId(value);
|
|
105
|
+
async function once(method, path, body, signal) {
|
|
106
|
+
if (signal?.aborted)
|
|
107
|
+
throw aborted();
|
|
108
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
109
|
+
const combined = signal ? AbortSignal.any([timeout, signal]) : timeout;
|
|
110
|
+
const failed = (error) => {
|
|
111
|
+
if (isAgentexApiError(error))
|
|
112
|
+
throw error;
|
|
113
|
+
if (signal?.aborted)
|
|
114
|
+
throw aborted();
|
|
115
|
+
if (timeout.aborted)
|
|
116
|
+
throw new AgentexApiError('network', null, 'timeout', `${method} ${path} timed out after ${timeoutMs} ms.`, true);
|
|
117
|
+
throw new AgentexApiError('network', null, 'network', `${method} ${path} could not reach the API.`, true);
|
|
118
|
+
};
|
|
119
|
+
let response;
|
|
120
|
+
try {
|
|
121
|
+
response = await doFetch(`${origin}${path}`, { method, redirect: 'error', credentials: 'omit', signal: combined,
|
|
122
|
+
headers: { authorization: `Bearer ${apiKey}`, accept: 'application/json', ...(body === undefined ? {} : { 'content-type': 'application/json' }) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return failed(error);
|
|
126
|
+
}
|
|
127
|
+
const tooLarge = () => new AgentexApiError('invalid_response', response.status, 'invalid_response', 'The API response exceeds the SDK size limit.', false);
|
|
128
|
+
if (Number(response.headers.get('content-length') ?? 0) > MAX_RESPONSE_BYTES) {
|
|
129
|
+
await response.body?.cancel().catch(() => undefined);
|
|
130
|
+
throw tooLarge();
|
|
131
|
+
}
|
|
132
|
+
const reader = response.body?.getReader();
|
|
133
|
+
const chunks = [];
|
|
134
|
+
let size = 0;
|
|
135
|
+
try {
|
|
136
|
+
while (reader) {
|
|
137
|
+
const { done, value } = await reader.read();
|
|
138
|
+
if (done)
|
|
139
|
+
break;
|
|
140
|
+
size += value.byteLength;
|
|
141
|
+
if (size > MAX_RESPONSE_BYTES)
|
|
142
|
+
throw tooLarge();
|
|
143
|
+
chunks.push(value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return failed(error);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
await reader?.cancel().catch(() => undefined);
|
|
151
|
+
}
|
|
152
|
+
let data = null;
|
|
153
|
+
if (size) {
|
|
154
|
+
const bytes = new Uint8Array(size);
|
|
155
|
+
let offset = 0;
|
|
156
|
+
for (const chunk of chunks) {
|
|
157
|
+
bytes.set(chunk, offset);
|
|
158
|
+
offset += chunk.byteLength;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
data = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// A failure page that is not JSON (for example a proxy's HTML 502) is still classified by its status below.
|
|
165
|
+
if (response.ok)
|
|
166
|
+
throw new AgentexApiError('invalid_response', response.status, 'invalid_response', `${method} ${path} returned a non-JSON response.`, false);
|
|
167
|
+
data = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
const error = errorBody(data);
|
|
172
|
+
const kind = statusKinds[response.status] ?? (response.status >= 500 ? 'server' : 'invalid_response');
|
|
173
|
+
const retryable = kind === 'rate_limited' || kind === 'network' || (response.status >= 500 && response.status !== 503);
|
|
174
|
+
throw new AgentexApiError(kind, response.status, error?.code ?? 'http_error', error?.message ?? `${method} ${path} failed with HTTP ${response.status}.`, retryable, retryAfter(response.headers));
|
|
175
|
+
}
|
|
176
|
+
return { status: response.status, headers: response.headers, data };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* GETs and idempotent POSTs (idempotency-keyed acceptance and monitor creation; cancel, stop and acknowledge) retry network failures, 5xx
|
|
180
|
+
* other than 503, and 429 with backoff. Quote creation is never retried: a lost quote simply expires unused.
|
|
181
|
+
*/
|
|
182
|
+
async function request(method, path, requestOptions = {}) {
|
|
183
|
+
const retry = requestOptions.retry ?? method === 'GET';
|
|
184
|
+
for (let attempt = 0;; attempt++) {
|
|
185
|
+
try {
|
|
186
|
+
return await once(method, path, requestOptions.body, requestOptions.signal);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (!retry || !isAgentexApiError(error) || !error.retryable || attempt >= maxRetries)
|
|
190
|
+
throw error;
|
|
191
|
+
if (error.retryAfterMs !== null && error.retryAfterMs > MAX_RETRY_AFTER_MS)
|
|
192
|
+
throw error;
|
|
193
|
+
await sleep(Math.max(error.retryAfterMs ?? 0, Math.min(retryDelayMs * 2 ** attempt, 8_000)), requestOptions.signal);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const get = async (path, signal) => (await request('GET', path, { signal })).data;
|
|
198
|
+
const catalogQuery = (input) => {
|
|
199
|
+
const params = new URLSearchParams();
|
|
200
|
+
const { assetId, ...rest } = input;
|
|
201
|
+
if (assetId !== undefined && rest.profile === undefined)
|
|
202
|
+
params.set('profile', profileForAsset(assetId));
|
|
203
|
+
for (const [name, value] of Object.entries(rest))
|
|
204
|
+
if (value !== undefined)
|
|
205
|
+
params.set(name, String(value));
|
|
206
|
+
const text = params.toString();
|
|
207
|
+
return text ? `?${text}` : '';
|
|
208
|
+
};
|
|
209
|
+
let grant;
|
|
210
|
+
/** The key's grant, read once: its profile and asset never change. */
|
|
211
|
+
const keyGrant = () => grant ??= get('/v1/buyer/key').then(data => data.key, (error) => { grant = undefined; throw error; });
|
|
212
|
+
async function resolveProfile(assetId) {
|
|
213
|
+
if (assetId)
|
|
214
|
+
return profileForAsset(assetId);
|
|
215
|
+
if (profile)
|
|
216
|
+
return profile;
|
|
217
|
+
profile = (await keyGrant()).profile;
|
|
218
|
+
return profile;
|
|
219
|
+
}
|
|
220
|
+
const solanaRail = (value) => value === 'solana_sol' || value === 'solana_usdc';
|
|
221
|
+
async function waitUntil(read, done, wait, label) {
|
|
222
|
+
const deadline = Date.now() + (wait.timeoutMs ?? 300_000);
|
|
223
|
+
let interval = Math.max(50, wait.intervalMs ?? 1000);
|
|
224
|
+
const longest = Math.max(interval, wait.maxIntervalMs ?? 10_000);
|
|
225
|
+
const factor = Math.max(1, wait.backoff ?? 1.5);
|
|
226
|
+
for (;;) {
|
|
227
|
+
if (wait.signal?.aborted)
|
|
228
|
+
throw aborted();
|
|
229
|
+
let value;
|
|
230
|
+
let readOk = false;
|
|
231
|
+
try {
|
|
232
|
+
value = await read();
|
|
233
|
+
readOk = true;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (!isAgentexApiError(error) || !error.retryable || Date.now() >= deadline)
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
if (readOk && done(value))
|
|
240
|
+
return value;
|
|
241
|
+
const remaining = deadline - Date.now();
|
|
242
|
+
if (remaining <= 0)
|
|
243
|
+
throw new AgentexApiError('timeout', null, 'wait_timeout', `${label} did not finish before the wait timeout. Waiting again is safe.`, true);
|
|
244
|
+
await sleep(Math.min(interval, remaining), wait.signal);
|
|
245
|
+
interval = Math.min(longest, Math.ceil(interval * factor));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const client = {
|
|
249
|
+
origin,
|
|
250
|
+
/** The explicit or derived payment profile; undefined until the key's own profile has been read. */
|
|
251
|
+
get profile() { return profile; },
|
|
252
|
+
/** The profile this client quotes and pays with: the explicit one, the asset's, or the key's own (one `GET /v1/buyer/key`). */
|
|
253
|
+
resolveProfile: () => resolveProfile(defaultAsset),
|
|
254
|
+
/** The asset this client pays in: the client's `assetId`, else the key's own asset. */
|
|
255
|
+
async paymentAsset() { return describeAsset(defaultAsset ?? (await keyGrant()).assetId); },
|
|
256
|
+
/** The key's own scopes, asset, expiry and `remainingSpendAtomic`. Any valid key may read it. */
|
|
257
|
+
async key(call = {}) { const key = (await get('/v1/buyer/key', call.signal)).key; profile ??= key.profile; return key; },
|
|
258
|
+
/** One page of public agents. Pass `nextCursor` back as `cursor`; `null` means the last page. Needs `catalog:read`. */
|
|
259
|
+
async listAgents(input = {}, call = {}) { return get(`/v1/public/agents${catalogQuery(input)}`, call.signal); },
|
|
260
|
+
/** Iterates pages until `nextCursor` is null, bounded by `maxPages` (default 20). */
|
|
261
|
+
async *agents(input = {}, maxPages = 20) {
|
|
262
|
+
let cursor;
|
|
263
|
+
for (let page = 0; page < maxPages; page++) {
|
|
264
|
+
const result = await client.listAgents({ ...input, ...(cursor ? { cursor } : {}) });
|
|
265
|
+
yield* result.agents;
|
|
266
|
+
if (!result.nextCursor)
|
|
267
|
+
return;
|
|
268
|
+
cursor = result.nextCursor;
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
/** Permissions, limits, input fields, offers (one per payment rail) and limitations of one listing. */
|
|
272
|
+
async getAgent(listingId, call = {}) { return (await get(`/v1/public/agents/${id(listingId, 'listingId')}`, call.signal)).agent; },
|
|
273
|
+
/** The listings this key can buy with its rail (full manifests and tariffs), with the rail's pricing policy. */
|
|
274
|
+
async listings(input = {}) {
|
|
275
|
+
const assetId = assetOption(input.assetId);
|
|
276
|
+
const current = await resolveProfile(assetId);
|
|
277
|
+
const query = assetId && current !== 'sim' ? `?assetId=${encodeURIComponent(assetId)}` : '';
|
|
278
|
+
return get(`${railPath(current)}/catalog${query}`, input.signal);
|
|
279
|
+
},
|
|
280
|
+
/**
|
|
281
|
+
* Creates a server-priced quote on the rail of `assetId` (or the client's default, or the key's own asset). It reserves nothing and expires
|
|
282
|
+
* after 240 seconds. Never retried. When an asset was named, a quote priced in any other asset is refused with `asset_mismatch`.
|
|
283
|
+
*/
|
|
284
|
+
async createQuote(input) {
|
|
285
|
+
const listingId = id(input.listingId, 'listingId');
|
|
286
|
+
const assetId = assetOption(input.assetId);
|
|
287
|
+
const current = await resolveProfile(assetId);
|
|
288
|
+
const body = { listingId, input: input.input, resultPolicy: input.resultPolicy ?? 'complete_only', ...(assetId && solanaRail(current) ? { assetId } : {}) };
|
|
289
|
+
const quote = (await request('POST', `${railPath(current)}/quotes`, { body, retry: false, signal: input.signal })).data.quote;
|
|
290
|
+
const quoted = assetIdOf(quote.asset);
|
|
291
|
+
if (assetId && quoted && quoted !== assetId)
|
|
292
|
+
throw new AgentexApiError('conflict', null, 'asset_mismatch', `The server quoted in ${quoted}, not the requested ${assetId}. Nothing was reserved; the quote expires unused.`, false);
|
|
293
|
+
return quote;
|
|
294
|
+
},
|
|
295
|
+
/**
|
|
296
|
+
* Consents to one exact quote. The contract hash must be the one returned by createQuote. One idempotency key is used for all retries
|
|
297
|
+
* of this call (generated if omitted and returned), so a lost response never reserves twice. Pass the quote's `assetId` when the client
|
|
298
|
+
* has no default asset and the key's rail is not the quote's.
|
|
299
|
+
*/
|
|
300
|
+
async acceptQuote(input) {
|
|
301
|
+
const idempotencyKey = IdempotencyKeySchema.parse(input.idempotencyKey ?? `sdk-${randomUUID()}`);
|
|
302
|
+
if (!Hash.safeParse(input.contractHash).success)
|
|
303
|
+
throw new AgentexApiError('invalid_input', null, 'invalid_input', 'contractHash must be the 64-character hex hash from the quote.', false);
|
|
304
|
+
const quoteId = id(input.quoteId, 'quoteId');
|
|
305
|
+
const current = await resolveProfile(assetOption(input.assetId));
|
|
306
|
+
const result = await request('POST', `${railPath(current)}/quotes/${quoteId}/accept`, { body: { acceptedContractHash: input.contractHash, idempotencyKey }, retry: true, signal: input.signal });
|
|
307
|
+
return { ...result.data, idempotencyKey };
|
|
308
|
+
},
|
|
309
|
+
/** Run status and its latest events (at most 250). */
|
|
310
|
+
async getRun(runId, call = {}) { return get(`/v1/runs/${id(runId, 'runId')}`, call.signal); },
|
|
311
|
+
/**
|
|
312
|
+
* Polls until the run leaves queued/running, with backoff (1 s growing to 10 s by default). Transient failures keep polling; a timeout
|
|
313
|
+
* throws a retryable `timeout` error (`wait_timeout`) and an aborted signal throws `aborted`. The run itself is never affected.
|
|
314
|
+
*/
|
|
315
|
+
async waitForRun(runId, wait = {}) {
|
|
316
|
+
id(runId, 'runId');
|
|
317
|
+
return waitUntil(() => client.getRun(runId, wait.signal ? { signal: wait.signal } : {}), state => { wait.onStatus?.(state); return TERMINAL.has(state.run.status); }, wait, `Run ${runId}`);
|
|
318
|
+
},
|
|
319
|
+
/** Typed findings, evidence and sources of a finished run. A run without a report yet is a `conflict` (`report_unavailable`). */
|
|
320
|
+
async getReport(runId, call = {}) {
|
|
321
|
+
const state = await client.getRun(runId, call);
|
|
322
|
+
if (!state.run.output)
|
|
323
|
+
throw new AgentexApiError('conflict', null, 'report_unavailable', 'This run does not have a report yet. Wait for it to finish first.', false);
|
|
324
|
+
return readReport(state.run.output);
|
|
325
|
+
},
|
|
326
|
+
/** The canonical stored export (run, immutable version, events). A run without a report yet is a 409 `conflict`. */
|
|
327
|
+
async getResult(runId, call = {}) {
|
|
328
|
+
const response = await request('GET', `/v1/runs/${id(runId, 'runId')}/export`, { signal: call.signal });
|
|
329
|
+
return { artifactId: response.headers.get('x-agentex-artifact-id'), ...response.data };
|
|
330
|
+
},
|
|
331
|
+
/** Cancels a run; repeating it returns the current state. Cancellation charges only execution already incurred. */
|
|
332
|
+
async cancelRun(runId, call = {}) { return (await request('POST', `/v1/runs/${id(runId, 'runId')}/cancel`, { retry: true, signal: call.signal })).data.run; },
|
|
333
|
+
/** Reservation and settlement of a run on its rail (the client's default asset or the key's own). */
|
|
334
|
+
async getBilling(runId, input = {}) {
|
|
335
|
+
const current = await resolveProfile(assetOption(input.assetId));
|
|
336
|
+
return get(`${railPath(current)}/runs/${id(runId, 'runId')}/billing`, input.signal);
|
|
337
|
+
},
|
|
338
|
+
/**
|
|
339
|
+
* The run's billing with its payment asset and a formatted charge. The asset comes from `quote` when given, else `assetId`, else the key's
|
|
340
|
+
* own asset (a key only sees runs paid in its asset). `waitForSettlement` polls until the reservation settles.
|
|
341
|
+
*/
|
|
342
|
+
async getReceipt(runId, input = {}) {
|
|
343
|
+
id(runId, 'runId');
|
|
344
|
+
const assetId = (input.quote ? assetIdOf(input.quote.asset) : null) ?? (input.assetId !== undefined ? resolveAssetId(input.assetId) : (await keyGrant()).assetId);
|
|
345
|
+
const read = () => client.getBilling(runId, { assetId, ...(input.signal ? { signal: input.signal } : {}) });
|
|
346
|
+
const wait = input.waitForSettlement === true ? {} : input.waitForSettlement || null;
|
|
347
|
+
const billing = wait ? await waitUntil(read, value => value.status === 'settled', { ...(input.signal ? { signal: input.signal } : {}), ...wait }, `Settlement of run ${runId}`) : await read();
|
|
348
|
+
const payment = describeAsset(assetId);
|
|
349
|
+
const settlement = billing.settlement;
|
|
350
|
+
return { runId: billing.runId, quoteId: billing.quoteId, reservationId: billing.reservationId, status: billing.status, outcome: settlement?.outcome ?? null, payment,
|
|
351
|
+
maximumDebitAtomic: input.quote?.maximumBuyerDebitAtomic ?? null, chargedAtomic: settlement?.chargedAtomic ?? null,
|
|
352
|
+
charged: settlement ? formatAmount(settlement.chargedAtomic, payment) : null,
|
|
353
|
+
fees: settlement ? { creatorFeeAtomic: settlement.creatorFeeAtomic, platformFeeAtomic: settlement.platformFeeAtomic, executionChargeAtomic: settlement.executionChargeAtomic } : null,
|
|
354
|
+
pendingReason: billing.pendingReason, billing };
|
|
355
|
+
},
|
|
356
|
+
/**
|
|
357
|
+
* Quote, consent check, accept and wait in one call. It needs a consent bound (`maxDebitAtomic`, `approve`, or both) and refuses, without
|
|
358
|
+
* accepting, a quote above it. A wait failure after acceptance carries `runId` so you can resume with `waitForRun`.
|
|
359
|
+
*/
|
|
360
|
+
async research(input) {
|
|
361
|
+
if (input.maxDebitAtomic === undefined && !input.approve)
|
|
362
|
+
throw new AgentexApiError('refused', null, 'consent_required', 'research() spends from your balance: pass maxDebitAtomic (the most one run may reserve) or an approve(quote) callback.', false);
|
|
363
|
+
if (input.maxDebitAtomic !== undefined && !ATOMIC.test(input.maxDebitAtomic))
|
|
364
|
+
throw new AgentexApiError('invalid_input', null, 'invalid_amount', 'maxDebitAtomic must be a non-negative integer atomic amount.', false);
|
|
365
|
+
if (input.idempotencyKey !== undefined)
|
|
366
|
+
IdempotencyKeySchema.parse(input.idempotencyKey);
|
|
367
|
+
const signal = input.signal ? { signal: input.signal } : {};
|
|
368
|
+
const quote = await client.createQuote({ listingId: input.listingId, input: input.input, ...(input.resultPolicy ? { resultPolicy: input.resultPolicy } : {}), ...(input.assetId !== undefined ? { assetId: input.assetId } : {}), ...signal });
|
|
369
|
+
const assetId = assetIdOf(quote.asset) ?? assetOption(input.assetId) ?? null;
|
|
370
|
+
const payment = assetId ? describeAsset(assetId) : null;
|
|
371
|
+
const amount = (value) => payment ? formatAmount(value, payment) : `${value} atomic units`;
|
|
372
|
+
if (input.maxDebitAtomic !== undefined && BigInt(quote.maximumBuyerDebitAtomic) > BigInt(input.maxDebitAtomic))
|
|
373
|
+
throw new AgentexApiError('refused', null, 'quote_exceeds_limit', `The quote can reserve up to ${amount(quote.maximumBuyerDebitAtomic)}, above your limit of ${amount(input.maxDebitAtomic)}. Nothing was accepted.`, false);
|
|
374
|
+
if (input.approve && !(await input.approve(quote)))
|
|
375
|
+
throw new AgentexApiError('refused', null, 'not_approved', 'The quote was not approved. Nothing was accepted.', false);
|
|
376
|
+
const acceptance = await client.acceptQuote({ quoteId: quote.id, contractHash: quote.contractHash, ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}), ...(assetId ? { assetId } : {}), ...signal });
|
|
377
|
+
let state;
|
|
378
|
+
try {
|
|
379
|
+
state = await client.waitForRun(acceptance.run.id, { ...input.wait, ...(input.wait?.signal ?? input.signal ? { signal: input.wait?.signal ?? input.signal } : {}) });
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
if (isAgentexApiError(error))
|
|
383
|
+
error.runId = acceptance.run.id;
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
return { quote, acceptance, idempotencyKey: acceptance.idempotencyKey, run: state.run, events: state.events, report: readReport(state.run.output), payment };
|
|
387
|
+
},
|
|
388
|
+
/** Creates a monitor on your own Watchtower version. The webhook secret is returned only on first creation (null on replay). */
|
|
389
|
+
async createMonitor(input, call = {}) {
|
|
390
|
+
const body = { ...input, idempotencyKey: IdempotencyKeySchema.parse(input.idempotencyKey ?? `sdk-${randomUUID()}`) };
|
|
391
|
+
const response = await request('POST', '/v1/monitors', { body, retry: true, signal: call.signal });
|
|
392
|
+
return { ...response.data, idempotencyKey: body.idempotencyKey };
|
|
393
|
+
},
|
|
394
|
+
async listMonitors(call = {}) { return (await get('/v1/monitors', call.signal)).monitors; },
|
|
395
|
+
async getMonitor(monitorId, call = {}) { return get(`/v1/monitors/${id(monitorId, 'monitorId')}`, call.signal); },
|
|
396
|
+
async stopMonitor(monitorId, call = {}) { return (await request('POST', `/v1/monitors/${id(monitorId, 'monitorId')}/stop`, { retry: true, signal: call.signal })).data.monitor; },
|
|
397
|
+
async listNotifications(call = {}) { return (await get('/v1/notifications', call.signal)).notifications; },
|
|
398
|
+
async acknowledgeNotification(notificationId, call = {}) { return (await request('POST', `/v1/notifications/${id(notificationId, 'notificationId')}/ack`, { retry: true, signal: call.signal })).data.notification; },
|
|
399
|
+
};
|
|
400
|
+
return client;
|
|
401
|
+
}
|
|
402
|
+
export function createMemoryReplayStore(maximumEntries = 10_000) {
|
|
403
|
+
const seen = new Map();
|
|
404
|
+
return { claim(deliveryId, expiresAt) {
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
for (const [key, expiry] of seen) {
|
|
407
|
+
if (expiry > now && seen.size < maximumEntries)
|
|
408
|
+
break;
|
|
409
|
+
seen.delete(key);
|
|
410
|
+
}
|
|
411
|
+
if ((seen.get(deliveryId) ?? 0) > now)
|
|
412
|
+
return false;
|
|
413
|
+
seen.set(deliveryId, expiresAt.getTime());
|
|
414
|
+
return true;
|
|
415
|
+
} };
|
|
416
|
+
}
|
|
417
|
+
export const WebhookPayloadSchema = z.looseObject({ type: z.enum(['monitor.event', 'monitor.event.retracted']), eventId: Uuid, monitorId: Uuid, chain: z.string(), status: z.enum(['active', 'retracted']) });
|
|
418
|
+
/**
|
|
419
|
+
* Verifies a monitor webhook with the monitoring package's HMAC check (signature, event ID and a 5-minute timestamp window), then
|
|
420
|
+
* claims the delivery ID in `replay` so a duplicate callback is rejected. Pass the raw request body exactly as received.
|
|
421
|
+
* Signature failure is checked first, so a forged request can never consume a genuine delivery ID.
|
|
422
|
+
*/
|
|
423
|
+
export async function verifyWebhook(input) {
|
|
424
|
+
const headers = Object.fromEntries(Object.entries(input.headers).map(([name, value]) => [name.toLowerCase(), value]));
|
|
425
|
+
const tolerance = input.toleranceSeconds ?? 300;
|
|
426
|
+
const now = input.now ?? new Date();
|
|
427
|
+
if (!input.secret || !verifyWebhookSignature(input.secret, { headers, body: input.body }, { now, toleranceSeconds: tolerance }))
|
|
428
|
+
return { ok: false, reason: 'invalid_signature' };
|
|
429
|
+
let payload;
|
|
430
|
+
try {
|
|
431
|
+
payload = WebhookPayloadSchema.parse(JSON.parse(input.body));
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return { ok: false, reason: 'invalid_payload' };
|
|
435
|
+
}
|
|
436
|
+
const header = headers[WEBHOOK_EVENT_ID_HEADER];
|
|
437
|
+
const deliveryId = Array.isArray(header) ? header[0] : header;
|
|
438
|
+
if (!(await input.replay.claim(deliveryId, new Date(now.getTime() + 2 * tolerance * 1000))))
|
|
439
|
+
return { ok: false, reason: 'duplicate' };
|
|
440
|
+
return { ok: true, deliveryId, payload };
|
|
441
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creator CLI for the published `agentex` bin (N01). Behaviour matches the repository script `scripts/agentex.ts`: every file stays
|
|
3
|
+
* inside the working directory, outputs never overwrite, `submit` is loopback-only with a development session, and errors never
|
|
4
|
+
* echo arguments, server bodies or session material.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CREATOR_HELP = "AGENTEX creator CLI 1.0.0\n scaffold --id ID --name NAME [--chains ethereum,base] [--description TEXT]\n [--read-owner true|false] [--read-proxy-slots true|false]\n [--max-rpc N] [--max-duration-ms N] [--max-output-bytes N] --out manifest.json\n validate --file manifest.json [--out validation.json]\n test --file manifest.json [--out evaluation.json]\n estimate --file manifest.json [--out estimate.json]\n pack --file manifest.json --out package.json\n submit --file package.json --api http://127.0.0.1:4000 --origin http://localhost:3000\n --key IDEMPOTENCY_KEY [--session-stdin] [--out imported.json]\n version\n\nFiles must be regular JSON inside the current directory; output parents must exist and files are never overwritten.\nscaffold, validate, test, estimate and pack are offline. submit imports a private creator version into a local development API\nusing AGENTEX_SESSION_TOKEN or --session-stdin; it does not publish, approve, quote or purchase it.\nBuyer commands against https://agentex.sh use the separate agentex-buyer bin with a scoped AGENTEX_API_KEY.\n";
|
|
7
|
+
export interface CreatorCliIo {
|
|
8
|
+
env: Record<string, string | undefined>;
|
|
9
|
+
stdout(text: string): void;
|
|
10
|
+
stderr(text: string): void;
|
|
11
|
+
stdin?: AsyncIterable<Buffer | string>;
|
|
12
|
+
}
|
|
13
|
+
/** Runs one creator CLI command and returns the exit code. */
|
|
14
|
+
export declare function runCreatorCli(argv: readonly string[], io: CreatorCliIo): Promise<number>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ImportKeySchema, SDK_VERSION, estimateManifest, parseCreatorPackage, prepareCreatorPackage, scaffoldTokenResearcher, submitCreatorPackage, testManifest, validateManifest } from './index.js';
|
|
4
|
+
import { createLocalImportTransport, loopbackOrigin, readCreatorFile, writeCreatorFile } from './local.js';
|
|
5
|
+
/**
|
|
6
|
+
* Creator CLI for the published `agentex` bin (N01). Behaviour matches the repository script `scripts/agentex.ts`: every file stays
|
|
7
|
+
* inside the working directory, outputs never overwrite, `submit` is loopback-only with a development session, and errors never
|
|
8
|
+
* echo arguments, server bodies or session material.
|
|
9
|
+
*/
|
|
10
|
+
export const CREATOR_HELP = `AGENTEX creator CLI ${SDK_VERSION}
|
|
11
|
+
scaffold --id ID --name NAME [--chains ethereum,base] [--description TEXT]
|
|
12
|
+
[--read-owner true|false] [--read-proxy-slots true|false]
|
|
13
|
+
[--max-rpc N] [--max-duration-ms N] [--max-output-bytes N] --out manifest.json
|
|
14
|
+
validate --file manifest.json [--out validation.json]
|
|
15
|
+
test --file manifest.json [--out evaluation.json]
|
|
16
|
+
estimate --file manifest.json [--out estimate.json]
|
|
17
|
+
pack --file manifest.json --out package.json
|
|
18
|
+
submit --file package.json --api http://127.0.0.1:4000 --origin http://localhost:3000
|
|
19
|
+
--key IDEMPOTENCY_KEY [--session-stdin] [--out imported.json]
|
|
20
|
+
version
|
|
21
|
+
|
|
22
|
+
Files must be regular JSON inside the current directory; output parents must exist and files are never overwritten.
|
|
23
|
+
scaffold, validate, test, estimate and pack are offline. submit imports a private creator version into a local development API
|
|
24
|
+
using AGENTEX_SESSION_TOKEN or --session-stdin; it does not publish, approve, quote or purchase it.
|
|
25
|
+
Buyer commands against https://agentex.sh use the separate agentex-buyer bin with a scoped AGENTEX_API_KEY.
|
|
26
|
+
`;
|
|
27
|
+
const specifications = {
|
|
28
|
+
id: { type: 'string' }, name: { type: 'string' }, description: { type: 'string' }, chains: { type: 'string' },
|
|
29
|
+
'read-owner': { type: 'string' }, 'read-proxy-slots': { type: 'string' }, 'max-rpc': { type: 'string' },
|
|
30
|
+
'max-duration-ms': { type: 'string' }, 'max-output-bytes': { type: 'string' }, file: { type: 'string' }, out: { type: 'string' },
|
|
31
|
+
api: { type: 'string' }, origin: { type: 'string' }, key: { type: 'string' }, 'session-stdin': { type: 'boolean' }, help: { type: 'boolean' },
|
|
32
|
+
};
|
|
33
|
+
const allowed = {
|
|
34
|
+
scaffold: ['id', 'name', 'description', 'chains', 'read-owner', 'read-proxy-slots', 'max-rpc', 'max-duration-ms', 'max-output-bytes', 'out'],
|
|
35
|
+
validate: ['file', 'out'], test: ['file', 'out'], estimate: ['file', 'out'], pack: ['file', 'out'],
|
|
36
|
+
submit: ['file', 'out', 'api', 'origin', 'key', 'session-stdin'],
|
|
37
|
+
};
|
|
38
|
+
const boolean = (raw, fallback) => raw === undefined ? fallback : z.enum(['true', 'false']).parse(raw) === 'true';
|
|
39
|
+
const integer = (raw) => raw === undefined ? undefined : Number(z.string().regex(/^(0|[1-9][0-9]*)$/).parse(raw));
|
|
40
|
+
async function sessionFromStdin(stdin) {
|
|
41
|
+
let size = 0;
|
|
42
|
+
const chunks = [];
|
|
43
|
+
const reading = (async () => {
|
|
44
|
+
for await (const chunk of stdin) {
|
|
45
|
+
const bytes = Buffer.from(chunk);
|
|
46
|
+
size += bytes.length;
|
|
47
|
+
if (size > 128)
|
|
48
|
+
throw new Error('Session input exceeds its size limit.');
|
|
49
|
+
chunks.push(bytes);
|
|
50
|
+
}
|
|
51
|
+
return Buffer.concat(chunks).toString('utf8').trim();
|
|
52
|
+
})();
|
|
53
|
+
let timer;
|
|
54
|
+
const timeout = new Promise((_resolve, reject) => { timer = setTimeout(() => reject(new Error('Session input timed out.')), 5_000); });
|
|
55
|
+
try {
|
|
56
|
+
return await Promise.race([reading, timeout]);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Runs one creator CLI command and returns the exit code. */
|
|
63
|
+
export async function runCreatorCli(argv, io) {
|
|
64
|
+
let exitCode = 0;
|
|
65
|
+
try {
|
|
66
|
+
const command = argv[0];
|
|
67
|
+
if (!command || command === '--help' || command === 'help') {
|
|
68
|
+
io.stdout(CREATOR_HELP);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (command === 'version' || command === '--version') {
|
|
72
|
+
io.stdout(`${SDK_VERSION}\n`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
if (!Object.hasOwn(allowed, command))
|
|
76
|
+
throw new Error('Unsupported CLI command.');
|
|
77
|
+
const { values, tokens } = parseArgs({ args: argv.slice(1), options: specifications, strict: true, allowPositionals: false, tokens: true });
|
|
78
|
+
const names = tokens.filter(token => token.kind === 'option').map(token => token.name);
|
|
79
|
+
if (new Set(names).size !== names.length || names.some(name => name !== 'help' && !allowed[command].includes(name)))
|
|
80
|
+
throw new Error('Duplicate or unsupported command options.');
|
|
81
|
+
if (values.help) {
|
|
82
|
+
io.stdout(CREATOR_HELP);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
let result;
|
|
86
|
+
if (command === 'scaffold') {
|
|
87
|
+
if (!values.id || !values.name || !values.out)
|
|
88
|
+
throw new Error('Scaffold requires id, name and out.');
|
|
89
|
+
const limits = { ...(values['max-rpc'] === undefined ? {} : { maximumRpcCalls: integer(values['max-rpc']) }),
|
|
90
|
+
...(values['max-duration-ms'] === undefined ? {} : { maximumDurationMs: integer(values['max-duration-ms']) }),
|
|
91
|
+
...(values['max-output-bytes'] === undefined ? {} : { maximumOutputBytes: integer(values['max-output-bytes']) }) };
|
|
92
|
+
result = scaffoldTokenResearcher({ id: values.id, name: values.name, ...(values.description ? { description: values.description } : {}),
|
|
93
|
+
...(values.chains ? { chains: z.array(z.enum(['ethereum', 'base'])).parse(values.chains.split(',')) } : {}),
|
|
94
|
+
...(values['read-owner'] === undefined && values['read-proxy-slots'] === undefined ? {} : { researchPolicy: { readOwner: boolean(values['read-owner'], true), readProxySlots: boolean(values['read-proxy-slots'], true) } }),
|
|
95
|
+
...(Object.keys(limits).length ? { limits } : {}) });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
if (!values.file)
|
|
99
|
+
throw new Error('This command requires a JSON file.');
|
|
100
|
+
if ((command === 'pack' && !values.out) || (command === 'submit' && (!values.api || !values.origin || !values.key)))
|
|
101
|
+
throw new Error('Required command options are missing.');
|
|
102
|
+
// Reject endpoint/key misuse before reading a session or making a request.
|
|
103
|
+
if (command === 'submit') {
|
|
104
|
+
loopbackOrigin(values.api);
|
|
105
|
+
loopbackOrigin(values.origin);
|
|
106
|
+
ImportKeySchema.parse(values.key);
|
|
107
|
+
}
|
|
108
|
+
const raw = await readCreatorFile(values.file);
|
|
109
|
+
if (command === 'validate') {
|
|
110
|
+
const manifest = validateManifest(raw);
|
|
111
|
+
result = { status: 'valid', manifestHash: prepareCreatorPackage(manifest).manifestHash, executable: 'bounded-token-researcher' };
|
|
112
|
+
}
|
|
113
|
+
else if (command === 'estimate')
|
|
114
|
+
result = estimateManifest(raw);
|
|
115
|
+
else if (command === 'pack')
|
|
116
|
+
result = prepareCreatorPackage(raw);
|
|
117
|
+
else if (command === 'test') {
|
|
118
|
+
const evaluation = await testManifest(raw);
|
|
119
|
+
result = { ...evaluation, fixtures: evaluation.fixtures.map(({ report: _report, ...fixture }) => fixture) };
|
|
120
|
+
if (evaluation.status !== 'passed')
|
|
121
|
+
exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const envelope = parseCreatorPackage(raw);
|
|
125
|
+
if (values['session-stdin'] && io.env.AGENTEX_SESSION_TOKEN)
|
|
126
|
+
throw new Error('Choose one session input source.');
|
|
127
|
+
const sessionToken = values['session-stdin'] ? await sessionFromStdin(io.stdin ?? process.stdin) : io.env.AGENTEX_SESSION_TOKEN;
|
|
128
|
+
if (!sessionToken)
|
|
129
|
+
throw new Error('An authenticated development session is required.');
|
|
130
|
+
result = await submitCreatorPackage(envelope, { idempotencyKey: values.key, transport: createLocalImportTransport({ api: values.api, origin: values.origin, sessionToken }) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (values.out) {
|
|
134
|
+
await writeCreatorFile(values.out, result);
|
|
135
|
+
io.stdout(`${JSON.stringify({ command, output: 'written', status: exitCode ? 'failed' : 'ok' })}\n`);
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
io.stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
139
|
+
return exitCode;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// Do not echo argv, package labels, server response bodies, cookie values, URLs with credentials or stacks.
|
|
143
|
+
if (error instanceof z.ZodError)
|
|
144
|
+
io.stderr('Validation failed: the supplied JSON or command options do not match the supported creator contract.\n');
|
|
145
|
+
else
|
|
146
|
+
io.stderr('Command failed. Check required options, bounded JSON files, existing output paths and local authentication; no files are overwritten.\n');
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every failure the buyer client raises is an `AgentexApiError`. `status` is the HTTP status (null when no response arrived), `code` is the
|
|
3
|
+
* server's error code (or an SDK code such as `network`, `timeout`, `wait_timeout`, `aborted`, `consent_required`, `asset_mismatch`), and the
|
|
4
|
+
* message is safe to show a user: it never contains the API key.
|
|
5
|
+
*/
|
|
6
|
+
export type BuyerErrorKind = 'invalid_input' | 'unauthorized' | 'forbidden' | 'not_found' | 'conflict' | 'gone' | 'rate_limited' | 'unavailable' | 'server' | 'network' | 'invalid_response'
|
|
7
|
+
/** A wait (run or settlement) passed its deadline. Waiting again is safe. */
|
|
8
|
+
| 'timeout'
|
|
9
|
+
/** The caller's AbortSignal fired. */
|
|
10
|
+
| 'aborted'
|
|
11
|
+
/** The SDK refused to spend: no consent bound was given, or the quote exceeded it. Nothing was reserved. */
|
|
12
|
+
| 'refused';
|
|
13
|
+
export declare class AgentexApiError extends Error {
|
|
14
|
+
readonly kind: BuyerErrorKind;
|
|
15
|
+
readonly status: number | null;
|
|
16
|
+
readonly code: string;
|
|
17
|
+
/** True when repeating the same call can succeed (network failures, 5xx other than 503, rate limits, timeouts). */
|
|
18
|
+
readonly retryable: boolean;
|
|
19
|
+
/** Server-suggested wait before retrying (from `Retry-After`), when one was sent. */
|
|
20
|
+
readonly retryAfterMs: number | null;
|
|
21
|
+
readonly name = "AgentexApiError";
|
|
22
|
+
constructor(kind: BuyerErrorKind, status: number | null, code: string, message: string,
|
|
23
|
+
/** True when repeating the same call can succeed (network failures, 5xx other than 503, rate limits, timeouts). */
|
|
24
|
+
retryable: boolean,
|
|
25
|
+
/** Server-suggested wait before retrying (from `Retry-After`), when one was sent. */
|
|
26
|
+
retryAfterMs?: number | null);
|
|
27
|
+
/** Set when the failure happened after a run was accepted (`research()` waiting), so the caller can resume with `waitForRun(runId)`. */
|
|
28
|
+
runId?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Type guard for `catch (error)` blocks. */
|
|
31
|
+
export declare function isAgentexApiError(error: unknown): error is AgentexApiError;
|