site-agent-pro 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/README.md +689 -0
- package/dist/auth/credentialStore.js +62 -0
- package/dist/auth/inbox.js +193 -0
- package/dist/auth/profile.js +379 -0
- package/dist/auth/runner.js +1124 -0
- package/dist/backend/dashboardData.js +194 -0
- package/dist/backend/runArtifacts.js +48 -0
- package/dist/backend/runRepository.js +93 -0
- package/dist/bin.js +2 -0
- package/dist/cli/backfillSiteChecks.js +143 -0
- package/dist/cli/run.js +309 -0
- package/dist/cli/trade.js +69 -0
- package/dist/config.js +199 -0
- package/dist/core/agentProfiles.js +55 -0
- package/dist/core/aggregateReport.js +382 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/customTaskSuite.js +148 -0
- package/dist/core/evaluator.js +217 -0
- package/dist/core/executor.js +788 -0
- package/dist/core/fallbackReport.js +335 -0
- package/dist/core/formHeuristics.js +411 -0
- package/dist/core/gameplaySummary.js +164 -0
- package/dist/core/interaction.js +202 -0
- package/dist/core/pageState.js +201 -0
- package/dist/core/planner.js +1669 -0
- package/dist/core/processSubmissionBatch.js +204 -0
- package/dist/core/runAuditJob.js +170 -0
- package/dist/core/runner.js +2352 -0
- package/dist/core/siteBrief.js +107 -0
- package/dist/core/siteChecks.js +1526 -0
- package/dist/core/taskDirectives.js +279 -0
- package/dist/core/taskHeuristics.js +263 -0
- package/dist/dashboard/client.js +1256 -0
- package/dist/dashboard/contracts.js +95 -0
- package/dist/dashboard/narrative.js +277 -0
- package/dist/dashboard/server.js +458 -0
- package/dist/dashboard/theme.js +888 -0
- package/dist/index.js +84 -0
- package/dist/llm/client.js +188 -0
- package/dist/paystack/account.js +123 -0
- package/dist/paystack/client.js +100 -0
- package/dist/paystack/index.js +13 -0
- package/dist/paystack/test-paystack.js +83 -0
- package/dist/paystack/transfer.js +138 -0
- package/dist/paystack/types.js +74 -0
- package/dist/paystack/webhook.js +121 -0
- package/dist/prompts/browserAgent.js +124 -0
- package/dist/prompts/reviewer.js +71 -0
- package/dist/reporting/clickReplay.js +290 -0
- package/dist/reporting/html.js +930 -0
- package/dist/reporting/markdown.js +238 -0
- package/dist/reporting/template.js +1141 -0
- package/dist/schemas/types.js +361 -0
- package/dist/submissions/customTasks.js +196 -0
- package/dist/submissions/html.js +770 -0
- package/dist/submissions/model.js +56 -0
- package/dist/submissions/publicUrl.js +76 -0
- package/dist/submissions/service.js +74 -0
- package/dist/submissions/store.js +37 -0
- package/dist/submissions/types.js +65 -0
- package/dist/trade/engine.js +241 -0
- package/dist/trade/evm/erc20.js +44 -0
- package/dist/trade/extractor.js +148 -0
- package/dist/trade/policy.js +35 -0
- package/dist/trade/session.js +31 -0
- package/dist/trade/types.js +107 -0
- package/dist/trade/validator.js +148 -0
- package/dist/utils/files.js +59 -0
- package/dist/utils/log.js +24 -0
- package/dist/utils/playwrightCompat.js +14 -0
- package/dist/utils/time.js +3 -0
- package/dist/wallet/provider.js +345 -0
- package/dist/wallet/relay.js +129 -0
- package/dist/wallet/wallet.js +178 -0
- package/docs/01-installation.md +134 -0
- package/docs/02-running-your-first-audit.md +136 -0
- package/docs/03-configuration.md +233 -0
- package/docs/04-how-the-agent-thinks.md +41 -0
- package/docs/05-extending-personas-and-tasks.md +42 -0
- package/docs/06-hardening-for-production.md +92 -0
- package/package.json +60 -0
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { authSettings, resolveAuthSessionStatePath, updateAuthSettings } from "../auth/profile.js";
|
|
4
|
+
import { runAuthFlow } from "../auth/runner.js";
|
|
5
|
+
import { config, updateConfig, resolveLlmRuntime } from "../config.js";
|
|
6
|
+
import { getWalletConfig, getWalletBalance } from "../wallet/wallet.js";
|
|
7
|
+
import { getAgentAccount } from "../paystack/index.js";
|
|
8
|
+
import { buildCustomTaskSuite } from "../core/customTaskSuite.js";
|
|
9
|
+
import { runAuditJob } from "../core/runAuditJob.js";
|
|
10
|
+
import { normalizeCustomTasks, SUBMISSION_TASKS_REQUIRED_MESSAGE } from "../submissions/customTasks.js";
|
|
11
|
+
import { buildDefaultTradeRunOptions } from "../trade/policy.js";
|
|
12
|
+
import { TradeStrategySchema } from "../trade/types.js";
|
|
13
|
+
import { exec } from "node:child_process";
|
|
14
|
+
import { resolveRunDir } from "../utils/files.js";
|
|
15
|
+
import { info, warn } from "../utils/log.js";
|
|
16
|
+
import { startDashboard } from "../dashboard/server.js";
|
|
17
|
+
import { updateWalletSettings } from "../wallet/wallet.js";
|
|
18
|
+
const program = new Command();
|
|
19
|
+
function summarizeCliPath(filePath) {
|
|
20
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
21
|
+
return relativePath && relativePath !== "" && !relativePath.startsWith("..") ? relativePath : filePath;
|
|
22
|
+
}
|
|
23
|
+
function resolveMaybeUrl(baseUrl, value) {
|
|
24
|
+
const trimmed = value?.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return new URL(trimmed, baseUrl).toString();
|
|
29
|
+
}
|
|
30
|
+
function collectRepeatedOption(value, previous) {
|
|
31
|
+
return [...previous, value];
|
|
32
|
+
}
|
|
33
|
+
function parseLlmProvider(value) {
|
|
34
|
+
const normalized = value.trim().toLowerCase();
|
|
35
|
+
if (normalized === "openai" || normalized === "ollama") {
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Unsupported LLM provider '${value}'. Use 'openai' or 'ollama'.`);
|
|
39
|
+
}
|
|
40
|
+
function parseTradeStrategy(value) {
|
|
41
|
+
const parsed = TradeStrategySchema.safeParse(value.trim());
|
|
42
|
+
if (!parsed.success) {
|
|
43
|
+
throw new Error(`Unsupported trade strategy '${value}'. Use 'auto', 'dapp_only', or 'deposit_only'.`);
|
|
44
|
+
}
|
|
45
|
+
return parsed.data;
|
|
46
|
+
}
|
|
47
|
+
function parseOptionalConfirmationCount(value) {
|
|
48
|
+
const parsed = Number(value);
|
|
49
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 12) {
|
|
50
|
+
throw new Error("Trade confirmations must be an integer between 0 and 12.");
|
|
51
|
+
}
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
function openUrl(url) {
|
|
55
|
+
const command = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
56
|
+
exec(`${command} ${url}`, (error) => {
|
|
57
|
+
if (error) {
|
|
58
|
+
warn(`Could not automatically open browser: ${error.message}`);
|
|
59
|
+
info(`Please open manually at: ${url}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
program
|
|
64
|
+
.name("site-agent-pro")
|
|
65
|
+
.description("AI-powered browser agent for website auditing and side-by-side development");
|
|
66
|
+
program
|
|
67
|
+
.command("run", { isDefault: true })
|
|
68
|
+
.description("Run an audit against a target URL")
|
|
69
|
+
.option("--url <url>", "Website URL to test")
|
|
70
|
+
.option("--headed", "Run browser in headed mode")
|
|
71
|
+
.option("--mobile", "Run using a mobile device profile")
|
|
72
|
+
.option("--ignore-https-errors", "Allow invalid or self-signed HTTPS certificates")
|
|
73
|
+
.option("--storage-state <path>", "Load Playwright storage state JSON before the run")
|
|
74
|
+
.option("--save-storage-state <path>", "Save Playwright storage state JSON after the run")
|
|
75
|
+
.option("--task <task>", "Accepted task for the agent to perform. Repeat for multiple tasks.", collectRepeatedOption, [])
|
|
76
|
+
.option("--llm-provider <provider>", "LLM provider to use: openai or ollama")
|
|
77
|
+
.option("--model <model>", "Override the model name for the selected LLM provider")
|
|
78
|
+
.option("--ollama-base-url <url>", "Override the Ollama base URL")
|
|
79
|
+
.option("--auth-flow", "Create or verify a test account, log in, save session state, then continue the accepted tasks behind auth")
|
|
80
|
+
.option("--auth-only", "Run only the signup/verification/login bootstrap and save session state")
|
|
81
|
+
.option("--signup-url <url>", "Absolute or relative signup URL to use during auth bootstrap")
|
|
82
|
+
.option("--login-url <url>", "Absolute or relative login URL to use during auth bootstrap")
|
|
83
|
+
.option("--access-url <url>", "Absolute or relative protected URL to verify after login")
|
|
84
|
+
.option("--trade-enabled", "Allow deterministic onchain trade execution for this run")
|
|
85
|
+
.option("--trade-dry-run", "Validate the extracted trade without broadcasting it")
|
|
86
|
+
.option("--trade-strategy <strategy>", "Trade strategy: auto, dapp_only, or deposit_only")
|
|
87
|
+
.option("--trade-confirmations <count>", "Confirmations to wait for before marking a trade confirmed", parseOptionalConfirmationCount)
|
|
88
|
+
.option("--openai-api-key <key>", "Override OpenAI/LLM API key")
|
|
89
|
+
.option("--imap-password <password>", "Override IMAP password for OTP retrieval")
|
|
90
|
+
.option("--auth-password <password>", "Override AUTH_TEST_PASSWORD for signup/login")
|
|
91
|
+
.option("--private-key <key>", "Override Web3 wallet private key")
|
|
92
|
+
.action(async (options) => {
|
|
93
|
+
if (!options.url) {
|
|
94
|
+
program.help();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Apply security overrides before doing anything else
|
|
98
|
+
if (options.openaiApiKey)
|
|
99
|
+
updateConfig({ openaiApiKey: options.openaiApiKey });
|
|
100
|
+
if (options.imapPassword)
|
|
101
|
+
updateAuthSettings({ AUTH_IMAP_PASSWORD: options.imapPassword });
|
|
102
|
+
if (options.authPassword)
|
|
103
|
+
updateAuthSettings({ AUTH_TEST_PASSWORD: options.authPassword });
|
|
104
|
+
if (options.privateKey)
|
|
105
|
+
updateWalletSettings({ WALLET_PRIVATE_KEY: options.privateKey });
|
|
106
|
+
try {
|
|
107
|
+
const baseUrl = options.url;
|
|
108
|
+
const storageStatePath = options.storageState?.trim() ? path.resolve(options.storageState) : undefined;
|
|
109
|
+
const saveStorageStatePath = options.saveStorageState?.trim() ? path.resolve(options.saveStorageState) : undefined;
|
|
110
|
+
const configuredStorageStatePath = config.playwrightStorageStatePath
|
|
111
|
+
? path.resolve(config.playwrightStorageStatePath)
|
|
112
|
+
: undefined;
|
|
113
|
+
const effectiveStorageStatePath = storageStatePath ?? configuredStorageStatePath;
|
|
114
|
+
const authRequested = Boolean(options.authFlow) || Boolean(options.authOnly);
|
|
115
|
+
const authOnly = Boolean(options.authOnly);
|
|
116
|
+
const acceptedTasks = normalizeCustomTasks(options.task ?? []);
|
|
117
|
+
const suiteOverride = !authOnly
|
|
118
|
+
? (() => {
|
|
119
|
+
if (acceptedTasks.length === 0) {
|
|
120
|
+
throw new Error(`${SUBMISSION_TASKS_REQUIRED_MESSAGE} Use --task \"...\" one or more times for CLI runs.`);
|
|
121
|
+
}
|
|
122
|
+
return buildCustomTaskSuite(acceptedTasks);
|
|
123
|
+
})()
|
|
124
|
+
: undefined;
|
|
125
|
+
const signupUrl = resolveMaybeUrl(baseUrl, options.signupUrl ?? authSettings.signupUrl);
|
|
126
|
+
const loginUrl = resolveMaybeUrl(baseUrl, options.loginUrl ?? authSettings.loginUrl);
|
|
127
|
+
const accessUrl = resolveMaybeUrl(baseUrl, options.accessUrl ?? authSettings.accessUrl);
|
|
128
|
+
const llmRuntime = resolveLlmRuntime({
|
|
129
|
+
...(options.llmProvider ? { provider: parseLlmProvider(options.llmProvider) } : {}),
|
|
130
|
+
...(options.model?.trim() ? { model: options.model.trim() } : {}),
|
|
131
|
+
...(options.ollamaBaseUrl?.trim() ? { ollamaBaseUrl: options.ollamaBaseUrl.trim() } : {})
|
|
132
|
+
});
|
|
133
|
+
const defaultTradeOptions = buildDefaultTradeRunOptions();
|
|
134
|
+
const tradeOptions = {
|
|
135
|
+
enabled: Boolean(options.tradeEnabled) || Boolean(options.tradeDryRun) || defaultTradeOptions.enabled,
|
|
136
|
+
dryRun: Boolean(options.tradeDryRun),
|
|
137
|
+
strategy: options.tradeStrategy ? parseTradeStrategy(options.tradeStrategy) : defaultTradeOptions.strategy,
|
|
138
|
+
confirmations: options.tradeConfirmations !== undefined ? options.tradeConfirmations : defaultTradeOptions.confirmations
|
|
139
|
+
};
|
|
140
|
+
const walletConfig = await getWalletConfig();
|
|
141
|
+
const walletBalance = await getWalletBalance().catch(() => "0");
|
|
142
|
+
let paystackAccount = null;
|
|
143
|
+
let paystackError = null;
|
|
144
|
+
try {
|
|
145
|
+
paystackAccount = await getAgentAccount();
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
paystackError = err.message;
|
|
149
|
+
}
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log("💳 Financial Identity");
|
|
152
|
+
console.log("────────────────────────────────────────────────────────");
|
|
153
|
+
if (walletConfig) {
|
|
154
|
+
console.log(` Wallet: ${walletConfig.address}`);
|
|
155
|
+
console.log(` Balance: ${walletBalance} ETH`);
|
|
156
|
+
console.log(` Chain ID: ${walletConfig.chainId}`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.log(" Wallet: Not configured ⚪");
|
|
160
|
+
}
|
|
161
|
+
if (paystackAccount) {
|
|
162
|
+
console.log(` Paystack: ${paystackAccount.account_number} (${paystackAccount.bank.name})`);
|
|
163
|
+
console.log(` Customer: ${paystackAccount.customer.email}`);
|
|
164
|
+
}
|
|
165
|
+
else if (paystackError) {
|
|
166
|
+
console.log(` Paystack: ⚠️ Error - ${paystackError}`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.log(" Paystack: Not configured ⚪");
|
|
170
|
+
}
|
|
171
|
+
console.log("────────────────────────────────────────────────────────");
|
|
172
|
+
console.log("");
|
|
173
|
+
info(`Running site agent against ${baseUrl}`);
|
|
174
|
+
info(`Total run budget is capped at ${Math.round(config.maxSessionDurationMs / 1000)} seconds`);
|
|
175
|
+
info(llmRuntime.provider === "ollama"
|
|
176
|
+
? `Using Ollama model ${llmRuntime.model} via ${llmRuntime.ollamaBaseUrl}`
|
|
177
|
+
: `Using OpenAI model ${llmRuntime.model}`);
|
|
178
|
+
if (!authOnly) {
|
|
179
|
+
info(`Using ${acceptedTasks.length} accepted task${acceptedTasks.length === 1 ? "" : "s"} from explicit input`);
|
|
180
|
+
}
|
|
181
|
+
if (options.ignoreHttpsErrors) {
|
|
182
|
+
info("Ignoring HTTPS certificate errors for this run");
|
|
183
|
+
}
|
|
184
|
+
if (tradeOptions.enabled) {
|
|
185
|
+
info(`Trade execution is enabled${tradeOptions.dryRun ? " in dry-run mode" : ""} using strategy '${tradeOptions.strategy}' with ${tradeOptions.confirmations} confirmation${tradeOptions.confirmations === 1 ? "" : "s"}`);
|
|
186
|
+
}
|
|
187
|
+
if (effectiveStorageStatePath) {
|
|
188
|
+
info(`Loading Playwright storage state from ${summarizeCliPath(effectiveStorageStatePath)}`);
|
|
189
|
+
}
|
|
190
|
+
if (saveStorageStatePath) {
|
|
191
|
+
info(`Will save Playwright storage state to ${summarizeCliPath(saveStorageStatePath)}`);
|
|
192
|
+
}
|
|
193
|
+
if (authRequested) {
|
|
194
|
+
const runDir = resolveRunDir(baseUrl);
|
|
195
|
+
const authSessionStatePath = saveStorageStatePath ?? configuredStorageStatePath ?? resolveAuthSessionStatePath();
|
|
196
|
+
info(`Auth bootstrap is enabled${authOnly ? " in auth-only mode" : ""}`);
|
|
197
|
+
info(`Authenticated storage state will be saved to ${summarizeCliPath(authSessionStatePath)}`);
|
|
198
|
+
if (signupUrl) {
|
|
199
|
+
info(`Using signup URL ${signupUrl}`);
|
|
200
|
+
}
|
|
201
|
+
if (loginUrl) {
|
|
202
|
+
info(`Using login URL ${loginUrl}`);
|
|
203
|
+
}
|
|
204
|
+
if (accessUrl) {
|
|
205
|
+
info(`Using protected access URL ${accessUrl}`);
|
|
206
|
+
}
|
|
207
|
+
const authResult = await runAuthFlow({
|
|
208
|
+
baseUrl,
|
|
209
|
+
runDir,
|
|
210
|
+
signupUrl,
|
|
211
|
+
loginUrl,
|
|
212
|
+
accessUrl,
|
|
213
|
+
headed: Boolean(options.headed),
|
|
214
|
+
mobile: Boolean(options.mobile),
|
|
215
|
+
ignoreHttpsErrors: Boolean(options.ignoreHttpsErrors),
|
|
216
|
+
saveStorageStatePath: authSessionStatePath
|
|
217
|
+
});
|
|
218
|
+
info(`Auth bootstrap finished with status: ${authResult.status}`);
|
|
219
|
+
info(`Auth artifacts were written to ${path.join(runDir, "auth-flow.json")}`);
|
|
220
|
+
if (authOnly) {
|
|
221
|
+
info(`Authenticated session is ready at ${summarizeCliPath(authSessionStatePath)}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (authResult.status === "failed") {
|
|
225
|
+
throw new Error("Auth bootstrap failed before the audit could start.");
|
|
226
|
+
}
|
|
227
|
+
const result = await runAuditJob({
|
|
228
|
+
baseUrl,
|
|
229
|
+
runDir,
|
|
230
|
+
suiteOverride: suiteOverride,
|
|
231
|
+
headed: Boolean(options.headed),
|
|
232
|
+
mobile: Boolean(options.mobile),
|
|
233
|
+
ignoreHttpsErrors: Boolean(options.ignoreHttpsErrors),
|
|
234
|
+
storageStatePath: authSessionStatePath,
|
|
235
|
+
saveStorageStatePath: authSessionStatePath,
|
|
236
|
+
tradeOptions,
|
|
237
|
+
extraInputs: {
|
|
238
|
+
customTasks: acceptedTasks,
|
|
239
|
+
instructionText: acceptedTasks.join("\n"),
|
|
240
|
+
instructionFileName: null,
|
|
241
|
+
tradeOptions,
|
|
242
|
+
authBootstrapEnabled: true,
|
|
243
|
+
authAccessConfirmed: authResult.accessConfirmed,
|
|
244
|
+
authVerificationMethod: authResult.verificationMethod,
|
|
245
|
+
...(signupUrl ? { authSignupUrl: signupUrl } : {}),
|
|
246
|
+
...(loginUrl ? { authLoginUrl: loginUrl } : {}),
|
|
247
|
+
...(accessUrl ? { authAccessUrl: accessUrl } : {})
|
|
248
|
+
},
|
|
249
|
+
llmProvider: llmRuntime.provider,
|
|
250
|
+
model: llmRuntime.model,
|
|
251
|
+
ollamaBaseUrl: llmRuntime.ollamaBaseUrl
|
|
252
|
+
});
|
|
253
|
+
info(`Artifacts will be written to ${result.runDir}`);
|
|
254
|
+
info(`Completed. Overall score: ${result.report.overall_score}/10`);
|
|
255
|
+
info(`Task summary: ${result.report.summary}`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const result = await runAuditJob({
|
|
259
|
+
baseUrl,
|
|
260
|
+
suiteOverride: suiteOverride,
|
|
261
|
+
headed: Boolean(options.headed),
|
|
262
|
+
mobile: Boolean(options.mobile),
|
|
263
|
+
ignoreHttpsErrors: Boolean(options.ignoreHttpsErrors),
|
|
264
|
+
storageStatePath,
|
|
265
|
+
saveStorageStatePath,
|
|
266
|
+
tradeOptions,
|
|
267
|
+
extraInputs: {
|
|
268
|
+
customTasks: acceptedTasks,
|
|
269
|
+
instructionText: acceptedTasks.join("\n"),
|
|
270
|
+
instructionFileName: null,
|
|
271
|
+
tradeOptions
|
|
272
|
+
},
|
|
273
|
+
llmProvider: llmRuntime.provider,
|
|
274
|
+
model: llmRuntime.model,
|
|
275
|
+
ollamaBaseUrl: llmRuntime.ollamaBaseUrl
|
|
276
|
+
});
|
|
277
|
+
info(`Artifacts will be written to ${result.runDir}`);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
process.stderr.write(`\n[ERROR] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
program
|
|
285
|
+
.command("ui")
|
|
286
|
+
.alias("start")
|
|
287
|
+
.description("Launch the web dashboard and open it in the browser")
|
|
288
|
+
.option("--port <port>", "Port to run the dashboard on")
|
|
289
|
+
.option("--host <host>", "Host to run the dashboard on")
|
|
290
|
+
.action(async (options) => {
|
|
291
|
+
try {
|
|
292
|
+
const port = options.port ? parseInt(options.port, 10) : undefined;
|
|
293
|
+
const { url } = await startDashboard({
|
|
294
|
+
...(port !== undefined ? { port } : {}),
|
|
295
|
+
...(options.host !== undefined ? { host: options.host } : {})
|
|
296
|
+
});
|
|
297
|
+
info("Opening dashboard in browser...");
|
|
298
|
+
openUrl(url);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
process.stderr.write(`\n[ERROR] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
306
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
307
|
+
process.stderr.write(`[ERROR] ${message}\n`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { buildDefaultTradeRunOptions, getTradePolicy } from "../trade/policy.js";
|
|
5
|
+
import { executeTradeInstruction } from "../trade/engine.js";
|
|
6
|
+
import { SellInstructionSchema, TradeStrategySchema } from "../trade/types.js";
|
|
7
|
+
import { ensureDir, resolveRunDir, writeJson } from "../utils/files.js";
|
|
8
|
+
import { info } from "../utils/log.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
function parseTradeStrategy(value) {
|
|
11
|
+
const parsed = TradeStrategySchema.safeParse(value.trim());
|
|
12
|
+
if (!parsed.success) {
|
|
13
|
+
throw new Error(`Unsupported trade strategy '${value}'. Use 'auto', 'dapp_only', or 'deposit_only'.`);
|
|
14
|
+
}
|
|
15
|
+
return parsed.data;
|
|
16
|
+
}
|
|
17
|
+
function parseConfirmationCount(value) {
|
|
18
|
+
const parsed = Number(value);
|
|
19
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 12) {
|
|
20
|
+
throw new Error("Confirmations must be an integer between 0 and 12.");
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
program
|
|
25
|
+
.name("site-agent-trade")
|
|
26
|
+
.description("Execute a deterministic trade instruction from the CLI")
|
|
27
|
+
.requiredOption("--instruction <file>", "Path to a JSON file containing a SellInstruction payload")
|
|
28
|
+
.option("--broadcast", "Broadcast the transaction instead of running in dry-run mode")
|
|
29
|
+
.option("--strategy <strategy>", "Trade strategy: auto, dapp_only, or deposit_only", parseTradeStrategy)
|
|
30
|
+
.option("--confirmations <count>", "Confirmations to wait for before marking confirmed", parseConfirmationCount)
|
|
31
|
+
.option("--run-dir <path>", "Directory to store trade execution artifacts")
|
|
32
|
+
.action(async (options) => {
|
|
33
|
+
const instructionPath = path.resolve(options.instruction);
|
|
34
|
+
const runDir = options.runDir?.trim()
|
|
35
|
+
? path.resolve(options.runDir)
|
|
36
|
+
: resolveRunDir("https://trade-cli.local");
|
|
37
|
+
const defaultTradeOptions = buildDefaultTradeRunOptions();
|
|
38
|
+
const instructionPayload = JSON.parse(fs.readFileSync(instructionPath, "utf8"));
|
|
39
|
+
const instruction = SellInstructionSchema.parse(instructionPayload);
|
|
40
|
+
const tradeOptions = {
|
|
41
|
+
enabled: true,
|
|
42
|
+
dryRun: !options.broadcast,
|
|
43
|
+
strategy: options.strategy ?? defaultTradeOptions.strategy,
|
|
44
|
+
confirmations: options.confirmations ?? defaultTradeOptions.confirmations
|
|
45
|
+
};
|
|
46
|
+
ensureDir(runDir);
|
|
47
|
+
writeJson(path.join(runDir, "trade-instruction.json"), instruction);
|
|
48
|
+
const record = await executeTradeInstruction({
|
|
49
|
+
runDir,
|
|
50
|
+
instruction,
|
|
51
|
+
runOptions: tradeOptions,
|
|
52
|
+
policy: getTradePolicy(),
|
|
53
|
+
source: "cli"
|
|
54
|
+
});
|
|
55
|
+
info(`Trade artifact directory: ${runDir}`);
|
|
56
|
+
info(`Trade status: ${record.status}`);
|
|
57
|
+
info(record.note);
|
|
58
|
+
if (record.txHash) {
|
|
59
|
+
info(`Transaction hash: ${record.txHash}`);
|
|
60
|
+
}
|
|
61
|
+
if (record.status === "blocked" || record.status === "failed") {
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
66
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
67
|
+
process.stderr.write(`[ERROR] ${message}\n`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { TradePolicySchema } from "./trade/types.js";
|
|
4
|
+
dotenv.config();
|
|
5
|
+
export const MAX_TOTAL_RUN_DURATION_SECONDS = 600;
|
|
6
|
+
export const MAX_TOTAL_RUN_DURATION_MS = MAX_TOTAL_RUN_DURATION_SECONDS * 1000;
|
|
7
|
+
export const DEFAULT_TOTAL_RUN_DURATION_MS = MAX_TOTAL_RUN_DURATION_MS;
|
|
8
|
+
export const MIN_TOTAL_RUN_DURATION_MS = 60000;
|
|
9
|
+
export const MIN_BROWSER_EXECUTION_BUDGET_MS = 45000;
|
|
10
|
+
export const MIN_REPORTING_RESERVE_MS = 15000;
|
|
11
|
+
export const MAX_REPORTING_RESERVE_MS = 45000;
|
|
12
|
+
export const POST_RUN_AUDIT_RESERVE_MS = 45000;
|
|
13
|
+
export const DETECTED_DEVICE_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
14
|
+
export const LlmProviderSchema = z.enum(["openai", "ollama"]);
|
|
15
|
+
export function clampRunDurationMs(value) {
|
|
16
|
+
return Math.min(MAX_TOTAL_RUN_DURATION_MS, Math.max(MIN_TOTAL_RUN_DURATION_MS, Math.round(value)));
|
|
17
|
+
}
|
|
18
|
+
export function deriveReportingReserveMs(totalRunDurationMs) {
|
|
19
|
+
const clampedTotalRunDurationMs = clampRunDurationMs(totalRunDurationMs);
|
|
20
|
+
const desiredReserveMs = Math.round(clampedTotalRunDurationMs * 0.15);
|
|
21
|
+
const maxAllowedReserveMs = Math.max(MIN_REPORTING_RESERVE_MS, clampedTotalRunDurationMs - MIN_BROWSER_EXECUTION_BUDGET_MS);
|
|
22
|
+
return Math.min(MAX_REPORTING_RESERVE_MS, Math.max(MIN_REPORTING_RESERVE_MS, Math.min(desiredReserveMs, maxAllowedReserveMs)));
|
|
23
|
+
}
|
|
24
|
+
export function deriveBrowserExecutionBudgetMs(totalRunDurationMs) {
|
|
25
|
+
const clampedTotalRunDurationMs = clampRunDurationMs(totalRunDurationMs);
|
|
26
|
+
return clampedTotalRunDurationMs - deriveReportingReserveMs(clampedTotalRunDurationMs);
|
|
27
|
+
}
|
|
28
|
+
function normalizeOptionalString(value) {
|
|
29
|
+
const trimmed = value?.trim();
|
|
30
|
+
return trimmed ? trimmed : undefined;
|
|
31
|
+
}
|
|
32
|
+
function blankEnvToUndefined(value) {
|
|
33
|
+
if (typeof value !== "string") {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
return normalizeOptionalString(value);
|
|
37
|
+
}
|
|
38
|
+
function parseBooleanFlag(value, fallback) {
|
|
39
|
+
const normalized = normalizeOptionalString(value)?.toLowerCase();
|
|
40
|
+
if (!normalized) {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
if (["true", "1", "yes", "on"].includes(normalized)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (["false", "0", "no", "off"].includes(normalized)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
function parseChainIdList(value) {
|
|
52
|
+
const normalized = normalizeOptionalString(value);
|
|
53
|
+
if (!normalized) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return normalized
|
|
57
|
+
.split(",")
|
|
58
|
+
.map((item) => Number(item.trim()))
|
|
59
|
+
.filter((item) => Number.isFinite(item) && item > 0)
|
|
60
|
+
.map((item) => Math.round(item));
|
|
61
|
+
}
|
|
62
|
+
function parseTokenRegistry(value) {
|
|
63
|
+
const normalized = normalizeOptionalString(value);
|
|
64
|
+
if (!normalized) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const parsedValue = JSON.parse(normalized);
|
|
69
|
+
const result = z.array(z.object({
|
|
70
|
+
chainId: z.number(),
|
|
71
|
+
symbol: z.string(),
|
|
72
|
+
assetKind: z.enum(["native", "erc20"]),
|
|
73
|
+
contract: z.string().optional(),
|
|
74
|
+
decimals: z.number()
|
|
75
|
+
})).parse(parsedValue);
|
|
76
|
+
return result.map((entry) => ({
|
|
77
|
+
chainId: Math.round(entry.chainId),
|
|
78
|
+
symbol: entry.symbol.trim().toUpperCase(),
|
|
79
|
+
assetKind: entry.assetKind,
|
|
80
|
+
contract: entry.contract?.trim() ? entry.contract.trim() : undefined,
|
|
81
|
+
decimals: Math.round(entry.decimals)
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw new Error(`TRADE_TOKEN_REGISTRY must be valid JSON (array of { chainId, symbol, assetKind, contract?, decimals }): ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const EnvSchema = z.object({
|
|
89
|
+
LLM_PROVIDER: LlmProviderSchema.default("openai"),
|
|
90
|
+
OPENAI_API_KEY: z
|
|
91
|
+
.string()
|
|
92
|
+
.optional()
|
|
93
|
+
.transform((value) => normalizeOptionalString(value)),
|
|
94
|
+
OPENAI_BASE_URL: z.preprocess(blankEnvToUndefined, z.string().url().optional()),
|
|
95
|
+
OPENAI_MODEL: z.string().default("gpt-5"),
|
|
96
|
+
OPENAI_TEMPERATURE: z.preprocess(blankEnvToUndefined, z.coerce.number().min(0).max(2).optional()),
|
|
97
|
+
OPENAI_MAX_TOKENS: z.preprocess(blankEnvToUndefined, z.coerce.number().int().positive().optional()),
|
|
98
|
+
OLLAMA_BASE_URL: z.string().url().default("http://127.0.0.1:11434"),
|
|
99
|
+
OLLAMA_MODEL: z.string().default("llama3.1:8b"),
|
|
100
|
+
APP_BASE_URL: z.string().optional(),
|
|
101
|
+
HEADLESS: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.transform((value) => value !== "false"),
|
|
105
|
+
MAX_SESSION_DURATION_MS: z.coerce.number().int().positive().default(DEFAULT_TOTAL_RUN_DURATION_MS),
|
|
106
|
+
MAX_STEPS_PER_TASK: z.coerce.number().int().positive().default(32),
|
|
107
|
+
ACTION_DELAY_MS: z.coerce.number().int().nonnegative().default(600),
|
|
108
|
+
NAVIGATION_TIMEOUT_MS: z.coerce.number().int().positive().default(25000),
|
|
109
|
+
REPORT_TTL_DAYS: z.coerce.number().int().positive().default(30),
|
|
110
|
+
PLAYWRIGHT_STORAGE_STATE_PATH: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.transform((value) => normalizeOptionalString(value)),
|
|
114
|
+
RECORD_VIDEO: z
|
|
115
|
+
.string()
|
|
116
|
+
.optional()
|
|
117
|
+
.transform((value) => value === "true"),
|
|
118
|
+
TRADE_ENABLED: z
|
|
119
|
+
.string()
|
|
120
|
+
.optional()
|
|
121
|
+
.transform((value) => parseBooleanFlag(value, false)),
|
|
122
|
+
TRADE_ALLOWLISTED_CHAIN_IDS: z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.transform((value) => parseChainIdList(value)),
|
|
126
|
+
TRADE_TOKEN_REGISTRY: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.transform((value) => parseTokenRegistry(value)),
|
|
130
|
+
TRADE_MAX_TOKEN_AMOUNT: z
|
|
131
|
+
.string()
|
|
132
|
+
.optional()
|
|
133
|
+
.transform((value) => normalizeOptionalString(value)),
|
|
134
|
+
TRADE_REQUIRE_EXACT_TOKEN_CONTRACT: z
|
|
135
|
+
.string()
|
|
136
|
+
.optional()
|
|
137
|
+
.transform((value) => parseBooleanFlag(value, true)),
|
|
138
|
+
TRADE_CONFIRMATIONS_REQUIRED: z.coerce.number().int().min(0).max(12).default(1),
|
|
139
|
+
TRADE_RECEIPT_TIMEOUT_MS: z.coerce.number().int().positive().default(120000)
|
|
140
|
+
});
|
|
141
|
+
const parsed = EnvSchema.parse(process.env);
|
|
142
|
+
const requestedMaxSessionDurationMs = parsed.MAX_SESSION_DURATION_MS;
|
|
143
|
+
const maxSessionDurationMs = clampRunDurationMs(requestedMaxSessionDurationMs);
|
|
144
|
+
const resolvedAppBaseUrl = normalizeOptionalString(parsed.APP_BASE_URL) ?? normalizeOptionalString(process.env.RENDER_EXTERNAL_URL);
|
|
145
|
+
const tradePolicy = TradePolicySchema.parse({
|
|
146
|
+
enabledByDefault: parsed.TRADE_ENABLED,
|
|
147
|
+
allowlistedChainIds: parsed.TRADE_ALLOWLISTED_CHAIN_IDS,
|
|
148
|
+
tokenRegistry: parsed.TRADE_TOKEN_REGISTRY,
|
|
149
|
+
maxTokenAmount: parsed.TRADE_MAX_TOKEN_AMOUNT,
|
|
150
|
+
requireExactTokenContract: parsed.TRADE_REQUIRE_EXACT_TOKEN_CONTRACT,
|
|
151
|
+
receiptTimeoutMs: parsed.TRADE_RECEIPT_TIMEOUT_MS,
|
|
152
|
+
confirmationsRequired: parsed.TRADE_CONFIRMATIONS_REQUIRED
|
|
153
|
+
});
|
|
154
|
+
function resolveDefaultModel(provider) {
|
|
155
|
+
return provider === "ollama" ? parsed.OLLAMA_MODEL : parsed.OPENAI_MODEL;
|
|
156
|
+
}
|
|
157
|
+
const overrides = {};
|
|
158
|
+
export const config = {
|
|
159
|
+
get llmProvider() { return overrides.llmProvider ?? parsed.LLM_PROVIDER; },
|
|
160
|
+
get openaiApiKey() { return overrides.openaiApiKey ?? parsed.OPENAI_API_KEY; },
|
|
161
|
+
get openaiBaseUrl() { return overrides.openaiBaseUrl ?? parsed.OPENAI_BASE_URL; },
|
|
162
|
+
get openaiModel() { return overrides.openaiModel ?? parsed.OPENAI_MODEL; },
|
|
163
|
+
get openaiTemperature() { return overrides.openaiTemperature ?? parsed.OPENAI_TEMPERATURE; },
|
|
164
|
+
get openaiMaxTokens() { return overrides.openaiMaxTokens ?? parsed.OPENAI_MAX_TOKENS; },
|
|
165
|
+
get ollamaBaseUrl() { return overrides.ollamaBaseUrl ?? parsed.OLLAMA_BASE_URL; },
|
|
166
|
+
get ollamaModel() { return overrides.ollamaModel ?? parsed.OLLAMA_MODEL; },
|
|
167
|
+
get model() { return resolveDefaultModel(this.llmProvider); },
|
|
168
|
+
get appBaseUrl() { return overrides.appBaseUrl ?? resolvedAppBaseUrl; },
|
|
169
|
+
get headless() { return overrides.headless ?? parsed.HEADLESS; },
|
|
170
|
+
deviceTimezone: DETECTED_DEVICE_TIMEZONE,
|
|
171
|
+
get requestedMaxSessionDurationMs() { return overrides.requestedMaxSessionDurationMs ?? requestedMaxSessionDurationMs; },
|
|
172
|
+
get maxSessionDurationMs() { return clampRunDurationMs(this.requestedMaxSessionDurationMs); },
|
|
173
|
+
get browserExecutionBudgetMs() { return deriveBrowserExecutionBudgetMs(this.maxSessionDurationMs); },
|
|
174
|
+
get reportingReserveMs() { return deriveReportingReserveMs(this.maxSessionDurationMs); },
|
|
175
|
+
postRunAuditReserveMs: POST_RUN_AUDIT_RESERVE_MS,
|
|
176
|
+
get maxStepsPerTask() { return overrides.maxStepsPerTask ?? parsed.MAX_STEPS_PER_TASK; },
|
|
177
|
+
get actionDelayMs() { return overrides.actionDelayMs ?? parsed.ACTION_DELAY_MS; },
|
|
178
|
+
get navigationTimeoutMs() { return overrides.navigationTimeoutMs ?? parsed.NAVIGATION_TIMEOUT_MS; },
|
|
179
|
+
get reportTtlDays() { return overrides.reportTtlDays ?? parsed.REPORT_TTL_DAYS; },
|
|
180
|
+
get playwrightStorageStatePath() { return overrides.playwrightStorageStatePath ?? parsed.PLAYWRIGHT_STORAGE_STATE_PATH; },
|
|
181
|
+
get recordVideo() { return overrides.recordVideo ?? parsed.RECORD_VIDEO; },
|
|
182
|
+
get tradePolicy() { return tradePolicy; },
|
|
183
|
+
desktopViewport: { width: 1440, height: 900 },
|
|
184
|
+
mobileViewport: { width: 390, height: 844 }
|
|
185
|
+
};
|
|
186
|
+
export function updateConfig(updates) {
|
|
187
|
+
Object.assign(overrides, updates);
|
|
188
|
+
}
|
|
189
|
+
export function resolveLlmRuntime(options) {
|
|
190
|
+
const provider = options?.provider ?? config.llmProvider;
|
|
191
|
+
const model = normalizeOptionalString(options?.model) ??
|
|
192
|
+
(provider === "ollama" ? config.ollamaModel : config.openaiModel);
|
|
193
|
+
const ollamaBaseUrl = normalizeOptionalString(options?.ollamaBaseUrl) ?? config.ollamaBaseUrl;
|
|
194
|
+
return {
|
|
195
|
+
provider,
|
|
196
|
+
model,
|
|
197
|
+
ollamaBaseUrl
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function formatAgentOrdinal(index) {
|
|
2
|
+
return String(index).padStart(2, "0");
|
|
3
|
+
}
|
|
4
|
+
function formatAgentLabel(index) {
|
|
5
|
+
return `Agent-${formatAgentOrdinal(index)}`;
|
|
6
|
+
}
|
|
7
|
+
function cloneTaskSuite(baseSuite) {
|
|
8
|
+
return {
|
|
9
|
+
tasks: baseSuite.tasks.map((task) => ({
|
|
10
|
+
...task,
|
|
11
|
+
failure_signals: [...task.failure_signals],
|
|
12
|
+
...(task.gameplay ? { gameplay: { ...task.gameplay } } : {})
|
|
13
|
+
})),
|
|
14
|
+
persona: {
|
|
15
|
+
name: baseSuite.persona.name,
|
|
16
|
+
intent: baseSuite.persona.intent,
|
|
17
|
+
constraints: [...baseSuite.persona.constraints]
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function buildAgentVariants(agentCount, baseSuiteOverride) {
|
|
22
|
+
const baseSuite = baseSuiteOverride;
|
|
23
|
+
const cappedAgentCount = Math.min(5, Math.max(1, Math.round(agentCount)));
|
|
24
|
+
return Array.from({ length: cappedAgentCount }, (_, index) => {
|
|
25
|
+
const agentIndex = index + 1;
|
|
26
|
+
const agentLabel = formatAgentLabel(agentIndex);
|
|
27
|
+
return {
|
|
28
|
+
id: `agent-${formatAgentOrdinal(agentIndex)}`,
|
|
29
|
+
index: agentIndex,
|
|
30
|
+
label: agentLabel,
|
|
31
|
+
profileLabel: agentLabel,
|
|
32
|
+
personaVariantKey: agentLabel.toLowerCase(),
|
|
33
|
+
personaName: agentLabel,
|
|
34
|
+
taskSuite: cloneTaskSuite(baseSuite)
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function buildInitialAgentRuns(agentCount, baseSuiteOverride) {
|
|
39
|
+
return buildAgentVariants(agentCount, baseSuiteOverride).map((variant) => ({
|
|
40
|
+
id: variant.id,
|
|
41
|
+
index: variant.index,
|
|
42
|
+
label: variant.label,
|
|
43
|
+
profileLabel: variant.profileLabel,
|
|
44
|
+
personaName: variant.personaName,
|
|
45
|
+
personaVariantKey: variant.personaVariantKey,
|
|
46
|
+
status: "queued",
|
|
47
|
+
startedAt: null,
|
|
48
|
+
completedAt: null,
|
|
49
|
+
runId: null,
|
|
50
|
+
runDir: null,
|
|
51
|
+
error: null,
|
|
52
|
+
reportSummary: null,
|
|
53
|
+
overallScore: null
|
|
54
|
+
}));
|
|
55
|
+
}
|