@bloxwap/agent-cli 0.0.0-stage → 0.4.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 +42 -0
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/SKILL.md +212 -0
- package/dist/chunk-R7P7UMOD.js +9003 -0
- package/dist/cli.js +2345 -0
- package/dist/rpc-F7ZZ354G.js +14 -0
- package/package.json +63 -4
- package/policies/agent-trade-only.json +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AgentCliError,
|
|
4
|
+
PERPS_USDC,
|
|
5
|
+
PaperStore,
|
|
6
|
+
RelayClient,
|
|
7
|
+
Runner,
|
|
8
|
+
abandonDeposit,
|
|
9
|
+
accountEquity,
|
|
10
|
+
accrueFunding,
|
|
11
|
+
activeRunner,
|
|
12
|
+
applyPolicy,
|
|
13
|
+
assertApprovedNetwork,
|
|
14
|
+
assertCanTrade,
|
|
15
|
+
assertLiveMode,
|
|
16
|
+
assertSafeInput,
|
|
17
|
+
atomicWrite,
|
|
18
|
+
auditStoreFor,
|
|
19
|
+
checkRisk,
|
|
20
|
+
configPath,
|
|
21
|
+
copyAgentFields,
|
|
22
|
+
createAgent,
|
|
23
|
+
createClients,
|
|
24
|
+
detachRunner,
|
|
25
|
+
dispatch,
|
|
26
|
+
eventTypes,
|
|
27
|
+
events,
|
|
28
|
+
exchangeAction,
|
|
29
|
+
expiryWarnings,
|
|
30
|
+
followEvents,
|
|
31
|
+
formatUsdc,
|
|
32
|
+
hlEndpoint,
|
|
33
|
+
hlInfo,
|
|
34
|
+
jsonRequest,
|
|
35
|
+
levels,
|
|
36
|
+
liquidate,
|
|
37
|
+
liquidationPx,
|
|
38
|
+
loadConfig,
|
|
39
|
+
mapHyperliquidError,
|
|
40
|
+
matchOrders,
|
|
41
|
+
migrate,
|
|
42
|
+
newCloid,
|
|
43
|
+
nextNonce,
|
|
44
|
+
normalizeError,
|
|
45
|
+
openSigner,
|
|
46
|
+
paperAccountState,
|
|
47
|
+
paperData,
|
|
48
|
+
paperDataNetwork,
|
|
49
|
+
paperDir,
|
|
50
|
+
paperEvents,
|
|
51
|
+
paperOpenOrders,
|
|
52
|
+
parseQuote,
|
|
53
|
+
parseUsdc,
|
|
54
|
+
pollDeposits,
|
|
55
|
+
prepareEoaAgent,
|
|
56
|
+
privyClient,
|
|
57
|
+
readAudit,
|
|
58
|
+
readDeposits,
|
|
59
|
+
readEvents,
|
|
60
|
+
readStoredConfig,
|
|
61
|
+
redact,
|
|
62
|
+
register,
|
|
63
|
+
requiredDailyLoss,
|
|
64
|
+
resolveMode,
|
|
65
|
+
rolloverDay,
|
|
66
|
+
roundPrice,
|
|
67
|
+
roundSize,
|
|
68
|
+
routeToRunner,
|
|
69
|
+
rpcServer,
|
|
70
|
+
runnerPidPath,
|
|
71
|
+
runningInfo,
|
|
72
|
+
saveConfig,
|
|
73
|
+
servicesForMode,
|
|
74
|
+
setActiveRunner,
|
|
75
|
+
setRouting,
|
|
76
|
+
stateDir,
|
|
77
|
+
updateConfig,
|
|
78
|
+
updateDeposits,
|
|
79
|
+
verifyAudit,
|
|
80
|
+
withTradeLock
|
|
81
|
+
} from "./chunk-R7P7UMOD.js";
|
|
82
|
+
|
|
83
|
+
// src/commands/config.ts
|
|
84
|
+
import { z } from "zod";
|
|
85
|
+
register("config", {
|
|
86
|
+
description: "Get, set or validate local configuration",
|
|
87
|
+
schema: z.object({
|
|
88
|
+
args: z.array(z.string()).default([]),
|
|
89
|
+
yes: z.boolean().default(false)
|
|
90
|
+
}),
|
|
91
|
+
execute: async ({ args, yes }, context) => {
|
|
92
|
+
const [action = "get", key, value] = args;
|
|
93
|
+
const config = await loadConfig(context.configPath, {});
|
|
94
|
+
if (action === "validate") return { valid: true, version: config.version };
|
|
95
|
+
if (action === "get")
|
|
96
|
+
return key ? key.split(".").reduce(
|
|
97
|
+
(v, k) => v?.[k],
|
|
98
|
+
config
|
|
99
|
+
) ?? null : config;
|
|
100
|
+
if (action !== "set" || !key || value === void 0)
|
|
101
|
+
throw new AgentCliError("CONFIG_INVALID", "Use config set key value");
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(value);
|
|
105
|
+
} catch {
|
|
106
|
+
parsed = value;
|
|
107
|
+
}
|
|
108
|
+
if (key === "network" && config.approvedNetwork && parsed !== config.approvedNetwork)
|
|
109
|
+
throw new AgentCliError(
|
|
110
|
+
"CONFIG_INVALID",
|
|
111
|
+
"Network is pinned to the agent approval; use a separate config for another network"
|
|
112
|
+
);
|
|
113
|
+
if (key === "mode" && parsed === "live" && !yes && config.mode !== "live")
|
|
114
|
+
throw new AgentCliError(
|
|
115
|
+
"CONFIG_INVALID",
|
|
116
|
+
"Switching to live mode requires explicit confirmation; re-run with --yes"
|
|
117
|
+
);
|
|
118
|
+
const updated = structuredClone(config);
|
|
119
|
+
const parts = key.split(".");
|
|
120
|
+
let cursor = updated;
|
|
121
|
+
for (const part of parts.slice(0, -1)) {
|
|
122
|
+
if (!Object.hasOwn(cursor, part) || !cursor[part] || typeof cursor[part] !== "object")
|
|
123
|
+
throw new AgentCliError("CONFIG_INVALID", "Unknown config key");
|
|
124
|
+
cursor = cursor[part];
|
|
125
|
+
}
|
|
126
|
+
if (["__proto__", "constructor", "prototype"].some((p) => parts.includes(p)))
|
|
127
|
+
throw new AgentCliError("CONFIG_INVALID", "Invalid key");
|
|
128
|
+
cursor[parts.at(-1)] = parsed;
|
|
129
|
+
const validated = migrate(updated);
|
|
130
|
+
await saveConfig(validated, context.configPath);
|
|
131
|
+
return validated;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// src/commands/init.ts
|
|
136
|
+
import { z as z2 } from "zod";
|
|
137
|
+
|
|
138
|
+
// src/funding/qr.ts
|
|
139
|
+
import QRCode from "qrcode";
|
|
140
|
+
import { writeFile } from "fs/promises";
|
|
141
|
+
async function renderQr(value, options = {}) {
|
|
142
|
+
if (options.enabled === false) return { qrAscii: null, qrPath: null };
|
|
143
|
+
const qrAscii = await QRCode.toString(value, {
|
|
144
|
+
type: "terminal",
|
|
145
|
+
small: true
|
|
146
|
+
});
|
|
147
|
+
(options.stderr ?? ((value2) => process.stderr.write(value2)))(qrAscii + "\n");
|
|
148
|
+
if (options.path) {
|
|
149
|
+
if (options.path.endsWith(".svg"))
|
|
150
|
+
await writeFile(
|
|
151
|
+
options.path,
|
|
152
|
+
await QRCode.toString(value, { type: "svg" }),
|
|
153
|
+
{ mode: 384 }
|
|
154
|
+
);
|
|
155
|
+
else
|
|
156
|
+
await writeFile(
|
|
157
|
+
options.path,
|
|
158
|
+
await QRCode.toBuffer(value, { type: "png" }),
|
|
159
|
+
{ mode: 384 }
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return { qrAscii, qrPath: options.path ?? null };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/commands/init.ts
|
|
166
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
167
|
+
import { join } from "path";
|
|
168
|
+
|
|
169
|
+
// src/wallet/approval.ts
|
|
170
|
+
import { readFile } from "fs/promises";
|
|
171
|
+
import { ApproveAgentTypes } from "@bloxwap/hyperliquid/api/exchange/approveAgent";
|
|
172
|
+
import { ApproveBuilderFeeTypes } from "@bloxwap/hyperliquid/api/exchange/approveBuilderFee";
|
|
173
|
+
var APPROVAL_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
174
|
+
function approval(network, type, fields, nonce = Date.now()) {
|
|
175
|
+
const types = type === "approveAgent" ? ApproveAgentTypes : ApproveBuilderFeeTypes;
|
|
176
|
+
const action = {
|
|
177
|
+
type,
|
|
178
|
+
signatureChainId: "0xa4b1",
|
|
179
|
+
hyperliquidChain: network === "mainnet" ? "Mainnet" : "Testnet",
|
|
180
|
+
...fields,
|
|
181
|
+
nonce
|
|
182
|
+
};
|
|
183
|
+
const { type: _type, signatureChainId: _chain, ...message } = action;
|
|
184
|
+
void _type;
|
|
185
|
+
void _chain;
|
|
186
|
+
return {
|
|
187
|
+
action,
|
|
188
|
+
typedData: {
|
|
189
|
+
domain: {
|
|
190
|
+
name: "HyperliquidSignTransaction",
|
|
191
|
+
version: "1",
|
|
192
|
+
chainId: 42161,
|
|
193
|
+
verifyingContract: "0x" + "0".repeat(40)
|
|
194
|
+
},
|
|
195
|
+
types,
|
|
196
|
+
primaryType: Object.keys(types)[0],
|
|
197
|
+
message
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function submitApproval(network, pending, signature) {
|
|
202
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
203
|
+
throw new AgentCliError("CONFIG_INVALID", "Expected 65-byte signature");
|
|
204
|
+
const recovery = parseInt(signature.slice(-2), 16);
|
|
205
|
+
const v = recovery < 27 ? recovery + 27 : recovery;
|
|
206
|
+
if (v !== 27 && v !== 28)
|
|
207
|
+
throw new AgentCliError("CONFIG_INVALID", "Invalid signature recovery");
|
|
208
|
+
const response = await jsonRequest(`${hlEndpoint(network)}/exchange`, {
|
|
209
|
+
action: pending.action,
|
|
210
|
+
nonce: pending.action.nonce,
|
|
211
|
+
signature: {
|
|
212
|
+
r: signature.slice(0, 66),
|
|
213
|
+
s: "0x" + signature.slice(66, 130),
|
|
214
|
+
v
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
if (response.status !== "ok") {
|
|
218
|
+
const actionType = String(pending.action.type ?? "approval");
|
|
219
|
+
const rejection = typeof response.response === "string" ? response.response : JSON.stringify(response.response) ?? "Unknown rejection";
|
|
220
|
+
const nonceRejected = /nonce/i.test(rejection);
|
|
221
|
+
throw new AgentCliError(
|
|
222
|
+
"UPSTREAM_ERROR",
|
|
223
|
+
`${actionType} failed: ${rejection}`,
|
|
224
|
+
{ hyperliquidResponse: response, nonceRejected },
|
|
225
|
+
false
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return response;
|
|
229
|
+
}
|
|
230
|
+
function isFreshApproval(createdAt, now = Date.now()) {
|
|
231
|
+
return typeof createdAt === "number" && now >= createdAt && now - createdAt < APPROVAL_MAX_AGE_MS;
|
|
232
|
+
}
|
|
233
|
+
async function resolvePendingApproval(path, create, now = Date.now()) {
|
|
234
|
+
try {
|
|
235
|
+
const stored = JSON.parse(await readFile(path, "utf8"));
|
|
236
|
+
const approval2 = stored.approval?.action ? stored.approval : stored.action ? stored : void 0;
|
|
237
|
+
if (approval2?.action && isFreshApproval(stored.createdAt, now))
|
|
238
|
+
return {
|
|
239
|
+
approval: approval2,
|
|
240
|
+
createdAt: stored.createdAt,
|
|
241
|
+
expiresAt: stored.expiresAt ?? stored.createdAt + APPROVAL_MAX_AGE_MS
|
|
242
|
+
};
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error.code !== "ENOENT") throw error;
|
|
245
|
+
}
|
|
246
|
+
const pending = {
|
|
247
|
+
approval: await create(),
|
|
248
|
+
createdAt: now,
|
|
249
|
+
expiresAt: now + APPROVAL_MAX_AGE_MS
|
|
250
|
+
};
|
|
251
|
+
await atomicWrite(path, JSON.stringify(pending));
|
|
252
|
+
return pending;
|
|
253
|
+
}
|
|
254
|
+
async function submitFreshApproval(network, path, pending, signature, refresh) {
|
|
255
|
+
if (!isFreshApproval(pending.createdAt)) {
|
|
256
|
+
const next = await resolvePendingApproval(path, refresh, Date.now());
|
|
257
|
+
throw new AgentCliError(
|
|
258
|
+
"CONFIG_INVALID",
|
|
259
|
+
"Approval payload expired; sign the regenerated payload",
|
|
260
|
+
{ expiresAt: next.expiresAt, approval: next.approval }
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
return await submitApproval(network, pending.approval, signature);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error instanceof AgentCliError && error.details?.nonceRejected) {
|
|
267
|
+
const next = await resolvePendingApproval(path, refresh, Date.now());
|
|
268
|
+
throw new AgentCliError(
|
|
269
|
+
"CONFIG_INVALID",
|
|
270
|
+
"Approval nonce was rejected; sign the regenerated payload",
|
|
271
|
+
{ expiresAt: next.expiresAt, approval: next.approval }
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
var rejectMasterKey = assertSafeInput;
|
|
278
|
+
async function newApproval(config, type, fields) {
|
|
279
|
+
if (!config.masterAddress)
|
|
280
|
+
throw new AgentCliError("CONFIG_INVALID", "Master address required");
|
|
281
|
+
return approval(
|
|
282
|
+
config.network,
|
|
283
|
+
type,
|
|
284
|
+
fields,
|
|
285
|
+
await nextNonce(config.masterAddress)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/core/referral.ts
|
|
290
|
+
async function ensureReferral(config) {
|
|
291
|
+
if (!config.masterAddress)
|
|
292
|
+
throw new AgentCliError("CONFIG_INVALID", "Master address required");
|
|
293
|
+
const referral = await hlInfo(config.network, "referral", { user: config.masterAddress });
|
|
294
|
+
if (referral.referredBy)
|
|
295
|
+
return {
|
|
296
|
+
referralCode: referral.referredBy.code,
|
|
297
|
+
referralApplied: true
|
|
298
|
+
};
|
|
299
|
+
const signer = await openSigner(config);
|
|
300
|
+
const clients = createClients(
|
|
301
|
+
config.network,
|
|
302
|
+
signer,
|
|
303
|
+
() => nextNonce(signer.address)
|
|
304
|
+
);
|
|
305
|
+
await clients.exchange.setReferrer({ code: config.referralCode });
|
|
306
|
+
return { referralCode: config.referralCode, referralApplied: true };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/commands/init.ts
|
|
310
|
+
register("init", {
|
|
311
|
+
description: "Create agent and emit resumable master-wallet approvals",
|
|
312
|
+
schema: z2.object({
|
|
313
|
+
masterAddress: z2.string().regex(/^0x[0-9a-fA-F]{40}$/).optional(),
|
|
314
|
+
signerType: z2.enum(["privy", "eoa"]).optional(),
|
|
315
|
+
eoaKeyRef: z2.string().regex(/^env:[A-Z_][A-Z0-9_]*$|^file:.+/).optional(),
|
|
316
|
+
validUntil: z2.number().int().optional(),
|
|
317
|
+
signature: z2.string().optional(),
|
|
318
|
+
builderSignature: z2.string().optional(),
|
|
319
|
+
qr: z2.boolean().default(true)
|
|
320
|
+
}),
|
|
321
|
+
execute: async (input, context) => {
|
|
322
|
+
rejectMasterKey(input);
|
|
323
|
+
const config = await loadConfig(context.configPath);
|
|
324
|
+
if (context.paper !== void 0) config.mode = context.paper ? "paper" : "live";
|
|
325
|
+
assertLiveMode(config, "init");
|
|
326
|
+
const signerType = input.signerType ?? config.signerType;
|
|
327
|
+
if (config.agentAddress && signerType !== config.signerType)
|
|
328
|
+
throw new AgentCliError(
|
|
329
|
+
"CONFIG_INVALID",
|
|
330
|
+
"Initialized signer type differs; use a separate config for another agent"
|
|
331
|
+
);
|
|
332
|
+
config.signerType = signerType;
|
|
333
|
+
const stored = await readStoredConfig(context.configPath);
|
|
334
|
+
const requested = context.network ?? config.network;
|
|
335
|
+
if (stored?.approvedNetwork && requested !== stored.approvedNetwork)
|
|
336
|
+
throw new AgentCliError(
|
|
337
|
+
"CONFIG_INVALID",
|
|
338
|
+
"Network does not match the approved network",
|
|
339
|
+
{ network: requested, approvedNetwork: stored.approvedNetwork }
|
|
340
|
+
);
|
|
341
|
+
if (stored?.agentAddress && !stored.approvedNetwork && requested !== stored.network)
|
|
342
|
+
throw new AgentCliError(
|
|
343
|
+
"CONFIG_INVALID",
|
|
344
|
+
"Initialized wallet network differs; use a separate config for another network"
|
|
345
|
+
);
|
|
346
|
+
if (config.agentAddress && config.masterAddress && input.masterAddress && input.masterAddress.toLowerCase() !== config.masterAddress.toLowerCase())
|
|
347
|
+
throw new AgentCliError(
|
|
348
|
+
"CONFIG_INVALID",
|
|
349
|
+
"Initialized master address differs; use a separate config for another account"
|
|
350
|
+
);
|
|
351
|
+
if (context.network) config.network = context.network;
|
|
352
|
+
if (!config.approvedNetwork) config.approvedNetwork = config.network;
|
|
353
|
+
else if (config.approvedNetwork !== config.network)
|
|
354
|
+
throw new AgentCliError(
|
|
355
|
+
"CONFIG_INVALID",
|
|
356
|
+
"Network does not match the approved network",
|
|
357
|
+
{ network: config.network, approvedNetwork: config.approvedNetwork }
|
|
358
|
+
);
|
|
359
|
+
config.masterAddress = input.masterAddress ?? config.masterAddress;
|
|
360
|
+
if (!config.masterAddress)
|
|
361
|
+
throw new AgentCliError("CONFIG_INVALID", "Provide masterAddress");
|
|
362
|
+
const expiry = input.validUntil ?? config.agentExpiry ?? Date.now() + 179 * 864e5;
|
|
363
|
+
if (expiry <= Date.now() || expiry > Date.now() + 180 * 864e5)
|
|
364
|
+
throw new AgentCliError(
|
|
365
|
+
"CONFIG_INVALID",
|
|
366
|
+
"Expiry must be within 180 days"
|
|
367
|
+
);
|
|
368
|
+
config.agentExpiry = expiry;
|
|
369
|
+
let policyId;
|
|
370
|
+
if (signerType === "eoa") {
|
|
371
|
+
await prepareEoaAgent(
|
|
372
|
+
config,
|
|
373
|
+
context.configPath,
|
|
374
|
+
input.eoaKeyRef
|
|
375
|
+
);
|
|
376
|
+
} else {
|
|
377
|
+
const client = await privyClient(config);
|
|
378
|
+
policyId = await applyPolicy(config, client, context.configPath);
|
|
379
|
+
await createAgent(config, client, context.configPath, policyId);
|
|
380
|
+
}
|
|
381
|
+
if (config.approved && !config.revoked)
|
|
382
|
+
return {
|
|
383
|
+
...config,
|
|
384
|
+
policyId: config.policyId ?? null,
|
|
385
|
+
expiry: config.agentExpiry,
|
|
386
|
+
...await ensureReferral(config)
|
|
387
|
+
};
|
|
388
|
+
const path = join(
|
|
389
|
+
stateDir(),
|
|
390
|
+
`approval-${config.network}-${config.agentAddress}.json`
|
|
391
|
+
);
|
|
392
|
+
const createApprovals = async () => {
|
|
393
|
+
const created = {
|
|
394
|
+
agent: await newApproval(config, "approveAgent", {
|
|
395
|
+
agentAddress: config.agentAddress,
|
|
396
|
+
agentName: `${config.agentName} valid_until ${expiry}`
|
|
397
|
+
})
|
|
398
|
+
};
|
|
399
|
+
if (config.builder.fee > 0 && config.builder.address)
|
|
400
|
+
created.builder = await newApproval(config, "approveBuilderFee", {
|
|
401
|
+
builder: config.builder.address,
|
|
402
|
+
maxFeeRate: `${config.builder.fee / 1e3}%`
|
|
403
|
+
});
|
|
404
|
+
return created;
|
|
405
|
+
};
|
|
406
|
+
let pending;
|
|
407
|
+
let regenerated = false;
|
|
408
|
+
try {
|
|
409
|
+
const stored2 = JSON.parse(await readFile2(path, "utf8"));
|
|
410
|
+
if (stored2.agent?.action && isFreshApproval(stored2.createdAt))
|
|
411
|
+
pending = {
|
|
412
|
+
agent: stored2.agent,
|
|
413
|
+
builder: stored2.builder,
|
|
414
|
+
createdAt: stored2.createdAt,
|
|
415
|
+
expiresAt: stored2.expiresAt ?? stored2.createdAt + APPROVAL_MAX_AGE_MS
|
|
416
|
+
};
|
|
417
|
+
else throw Object.assign(new Error("stale"), { code: "ESTALE" });
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (error.code !== "ENOENT" && error.code !== "ESTALE")
|
|
420
|
+
throw error;
|
|
421
|
+
regenerated = error.code === "ESTALE";
|
|
422
|
+
const created = await createApprovals();
|
|
423
|
+
pending = {
|
|
424
|
+
...created,
|
|
425
|
+
createdAt: Date.now(),
|
|
426
|
+
expiresAt: Date.now() + APPROVAL_MAX_AGE_MS
|
|
427
|
+
};
|
|
428
|
+
await atomicWrite(path, JSON.stringify(pending));
|
|
429
|
+
}
|
|
430
|
+
if ((input.signature || input.builderSignature) && regenerated)
|
|
431
|
+
throw new AgentCliError(
|
|
432
|
+
"CONFIG_INVALID",
|
|
433
|
+
"Approval payload expired; sign the regenerated payload",
|
|
434
|
+
{ expiresAt: pending.expiresAt, approvals: pending }
|
|
435
|
+
);
|
|
436
|
+
const submit = async (approval2, signature) => {
|
|
437
|
+
try {
|
|
438
|
+
await submitApproval(config.network, approval2, signature);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (error instanceof AgentCliError && error.details?.nonceRejected) {
|
|
441
|
+
const created = await createApprovals();
|
|
442
|
+
pending = {
|
|
443
|
+
...created,
|
|
444
|
+
createdAt: Date.now(),
|
|
445
|
+
expiresAt: Date.now() + APPROVAL_MAX_AGE_MS
|
|
446
|
+
};
|
|
447
|
+
await atomicWrite(path, JSON.stringify(pending));
|
|
448
|
+
throw new AgentCliError(
|
|
449
|
+
"CONFIG_INVALID",
|
|
450
|
+
"Approval nonce was rejected; sign the regenerated payload",
|
|
451
|
+
{ expiresAt: pending.expiresAt, approvals: pending }
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
if (input.signature) await submit(pending.agent, input.signature);
|
|
458
|
+
if (input.builderSignature && pending.builder)
|
|
459
|
+
await submit(pending.builder, input.builderSignature);
|
|
460
|
+
const agents = await hlInfo(
|
|
461
|
+
config.network,
|
|
462
|
+
"extraAgents",
|
|
463
|
+
{ user: config.masterAddress }
|
|
464
|
+
);
|
|
465
|
+
config.approved = agents.some(
|
|
466
|
+
(agent) => agent.address.toLowerCase() === config.agentAddress?.toLowerCase() && agent.validUntil >= expiry
|
|
467
|
+
);
|
|
468
|
+
if (config.builder.fee > 0 && config.builder.address) {
|
|
469
|
+
const max = await hlInfo(config.network, "maxBuilderFee", {
|
|
470
|
+
user: config.masterAddress,
|
|
471
|
+
builder: config.builder.address
|
|
472
|
+
});
|
|
473
|
+
config.approved = config.approved && max >= config.builder.fee;
|
|
474
|
+
}
|
|
475
|
+
config.revoked = false;
|
|
476
|
+
await saveConfig(config, context.configPath);
|
|
477
|
+
const deeplink = `ethereum:${config.masterAddress}?request=${encodeURIComponent(JSON.stringify(pending.agent.typedData))}`;
|
|
478
|
+
return {
|
|
479
|
+
masterAddress: config.masterAddress,
|
|
480
|
+
agentAddress: config.agentAddress,
|
|
481
|
+
signerType,
|
|
482
|
+
expiry,
|
|
483
|
+
policyId: policyId ?? null,
|
|
484
|
+
approved: config.approved,
|
|
485
|
+
...config.approved ? await ensureReferral(config) : { referralCode: config.referralCode, referralApplied: false },
|
|
486
|
+
requiresSignature: !config.approved,
|
|
487
|
+
approvalExpiresAt: pending.expiresAt,
|
|
488
|
+
approvals: pending,
|
|
489
|
+
deeplink,
|
|
490
|
+
...await renderQr(deeplink, { enabled: input.qr })
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// src/core/account.ts
|
|
496
|
+
function collateralBalance(state) {
|
|
497
|
+
const equity = parseUsdc(state.marginSummary.accountValue, 8);
|
|
498
|
+
const pnl = (state.assetPositions ?? []).reduce(
|
|
499
|
+
(sum, { position }) => sum + parseUsdc(position.unrealizedPnl, 8),
|
|
500
|
+
0n
|
|
501
|
+
);
|
|
502
|
+
return formatUsdc(equity - pnl, 8);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/commands/fund.ts
|
|
506
|
+
import { z as z3 } from "zod";
|
|
507
|
+
|
|
508
|
+
// src/funding/signatures.ts
|
|
509
|
+
function signatureSteps(deposit) {
|
|
510
|
+
return deposit.quote.steps.flatMap(
|
|
511
|
+
(step) => step.kind === "signature" ? step.items.map((item, index) => ({
|
|
512
|
+
id: `${step.id}:${index}`,
|
|
513
|
+
requiresSignature: !deposit.completedSteps.includes(
|
|
514
|
+
`${step.id}:${index}`
|
|
515
|
+
),
|
|
516
|
+
payload: signaturePayload(item.data.sign),
|
|
517
|
+
deeplink: `ethereum:${deposit.recipient}?request=${encodeURIComponent(JSON.stringify(signaturePayload(item.data.sign)))}`,
|
|
518
|
+
post: item.data.post
|
|
519
|
+
})) : []
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
async function submitDepositSignature(depositId, stepId, signature, options = {}) {
|
|
523
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
524
|
+
throw new AgentCliError("CONFIG_INVALID", "Expected wallet signature");
|
|
525
|
+
const deposits = await readDeposits(options.path);
|
|
526
|
+
const deposit = deposits.find((d) => d.id === depositId);
|
|
527
|
+
if (!deposit) throw new AgentCliError("CONFIG_INVALID", "Unknown deposit");
|
|
528
|
+
const pending = signatureSteps(deposit).filter((s) => s.requiresSignature);
|
|
529
|
+
if (!pending.length) return { complete: true };
|
|
530
|
+
const step = pending[0];
|
|
531
|
+
if (step.id !== stepId)
|
|
532
|
+
throw new AgentCliError(
|
|
533
|
+
"CONFIG_INVALID",
|
|
534
|
+
"Submit signatures in step order",
|
|
535
|
+
{ nextStep: step.id }
|
|
536
|
+
);
|
|
537
|
+
const post = step.post;
|
|
538
|
+
if (!post || post.method !== "POST" || !post.endpoint.startsWith("/") || post.endpoint.startsWith("//"))
|
|
539
|
+
throw new AgentCliError(
|
|
540
|
+
"UPSTREAM_ERROR",
|
|
541
|
+
"Invalid signature submission endpoint"
|
|
542
|
+
);
|
|
543
|
+
const base = options.base ?? (deposit.network === "testnet" ? "https://api.testnets.relay.link" : "https://api.relay.link");
|
|
544
|
+
const url = new URL(post.endpoint, base);
|
|
545
|
+
if (url.origin !== new URL(base).origin)
|
|
546
|
+
throw new AgentCliError(
|
|
547
|
+
"POLICY_REJECTED",
|
|
548
|
+
"Untrusted signature destination"
|
|
549
|
+
);
|
|
550
|
+
url.searchParams.set("signature", signature);
|
|
551
|
+
await (options.submit ?? jsonRequest)(url.toString(), post.body);
|
|
552
|
+
let steps = signatureSteps(deposit);
|
|
553
|
+
await updateDeposits(options.path, (fresh) => {
|
|
554
|
+
const current = fresh.find((item) => item.id === depositId);
|
|
555
|
+
if (!current) throw new AgentCliError("CONFIG_INVALID", "Unknown deposit");
|
|
556
|
+
if (!current.completedSteps.includes(step.id))
|
|
557
|
+
current.completedSteps.push(step.id);
|
|
558
|
+
steps = signatureSteps(current);
|
|
559
|
+
});
|
|
560
|
+
return { depositId, steps };
|
|
561
|
+
}
|
|
562
|
+
function signaturePayload(value) {
|
|
563
|
+
if (!value || typeof value !== "object") return value;
|
|
564
|
+
const sign = value;
|
|
565
|
+
if (sign.signatureKind === "eip712")
|
|
566
|
+
return {
|
|
567
|
+
domain: sign.domain,
|
|
568
|
+
types: sign.types,
|
|
569
|
+
primaryType: sign.primaryType,
|
|
570
|
+
message: sign.message ?? sign.value
|
|
571
|
+
};
|
|
572
|
+
return value;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/core/builder.ts
|
|
576
|
+
async function approvedBuilder(config, fetchMaximum = (user, builder) => hlInfo(config.network, "maxBuilderFee", { user, builder })) {
|
|
577
|
+
if (config.builder.fee === 0) return void 0;
|
|
578
|
+
if (!config.builder.address) {
|
|
579
|
+
if (config.builder.fee)
|
|
580
|
+
throw new AgentCliError(
|
|
581
|
+
"CONFIG_INVALID",
|
|
582
|
+
"Builder address is required for a fee"
|
|
583
|
+
);
|
|
584
|
+
return void 0;
|
|
585
|
+
}
|
|
586
|
+
if (!config.masterAddress)
|
|
587
|
+
throw new AgentCliError("AGENT_NOT_APPROVED", "Master address missing");
|
|
588
|
+
const maximum = await fetchMaximum(
|
|
589
|
+
config.masterAddress,
|
|
590
|
+
config.builder.address
|
|
591
|
+
);
|
|
592
|
+
if (config.builder.fee > maximum)
|
|
593
|
+
throw new AgentCliError(
|
|
594
|
+
"AGENT_NOT_APPROVED",
|
|
595
|
+
"Approve configured builder fee with master wallet",
|
|
596
|
+
{ approvedMax: maximum, requested: config.builder.fee }
|
|
597
|
+
);
|
|
598
|
+
return { b: config.builder.address.toLowerCase(), f: config.builder.fee };
|
|
599
|
+
}
|
|
600
|
+
var builderRate = (config) => `${config.builder.fee / 1e3}%`;
|
|
601
|
+
|
|
602
|
+
// src/commands/fund.ts
|
|
603
|
+
var fundSchema = z3.object({
|
|
604
|
+
originChain: z3.number().int().positive().default(42161),
|
|
605
|
+
originCurrency: z3.string().default("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"),
|
|
606
|
+
originDecimals: z3.number().int().min(0).max(30).default(6),
|
|
607
|
+
amount: z3.string().default("10"),
|
|
608
|
+
qr: z3.boolean().default(true),
|
|
609
|
+
qrPath: z3.string().optional()
|
|
610
|
+
});
|
|
611
|
+
async function fund(config, input, dependencies = {}) {
|
|
612
|
+
if (!config.masterAddress)
|
|
613
|
+
throw new AgentCliError("AGENT_NOT_APPROVED", "Initialize master address");
|
|
614
|
+
const amount = parseUsdc(input.amount, input.originDecimals);
|
|
615
|
+
if (amount <= 0n)
|
|
616
|
+
throw new AgentCliError("INVALID_SIZE", "Positive funding amount required");
|
|
617
|
+
const route = JSON.stringify([
|
|
618
|
+
config.network,
|
|
619
|
+
config.masterAddress,
|
|
620
|
+
input.originChain,
|
|
621
|
+
input.originCurrency,
|
|
622
|
+
amount.toString()
|
|
623
|
+
]);
|
|
624
|
+
const deposits = await readDeposits(dependencies.path);
|
|
625
|
+
let deposit = deposits.find(
|
|
626
|
+
(d) => d.route === route && d.expiresAt > Date.now() && d.state === "awaiting_funds"
|
|
627
|
+
);
|
|
628
|
+
if (!deposit) {
|
|
629
|
+
const quote = await (dependencies.relay ?? new RelayClient(
|
|
630
|
+
config.network === "testnet" ? "https://api.testnets.relay.link" : "https://api.relay.link"
|
|
631
|
+
)).quote({
|
|
632
|
+
user: config.masterAddress,
|
|
633
|
+
recipient: config.masterAddress,
|
|
634
|
+
originChainId: input.originChain,
|
|
635
|
+
originCurrency: input.originCurrency,
|
|
636
|
+
amount: amount.toString(),
|
|
637
|
+
destinationChainId: 1337,
|
|
638
|
+
destinationCurrency: PERPS_USDC,
|
|
639
|
+
useDepositAddress: true,
|
|
640
|
+
tradeType: "EXACT_INPUT"
|
|
641
|
+
});
|
|
642
|
+
const parsed2 = parseQuote(quote);
|
|
643
|
+
const quotedDecimals = quote.details.currencyIn?.currency?.decimals;
|
|
644
|
+
const quotedAmount = quote.details.currencyIn?.amount;
|
|
645
|
+
if (typeof quotedDecimals !== "number")
|
|
646
|
+
throw new AgentCliError(
|
|
647
|
+
"CONFIG_INVALID",
|
|
648
|
+
"Quote did not include origin token decimals"
|
|
649
|
+
);
|
|
650
|
+
if (quotedDecimals !== input.originDecimals)
|
|
651
|
+
throw new AgentCliError(
|
|
652
|
+
"CONFIG_INVALID",
|
|
653
|
+
"Origin decimals do not match the quoted token",
|
|
654
|
+
{ originDecimals: input.originDecimals, quoteDecimals: quotedDecimals }
|
|
655
|
+
);
|
|
656
|
+
if (quotedAmount !== amount.toString())
|
|
657
|
+
throw new AgentCliError(
|
|
658
|
+
"CONFIG_INVALID",
|
|
659
|
+
"Quote amount does not match the requested amount",
|
|
660
|
+
{ requested: amount.toString(), quoted: quotedAmount ?? null }
|
|
661
|
+
);
|
|
662
|
+
if (amount < BigInt(parsed2.minAmount))
|
|
663
|
+
throw new AgentCliError("INVALID_SIZE", "Below minimum deposit", {
|
|
664
|
+
minimum: parsed2.minAmount
|
|
665
|
+
});
|
|
666
|
+
const baseline = dependencies.baseline ? await dependencies.baseline() : collateralBalance(
|
|
667
|
+
await hlInfo(config.network, "clearinghouseState", {
|
|
668
|
+
user: config.masterAddress
|
|
669
|
+
})
|
|
670
|
+
);
|
|
671
|
+
deposit = {
|
|
672
|
+
id: parsed2.depositId,
|
|
673
|
+
network: config.network,
|
|
674
|
+
recipient: config.masterAddress,
|
|
675
|
+
route,
|
|
676
|
+
quote,
|
|
677
|
+
expiresAt: parsed2.expiresAt,
|
|
678
|
+
state: "awaiting_funds",
|
|
679
|
+
baseline,
|
|
680
|
+
createdAt: Date.now(),
|
|
681
|
+
completedSteps: []
|
|
682
|
+
};
|
|
683
|
+
await updateDeposits(dependencies.path, (fresh) => {
|
|
684
|
+
const raced = fresh.find(
|
|
685
|
+
(item) => item.route === route && item.expiresAt > Date.now() && item.state === "awaiting_funds"
|
|
686
|
+
);
|
|
687
|
+
if (raced) deposit = raced;
|
|
688
|
+
else fresh.push(deposit);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
const parsed = parseQuote(deposit.quote);
|
|
692
|
+
const received = formatUsdc(
|
|
693
|
+
BigInt(parsed.expectedReceived),
|
|
694
|
+
parsed.receivedDecimals
|
|
695
|
+
);
|
|
696
|
+
return {
|
|
697
|
+
...parsed,
|
|
698
|
+
expectedReceived: parseUsdc(received, 8).toString(),
|
|
699
|
+
expectedReceivedHuman: received,
|
|
700
|
+
originChain: input.originChain,
|
|
701
|
+
originCurrency: input.originCurrency,
|
|
702
|
+
minDeposit: formatUsdc(BigInt(parsed.minAmount), input.originDecimals),
|
|
703
|
+
builderFeeRate: builderRate(config),
|
|
704
|
+
builderAddress: config.builder.address,
|
|
705
|
+
state: deposit.state,
|
|
706
|
+
signatureSteps: signatureSteps(deposit),
|
|
707
|
+
...await (dependencies.qr ?? renderQr)(parsed.depositAddress, {
|
|
708
|
+
enabled: input.qr,
|
|
709
|
+
path: input.qrPath
|
|
710
|
+
})
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
register("fund", {
|
|
714
|
+
description: "Quote funding with address, QR and transparent fees",
|
|
715
|
+
schema: fundSchema,
|
|
716
|
+
execute: async (input, context) => {
|
|
717
|
+
const config = await loadConfig(context.configPath);
|
|
718
|
+
if (context.paper !== void 0) config.mode = context.paper ? "paper" : "live";
|
|
719
|
+
assertLiveMode(config, "fund");
|
|
720
|
+
if (context.network) config.network = context.network;
|
|
721
|
+
return fund(config, input);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// src/commands/deposit.ts
|
|
726
|
+
import { z as z4 } from "zod";
|
|
727
|
+
register("deposit", {
|
|
728
|
+
description: "Resume deposit steps or submit a master-wallet signature",
|
|
729
|
+
schema: z4.object({
|
|
730
|
+
depositId: z4.string(),
|
|
731
|
+
stepId: z4.string().optional(),
|
|
732
|
+
signature: z4.string().optional(),
|
|
733
|
+
abandon: z4.boolean().default(false)
|
|
734
|
+
}),
|
|
735
|
+
execute: async (input) => {
|
|
736
|
+
if (input.abandon) return abandonDeposit(input.depositId);
|
|
737
|
+
if (input.signature && input.stepId)
|
|
738
|
+
return submitDepositSignature(
|
|
739
|
+
input.depositId,
|
|
740
|
+
input.stepId,
|
|
741
|
+
input.signature
|
|
742
|
+
);
|
|
743
|
+
const deposit = (await readDeposits()).find(
|
|
744
|
+
(d) => d.id === input.depositId
|
|
745
|
+
);
|
|
746
|
+
if (!deposit) throw new AgentCliError("CONFIG_INVALID", "Unknown deposit");
|
|
747
|
+
return { depositId: deposit.id, steps: signatureSteps(deposit) };
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// src/runner/health.ts
|
|
752
|
+
function health(runner, now = Date.now()) {
|
|
753
|
+
const subscriptions = runner.subscriptions;
|
|
754
|
+
return {
|
|
755
|
+
pid: String(runner.info.pid),
|
|
756
|
+
uptimeSeconds: String(Math.max(0, (now - runner.info.startedAt) / 1e3)),
|
|
757
|
+
wsState: subscriptions?.state ?? "disconnected",
|
|
758
|
+
wsStale: !subscriptions || now - subscriptions.lastMessage > 9e4,
|
|
759
|
+
lastFillTime: subscriptions?.lastFill ?? null,
|
|
760
|
+
signingSession: runner.signer ? "ready" : runner.config.mode === "paper" ? "simulated" : "unavailable"
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
async function runnerHealth(mode = "live") {
|
|
764
|
+
if (activeRunner && activeRunner.config.mode === mode) return health(activeRunner);
|
|
765
|
+
const info = await runningInfo(runnerPidPath(mode));
|
|
766
|
+
return info ? {
|
|
767
|
+
pid: String(info.pid),
|
|
768
|
+
uptimeSeconds: String((Date.now() - info.startedAt) / 1e3),
|
|
769
|
+
wsState: "unknown",
|
|
770
|
+
signingSession: mode === "paper" ? "simulated" : "unknown"
|
|
771
|
+
} : null;
|
|
772
|
+
}
|
|
773
|
+
function startHeartbeat(runner, subscriptions) {
|
|
774
|
+
runner.subscriptions = subscriptions;
|
|
775
|
+
let queue = Promise.resolve();
|
|
776
|
+
const tick = () => {
|
|
777
|
+
queue = queue.then(async () => {
|
|
778
|
+
await events.append("runner_state", health(runner));
|
|
779
|
+
await expiryWarnings(runner.config);
|
|
780
|
+
if (runner.config.masterAddress) {
|
|
781
|
+
const state = await hlInfo(runner.config.network, "clearinghouseState", {
|
|
782
|
+
user: runner.config.masterAddress
|
|
783
|
+
});
|
|
784
|
+
await pollDeposits(
|
|
785
|
+
runner.config.network,
|
|
786
|
+
runner.config.masterAddress,
|
|
787
|
+
collateralBalance(state),
|
|
788
|
+
{
|
|
789
|
+
emit: async (deposit) => {
|
|
790
|
+
await events.append("deposit", {
|
|
791
|
+
id: deposit.id,
|
|
792
|
+
state: deposit.state
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}).catch(async (error) => {
|
|
799
|
+
await events.append("error", normalizeError(error).toJSON());
|
|
800
|
+
});
|
|
801
|
+
};
|
|
802
|
+
tick();
|
|
803
|
+
const timer = setInterval(tick, 3e4);
|
|
804
|
+
runner.onShutdown(async () => {
|
|
805
|
+
clearInterval(timer);
|
|
806
|
+
await queue;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/commands/read.ts
|
|
811
|
+
import { z as z5 } from "zod";
|
|
812
|
+
function accountBalance(state) {
|
|
813
|
+
return {
|
|
814
|
+
accountValue: state.marginSummary.accountValue,
|
|
815
|
+
accountValueUnits: parseUsdc(
|
|
816
|
+
state.marginSummary.accountValue,
|
|
817
|
+
8
|
|
818
|
+
).toString(),
|
|
819
|
+
withdrawable: state.withdrawable,
|
|
820
|
+
marginUsed: state.marginSummary.totalMarginUsed
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function positions(state) {
|
|
824
|
+
return state.assetPositions.map(({ position: p }) => ({
|
|
825
|
+
coin: p.coin,
|
|
826
|
+
size: p.szi,
|
|
827
|
+
entryPrice: p.entryPx,
|
|
828
|
+
unrealizedPnl: p.unrealizedPnl,
|
|
829
|
+
liquidationPrice: p.liquidationPx,
|
|
830
|
+
leverage: String(p.leverage.value),
|
|
831
|
+
marginMode: p.leverage.type
|
|
832
|
+
}));
|
|
833
|
+
}
|
|
834
|
+
function expiryStatus(config, now = Date.now()) {
|
|
835
|
+
const days = config.agentExpiry === void 0 ? null : Math.max(0, (config.agentExpiry - now) / 864e5);
|
|
836
|
+
return {
|
|
837
|
+
expiry: config.agentExpiry ?? null,
|
|
838
|
+
daysRemaining: days === null ? null : String(days),
|
|
839
|
+
warnings: days === null ? [] : config.expiryWarningDays.filter((threshold) => days <= threshold).map((threshold) => `Agent expires within ${threshold} days`)
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async function paperRead(name, config) {
|
|
843
|
+
const store = new PaperStore();
|
|
844
|
+
const data = paperData();
|
|
845
|
+
const [state, marks] = await Promise.all([store.load(), data.marks()]);
|
|
846
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
847
|
+
for (const coin of new Set(state.positions.map((p) => p.coin)))
|
|
848
|
+
resolved.set(coin, await data.maxLeverageOf(coin));
|
|
849
|
+
const maxLeverageOf = (coin) => resolved.get(coin) ?? 1;
|
|
850
|
+
if (name === "orders") return paperOpenOrders(state);
|
|
851
|
+
const account = paperAccountState(state, marks, maxLeverageOf);
|
|
852
|
+
if (name === "balance") return accountBalance(account);
|
|
853
|
+
if (name === "positions") return positions(account);
|
|
854
|
+
return {
|
|
855
|
+
balance: accountBalance(account),
|
|
856
|
+
positions: positions(account),
|
|
857
|
+
mode: "paper",
|
|
858
|
+
dataNetwork: paperDataNetwork,
|
|
859
|
+
paper: {
|
|
860
|
+
startingBalance: state.startingBalance,
|
|
861
|
+
realizedPnl: state.realizedPnl,
|
|
862
|
+
feesPaid: state.feesPaid,
|
|
863
|
+
fundingPaid: state.fundingPaid,
|
|
864
|
+
openOrders: state.orders.length,
|
|
865
|
+
liquidations: state.liquidations
|
|
866
|
+
},
|
|
867
|
+
policy: {
|
|
868
|
+
id: config.policyId ?? null,
|
|
869
|
+
version: String(config.policyVersion ?? 0)
|
|
870
|
+
},
|
|
871
|
+
deposits: [],
|
|
872
|
+
runner: await runnerHealth("paper")
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async function readCommand(name, config) {
|
|
876
|
+
if (config.mode === "paper") return paperRead(name, config);
|
|
877
|
+
if (!config.masterAddress)
|
|
878
|
+
throw new AgentCliError("AGENT_NOT_APPROVED", "Initialize master address");
|
|
879
|
+
try {
|
|
880
|
+
if (name === "orders")
|
|
881
|
+
return (await hlInfo(config.network, "frontendOpenOrders", {
|
|
882
|
+
user: config.masterAddress
|
|
883
|
+
})).map(
|
|
884
|
+
(order) => Object.fromEntries(
|
|
885
|
+
Object.entries(order).map(([key, value]) => [
|
|
886
|
+
key,
|
|
887
|
+
typeof value === "number" ? String(value) : value
|
|
888
|
+
])
|
|
889
|
+
)
|
|
890
|
+
);
|
|
891
|
+
const state = await hlInfo(
|
|
892
|
+
config.network,
|
|
893
|
+
"clearinghouseState",
|
|
894
|
+
{ user: config.masterAddress }
|
|
895
|
+
);
|
|
896
|
+
if (name === "balance") return accountBalance(state);
|
|
897
|
+
if (name === "positions") return positions(state);
|
|
898
|
+
const [agents, deposits, maxBuilderFee] = await Promise.all([
|
|
899
|
+
hlInfo(
|
|
900
|
+
config.network,
|
|
901
|
+
"extraAgents",
|
|
902
|
+
{ user: config.masterAddress }
|
|
903
|
+
),
|
|
904
|
+
pollDeposits(
|
|
905
|
+
config.network,
|
|
906
|
+
config.masterAddress,
|
|
907
|
+
collateralBalance(state),
|
|
908
|
+
{
|
|
909
|
+
emit: activeRunner ? async (deposit) => {
|
|
910
|
+
await events.append("deposit", {
|
|
911
|
+
id: deposit.id,
|
|
912
|
+
state: deposit.state
|
|
913
|
+
});
|
|
914
|
+
} : void 0
|
|
915
|
+
}
|
|
916
|
+
),
|
|
917
|
+
config.builder.fee > 0 && config.builder.address ? hlInfo(config.network, "maxBuilderFee", {
|
|
918
|
+
user: config.masterAddress,
|
|
919
|
+
builder: config.builder.address
|
|
920
|
+
}) : Promise.resolve(0)
|
|
921
|
+
]);
|
|
922
|
+
return {
|
|
923
|
+
balance: accountBalance(state),
|
|
924
|
+
positions: positions(state),
|
|
925
|
+
agent: {
|
|
926
|
+
address: config.agentAddress,
|
|
927
|
+
approved: agents.some(
|
|
928
|
+
(a) => a.address.toLowerCase() === config.agentAddress?.toLowerCase()
|
|
929
|
+
),
|
|
930
|
+
revoked: config.revoked,
|
|
931
|
+
...expiryStatus(config)
|
|
932
|
+
},
|
|
933
|
+
policy: {
|
|
934
|
+
id: config.policyId ?? null,
|
|
935
|
+
version: String(config.policyVersion ?? 0)
|
|
936
|
+
},
|
|
937
|
+
deposits: deposits.map((d) => ({ id: d.id, state: d.state })),
|
|
938
|
+
approvedMaxBuilderFee: String(maxBuilderFee),
|
|
939
|
+
runner: await runnerHealth()
|
|
940
|
+
};
|
|
941
|
+
} catch (error) {
|
|
942
|
+
throw mapHyperliquidError(error);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async function commandConfig(context) {
|
|
946
|
+
const config = await loadConfig(context.configPath);
|
|
947
|
+
if (context.network) config.network = context.network;
|
|
948
|
+
if (context.paper !== void 0) config.mode = context.paper ? "paper" : "live";
|
|
949
|
+
return config;
|
|
950
|
+
}
|
|
951
|
+
for (const name of ["status", "balance", "positions", "orders"])
|
|
952
|
+
register(name, {
|
|
953
|
+
description: `Read account ${name}`,
|
|
954
|
+
schema: z5.object({}),
|
|
955
|
+
execute: async (_input, context) => readCommand(name, await commandConfig(context))
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
// src/commands/order.ts
|
|
959
|
+
import { z as z6 } from "zod";
|
|
960
|
+
var orderSchema = z6.object({
|
|
961
|
+
coin: z6.string(),
|
|
962
|
+
side: z6.enum(["buy", "sell"]),
|
|
963
|
+
size: z6.string(),
|
|
964
|
+
price: z6.string().optional(),
|
|
965
|
+
type: z6.enum(["limit", "market", "tp", "sl"]).default("limit"),
|
|
966
|
+
tif: z6.enum(["Gtc", "Ioc", "Alo"]).default("Gtc"),
|
|
967
|
+
reduceOnly: z6.boolean().default(false),
|
|
968
|
+
cloid: z6.string().regex(/^0x[0-9a-fA-F]{32}$/).optional(),
|
|
969
|
+
triggerPrice: z6.string().optional(),
|
|
970
|
+
slippage: z6.number().min(0).max(0.1).default(0.01),
|
|
971
|
+
intent: z6.string().optional()
|
|
972
|
+
});
|
|
973
|
+
async function prepareOrder(config, input, services) {
|
|
974
|
+
const market = await services.market(input.coin);
|
|
975
|
+
const size = roundSize(input.size, market.szDecimals);
|
|
976
|
+
let price = input.price;
|
|
977
|
+
if (input.type === "market" || (input.type === "tp" || input.type === "sl") && !price) {
|
|
978
|
+
const mid = (await services.mids())[input.coin];
|
|
979
|
+
if (!mid) throw new AgentCliError("UNKNOWN_MARKET", "Missing market price");
|
|
980
|
+
price = boundedMarketPrice(
|
|
981
|
+
mid,
|
|
982
|
+
input.slippage,
|
|
983
|
+
input.side,
|
|
984
|
+
market.szDecimals
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
if (!price) throw new AgentCliError("INVALID_PRICE", "Provide limit price");
|
|
988
|
+
price = roundPrice(price, market.szDecimals);
|
|
989
|
+
await checkRisk(
|
|
990
|
+
config,
|
|
991
|
+
{
|
|
992
|
+
coin: input.coin,
|
|
993
|
+
size,
|
|
994
|
+
price,
|
|
995
|
+
side: input.side,
|
|
996
|
+
leverage: 1,
|
|
997
|
+
reduceOnly: input.reduceOnly
|
|
998
|
+
},
|
|
999
|
+
services
|
|
1000
|
+
);
|
|
1001
|
+
let type = {
|
|
1002
|
+
limit: { tif: input.type === "market" ? "Ioc" : input.tif }
|
|
1003
|
+
};
|
|
1004
|
+
if (input.type === "tp" || input.type === "sl") {
|
|
1005
|
+
if (!input.triggerPrice)
|
|
1006
|
+
throw new AgentCliError("INVALID_PRICE", "Trigger price required");
|
|
1007
|
+
type = {
|
|
1008
|
+
trigger: {
|
|
1009
|
+
isMarket: !input.price,
|
|
1010
|
+
triggerPx: roundPrice(input.triggerPrice, market.szDecimals),
|
|
1011
|
+
tpsl: input.type
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
a: market.asset,
|
|
1017
|
+
b: input.side === "buy",
|
|
1018
|
+
p: price,
|
|
1019
|
+
s: size,
|
|
1020
|
+
r: input.reduceOnly,
|
|
1021
|
+
t: type,
|
|
1022
|
+
c: input.cloid
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function annotate(error, cloid) {
|
|
1026
|
+
if (error instanceof AgentCliError) {
|
|
1027
|
+
const details = error.details && typeof error.details === "object" && !Array.isArray(error.details) ? { ...error.details, cloid } : { cloid };
|
|
1028
|
+
return new AgentCliError(error.code, error.message, details, error.retryable);
|
|
1029
|
+
}
|
|
1030
|
+
return new AgentCliError(
|
|
1031
|
+
"INTERNAL",
|
|
1032
|
+
error instanceof Error ? error.message : String(error),
|
|
1033
|
+
{ cloid }
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
async function placeOrder(config, input, services) {
|
|
1037
|
+
const cloid = input.cloid ?? newCloid();
|
|
1038
|
+
const orderInput = { ...input, cloid };
|
|
1039
|
+
try {
|
|
1040
|
+
return await withTradeLock(config, async () => {
|
|
1041
|
+
await assertCanTrade(config, { reduceOnly: orderInput.reduceOnly });
|
|
1042
|
+
const clients = services ?? await servicesForMode(config);
|
|
1043
|
+
const existing = await clients.orderByCloid(cloid);
|
|
1044
|
+
const builder = config.builder.fee === 0 ? void 0 : config.mode === "paper" ? { b: config.builder.address.toLowerCase(), f: config.builder.fee } : await approvedBuilder(config);
|
|
1045
|
+
if (existing)
|
|
1046
|
+
return {
|
|
1047
|
+
result: existing,
|
|
1048
|
+
builderFeeRate: builderRate(config),
|
|
1049
|
+
builderAddress: config.builder.address,
|
|
1050
|
+
cloid,
|
|
1051
|
+
idempotent: true
|
|
1052
|
+
};
|
|
1053
|
+
const order = await prepareOrder(config, orderInput, clients);
|
|
1054
|
+
const result = await exchangeAction(
|
|
1055
|
+
"order",
|
|
1056
|
+
() => clients.exchange.order({
|
|
1057
|
+
orders: [order],
|
|
1058
|
+
grouping: "na",
|
|
1059
|
+
...builder ? { builder } : {}
|
|
1060
|
+
}),
|
|
1061
|
+
cloid
|
|
1062
|
+
);
|
|
1063
|
+
return {
|
|
1064
|
+
result,
|
|
1065
|
+
builderFeeRate: builderRate(config),
|
|
1066
|
+
builderAddress: config.builder.address,
|
|
1067
|
+
cloid,
|
|
1068
|
+
idempotent: false
|
|
1069
|
+
};
|
|
1070
|
+
});
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
throw annotate(error, cloid);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
register("order", {
|
|
1076
|
+
description: "Place limit, IOC market or TP/SL order",
|
|
1077
|
+
schema: orderSchema,
|
|
1078
|
+
execute: async (input, context) => placeOrder(await commandConfig(context), input)
|
|
1079
|
+
});
|
|
1080
|
+
function boundedMarketPrice(mid, slippage, side, szDecimals) {
|
|
1081
|
+
const units = parseUsdc(mid, 18);
|
|
1082
|
+
const ratio = BigInt(Math.round(slippage * 1e9));
|
|
1083
|
+
const bound = units * (side === "buy" ? 1000000000n + ratio : 1000000000n - ratio) / 1000000000n;
|
|
1084
|
+
const rounded = roundPrice(formatUsdc(bound, 18), szDecimals);
|
|
1085
|
+
if (side === "buy" || parseUsdc(rounded, 18) >= bound) return rounded;
|
|
1086
|
+
const precision = Math.max(
|
|
1087
|
+
0,
|
|
1088
|
+
Math.min(
|
|
1089
|
+
6 - szDecimals,
|
|
1090
|
+
4 - Math.floor(Math.log10(Number(formatUsdc(bound, 18))))
|
|
1091
|
+
)
|
|
1092
|
+
);
|
|
1093
|
+
const tick = 10n ** BigInt(18 - precision);
|
|
1094
|
+
return formatUsdc(parseUsdc(rounded, 18) + tick, 18);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/commands/cancel.ts
|
|
1098
|
+
import { z as z7 } from "zod";
|
|
1099
|
+
var cancelSchema = z7.object({
|
|
1100
|
+
oid: z7.number().int().positive().optional(),
|
|
1101
|
+
oids: z7.array(z7.number().int().positive()).optional(),
|
|
1102
|
+
cloid: z7.string().optional(),
|
|
1103
|
+
cloids: z7.array(z7.string()).optional(),
|
|
1104
|
+
coin: z7.string().optional(),
|
|
1105
|
+
all: z7.boolean().default(false),
|
|
1106
|
+
intent: z7.string().optional()
|
|
1107
|
+
});
|
|
1108
|
+
async function cancelOrders(config, input, services) {
|
|
1109
|
+
return withTradeLock(config, () => cancelLocked(config, input, services));
|
|
1110
|
+
}
|
|
1111
|
+
async function cancelLocked(config, input, services) {
|
|
1112
|
+
await assertCanTrade(config, { cancel: true });
|
|
1113
|
+
const clients = services ?? await servicesForMode(config);
|
|
1114
|
+
const orders = await clients.orders();
|
|
1115
|
+
const oids = input.oids ?? (input.oid ? [input.oid] : []), cloids = input.cloids ?? (input.cloid ? [input.cloid] : []);
|
|
1116
|
+
if (!input.all && !input.coin && !oids.length && !cloids.length)
|
|
1117
|
+
throw new AgentCliError("CONFIG_INVALID", "Select oid, cloid, coin or all");
|
|
1118
|
+
for (const oid of oids)
|
|
1119
|
+
if (!orders.some((o) => o.oid === oid))
|
|
1120
|
+
throw new AgentCliError("ORDER_NOT_FOUND", `Order ${oid} absent`);
|
|
1121
|
+
for (const cloid of cloids)
|
|
1122
|
+
if (!orders.some((o) => o.cloid === cloid))
|
|
1123
|
+
throw new AgentCliError("ORDER_NOT_FOUND", `Order ${cloid} absent`);
|
|
1124
|
+
const selected = orders.filter(
|
|
1125
|
+
(o) => (!input.coin || o.coin === input.coin) && (input.all || input.coin && !oids.length && !cloids.length || oids.includes(o.oid) || !!o.cloid && cloids.includes(o.cloid))
|
|
1126
|
+
);
|
|
1127
|
+
const cancels = await Promise.all(
|
|
1128
|
+
selected.map(async (o) => ({
|
|
1129
|
+
a: (await clients.market(o.coin)).asset,
|
|
1130
|
+
o: o.oid
|
|
1131
|
+
}))
|
|
1132
|
+
);
|
|
1133
|
+
if (!cancels.length) return { cancelled: [], result: null };
|
|
1134
|
+
return {
|
|
1135
|
+
cancelled: selected.map((o) => String(o.oid)),
|
|
1136
|
+
result: await exchangeAction(
|
|
1137
|
+
"cancel",
|
|
1138
|
+
() => clients.exchange.cancel({ cancels })
|
|
1139
|
+
)
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
register("cancel", {
|
|
1143
|
+
description: "Cancel by oid/cloid, coin or all",
|
|
1144
|
+
schema: cancelSchema,
|
|
1145
|
+
execute: async (input, context) => cancelOrders(
|
|
1146
|
+
await commandConfig(context),
|
|
1147
|
+
input
|
|
1148
|
+
)
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
// src/commands/modify.ts
|
|
1152
|
+
import { z as z8 } from "zod";
|
|
1153
|
+
var modification = z8.object({
|
|
1154
|
+
oid: z8.number().int().positive().optional(),
|
|
1155
|
+
cloid: z8.string().regex(/^0x[0-9a-fA-F]{32}$/).optional(),
|
|
1156
|
+
size: z8.string().optional(),
|
|
1157
|
+
price: z8.string().optional()
|
|
1158
|
+
});
|
|
1159
|
+
var modifySchema = modification.extend({
|
|
1160
|
+
modifications: z8.array(modification).optional(),
|
|
1161
|
+
intent: z8.string().optional()
|
|
1162
|
+
});
|
|
1163
|
+
async function modifyOrders(config, input, services) {
|
|
1164
|
+
return withTradeLock(config, () => modifyLocked(config, input, services));
|
|
1165
|
+
}
|
|
1166
|
+
async function modifyLocked(config, input, services) {
|
|
1167
|
+
const clients = services ?? await servicesForMode(config);
|
|
1168
|
+
const orders = await clients.orders();
|
|
1169
|
+
const staged = [...orders];
|
|
1170
|
+
const stagedServices = { ...clients, orders: async () => staged };
|
|
1171
|
+
const requests = input.modifications ?? [input];
|
|
1172
|
+
const originals = requests.map((request) => {
|
|
1173
|
+
if (!request.oid && !request.cloid)
|
|
1174
|
+
throw new AgentCliError("CONFIG_INVALID", "Specify oid or cloid");
|
|
1175
|
+
const original = orders.find(
|
|
1176
|
+
(o) => request.oid ? o.oid === request.oid : o.cloid === request.cloid
|
|
1177
|
+
);
|
|
1178
|
+
if (!original)
|
|
1179
|
+
throw new AgentCliError("ORDER_NOT_FOUND", "Resting order absent");
|
|
1180
|
+
return original;
|
|
1181
|
+
});
|
|
1182
|
+
await assertCanTrade(config, {
|
|
1183
|
+
reduceOnly: originals.length > 0 && originals.every((order) => order.reduceOnly)
|
|
1184
|
+
});
|
|
1185
|
+
const modifies = [];
|
|
1186
|
+
for (const [requestIndex, request] of requests.entries()) {
|
|
1187
|
+
const original = originals[requestIndex];
|
|
1188
|
+
if (original.isTrigger && (!original.triggerPx || !/take profit|stop/i.test(original.orderType ?? "")))
|
|
1189
|
+
throw new AgentCliError(
|
|
1190
|
+
"UPSTREAM_ERROR",
|
|
1191
|
+
"Trigger order metadata incomplete"
|
|
1192
|
+
);
|
|
1193
|
+
const market = await clients.market(original.coin);
|
|
1194
|
+
const size = roundSize(request.size ?? original.sz, market.szDecimals);
|
|
1195
|
+
const price = roundPrice(
|
|
1196
|
+
request.price ?? original.limitPx,
|
|
1197
|
+
market.szDecimals
|
|
1198
|
+
);
|
|
1199
|
+
await checkRisk(
|
|
1200
|
+
config,
|
|
1201
|
+
{
|
|
1202
|
+
coin: original.coin,
|
|
1203
|
+
size,
|
|
1204
|
+
price,
|
|
1205
|
+
side: original.side === "B" ? "buy" : "sell",
|
|
1206
|
+
leverage: 1,
|
|
1207
|
+
reduceOnly: original.reduceOnly
|
|
1208
|
+
},
|
|
1209
|
+
stagedServices,
|
|
1210
|
+
original.oid
|
|
1211
|
+
);
|
|
1212
|
+
const index = staged.findIndex((order) => order.oid === original.oid);
|
|
1213
|
+
staged[index] = { ...original, sz: size, limitPx: price };
|
|
1214
|
+
modifies.push({
|
|
1215
|
+
oid: request.cloid ?? original.oid,
|
|
1216
|
+
order: {
|
|
1217
|
+
a: market.asset,
|
|
1218
|
+
b: original.side === "B",
|
|
1219
|
+
p: price,
|
|
1220
|
+
s: size,
|
|
1221
|
+
r: original.reduceOnly ?? false,
|
|
1222
|
+
t: original.isTrigger ? {
|
|
1223
|
+
trigger: {
|
|
1224
|
+
triggerPx: roundPrice(original.triggerPx, market.szDecimals),
|
|
1225
|
+
isMarket: /market/i.test(original.orderType),
|
|
1226
|
+
tpsl: /take profit/i.test(original.orderType) ? "tp" : "sl"
|
|
1227
|
+
}
|
|
1228
|
+
} : { limit: { tif: original.tif ?? "Gtc" } },
|
|
1229
|
+
...original.cloid ? { c: original.cloid } : {}
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
return exchangeAction(
|
|
1234
|
+
"batchModify",
|
|
1235
|
+
() => clients.exchange.batchModify({ modifies })
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
register("modify", {
|
|
1239
|
+
description: "Modify price/size of single or batch resting orders",
|
|
1240
|
+
schema: modifySchema,
|
|
1241
|
+
execute: async (input, context) => modifyOrders(
|
|
1242
|
+
await commandConfig(context),
|
|
1243
|
+
input
|
|
1244
|
+
)
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
// src/commands/leverage.ts
|
|
1248
|
+
import { z as z9 } from "zod";
|
|
1249
|
+
var leverageSchema = z9.object({
|
|
1250
|
+
coin: z9.string(),
|
|
1251
|
+
leverage: z9.number().int().positive(),
|
|
1252
|
+
mode: z9.enum(["cross", "isolated"]).default("cross"),
|
|
1253
|
+
intent: z9.string().optional()
|
|
1254
|
+
});
|
|
1255
|
+
async function setLeverage(config, input, services) {
|
|
1256
|
+
return withTradeLock(config, async () => {
|
|
1257
|
+
await assertCanTrade(config);
|
|
1258
|
+
const clients = services ?? await servicesForMode(config);
|
|
1259
|
+
const market = await clients.market(input.coin);
|
|
1260
|
+
if (config.allowedMarkets.length && !config.allowedMarkets.includes(input.coin) || input.leverage > Math.min(market.maxLeverage, config.limits.maxLeverage ?? Infinity))
|
|
1261
|
+
throw new AgentCliError(
|
|
1262
|
+
"POLICY_REJECTED",
|
|
1263
|
+
"Leverage or market exceeds configured limits"
|
|
1264
|
+
);
|
|
1265
|
+
if (config.limits.dailyLoss !== void 0 && parseUsdc(await requiredDailyLoss(clients), 8) > parseUsdc(String(config.limits.dailyLoss), 8))
|
|
1266
|
+
throw new AgentCliError("POLICY_REJECTED", "Daily loss cap exceeded");
|
|
1267
|
+
return exchangeAction(
|
|
1268
|
+
"updateLeverage",
|
|
1269
|
+
() => clients.exchange.updateLeverage({
|
|
1270
|
+
asset: market.asset,
|
|
1271
|
+
isCross: input.mode === "cross",
|
|
1272
|
+
leverage: input.leverage
|
|
1273
|
+
})
|
|
1274
|
+
);
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
register("leverage", {
|
|
1278
|
+
description: "Set market leverage and cross/isolated margin mode",
|
|
1279
|
+
schema: leverageSchema,
|
|
1280
|
+
execute: async (input, context) => setLeverage(
|
|
1281
|
+
await commandConfig(context),
|
|
1282
|
+
input
|
|
1283
|
+
)
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
// src/runner/subscriptions.ts
|
|
1287
|
+
import { SubscriptionClient, WebSocketTransport } from "@bloxwap/hyperliquid";
|
|
1288
|
+
import WebSocket from "ws";
|
|
1289
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1290
|
+
import { join as join2 } from "path";
|
|
1291
|
+
function subscriptionSet(config) {
|
|
1292
|
+
return [
|
|
1293
|
+
"userEvents",
|
|
1294
|
+
"userFills",
|
|
1295
|
+
"orderUpdates",
|
|
1296
|
+
"clearinghouseState",
|
|
1297
|
+
...config.allowedMarkets.length ? ["allMids", ...config.allowedMarkets.map((coin) => `l2Book:${coin}`)] : []
|
|
1298
|
+
];
|
|
1299
|
+
}
|
|
1300
|
+
var Subscriptions = class {
|
|
1301
|
+
constructor(runner, backend, emit) {
|
|
1302
|
+
this.runner = runner;
|
|
1303
|
+
this.backend = backend;
|
|
1304
|
+
this.emit = emit;
|
|
1305
|
+
}
|
|
1306
|
+
runner;
|
|
1307
|
+
backend;
|
|
1308
|
+
emit;
|
|
1309
|
+
state = "connecting";
|
|
1310
|
+
lastMessage = Date.now();
|
|
1311
|
+
lastFill = null;
|
|
1312
|
+
seen = /* @__PURE__ */ new Set();
|
|
1313
|
+
queue = Promise.resolve();
|
|
1314
|
+
subscriptions = [];
|
|
1315
|
+
async start() {
|
|
1316
|
+
const path = join2(this.runner.directory, "subscriptions.json");
|
|
1317
|
+
try {
|
|
1318
|
+
const saved = JSON.parse(await readFile3(path, "utf8"));
|
|
1319
|
+
this.seen = new Set(saved.seen);
|
|
1320
|
+
this.lastFill = saved.lastFill;
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
if (error.code !== "ENOENT") throw error;
|
|
1323
|
+
}
|
|
1324
|
+
for (const tid of await this.journalTids()) this.seen.add(tid);
|
|
1325
|
+
for (const fill of await this.backend.gap(this.lastFill ?? void 0))
|
|
1326
|
+
await this.fill(fill);
|
|
1327
|
+
this.backend.onState((state) => {
|
|
1328
|
+
const reconnect = this.state !== "connecting" && state === "connected";
|
|
1329
|
+
this.state = state;
|
|
1330
|
+
this.lastMessage = Date.now();
|
|
1331
|
+
if (reconnect)
|
|
1332
|
+
this.enqueue(async () => {
|
|
1333
|
+
for (const fill of await this.backend.gap(
|
|
1334
|
+
this.lastFill ?? Date.now() - 6e4
|
|
1335
|
+
))
|
|
1336
|
+
await this.fill(fill);
|
|
1337
|
+
await this.emit("runner_state", { wsState: state });
|
|
1338
|
+
});
|
|
1339
|
+
});
|
|
1340
|
+
for (const name of subscriptionSet(this.runner.config)) {
|
|
1341
|
+
const [type, coin] = name.split(":");
|
|
1342
|
+
const params = coin ? { coin } : type === "allMids" ? {} : { user: this.runner.config.masterAddress };
|
|
1343
|
+
this.subscriptions.push(
|
|
1344
|
+
await this.backend.subscribe(type, params, (data) => {
|
|
1345
|
+
this.lastMessage = Date.now();
|
|
1346
|
+
this.enqueue(async () => {
|
|
1347
|
+
if (type === "userFills") {
|
|
1348
|
+
for (const fill of data.fills)
|
|
1349
|
+
await this.fill(fill);
|
|
1350
|
+
} else if (type === "userEvents" && "fills" in data) {
|
|
1351
|
+
for (const fill of data.fills)
|
|
1352
|
+
await this.fill(fill);
|
|
1353
|
+
} else if (type === "orderUpdates")
|
|
1354
|
+
await this.emit("order_update", data);
|
|
1355
|
+
else if (type === "clearinghouseState")
|
|
1356
|
+
await this.emit("position_change", data);
|
|
1357
|
+
});
|
|
1358
|
+
})
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
this.runner.onShutdown(async () => {
|
|
1362
|
+
await this.queue;
|
|
1363
|
+
await Promise.all(this.subscriptions.map((s) => s.unsubscribe()));
|
|
1364
|
+
await this.backend.close();
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
enqueue(operation) {
|
|
1368
|
+
this.queue = this.queue.then(operation).catch(async (error) => {
|
|
1369
|
+
await this.emit("error", normalizeError(error).toJSON());
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
async journalTids() {
|
|
1373
|
+
try {
|
|
1374
|
+
return (await readEvents(join2(this.runner.directory, "events.jsonl"))).filter((event) => event.type === "fill").map((event) => event.data.tid).filter((tid) => typeof tid === "number");
|
|
1375
|
+
} catch {
|
|
1376
|
+
return [];
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
async persist() {
|
|
1380
|
+
await atomicWrite(
|
|
1381
|
+
join2(this.runner.directory, "subscriptions.json"),
|
|
1382
|
+
JSON.stringify({
|
|
1383
|
+
seen: [...this.seen].slice(-1e4),
|
|
1384
|
+
lastFill: this.lastFill
|
|
1385
|
+
})
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
async fill(fill) {
|
|
1389
|
+
if (this.seen.has(fill.tid)) return;
|
|
1390
|
+
if ((await this.journalTids()).includes(fill.tid)) {
|
|
1391
|
+
this.seen.add(fill.tid);
|
|
1392
|
+
this.lastFill = Math.max(this.lastFill ?? 0, fill.time);
|
|
1393
|
+
await this.persist();
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
this.seen.add(fill.tid);
|
|
1397
|
+
this.lastFill = Math.max(this.lastFill ?? 0, fill.time);
|
|
1398
|
+
if (this.seen.size > 1e4)
|
|
1399
|
+
this.seen.delete(this.seen.values().next().value);
|
|
1400
|
+
await this.emit("fill", fill);
|
|
1401
|
+
await this.persist();
|
|
1402
|
+
}
|
|
1403
|
+
async flush() {
|
|
1404
|
+
await this.queue;
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
function sdkBackend(config) {
|
|
1408
|
+
const transport = new WebSocketTransport({
|
|
1409
|
+
isTestnet: config.network === "testnet",
|
|
1410
|
+
resubscribe: true,
|
|
1411
|
+
webSocketFactory: (url, protocols) => new WebSocket(url, protocols)
|
|
1412
|
+
});
|
|
1413
|
+
const client = new SubscriptionClient({ transport });
|
|
1414
|
+
return {
|
|
1415
|
+
subscribe: (type, params, listener) => client[type](params, listener),
|
|
1416
|
+
onState: (listener) => transport.events.addEventListener(
|
|
1417
|
+
"connectionstatechange",
|
|
1418
|
+
(event) => listener(event.detail)
|
|
1419
|
+
),
|
|
1420
|
+
close: async () => {
|
|
1421
|
+
await transport.close();
|
|
1422
|
+
},
|
|
1423
|
+
gap: async (startTime = Date.now() - 6e4) => {
|
|
1424
|
+
const fills = [];
|
|
1425
|
+
const endTime = Date.now();
|
|
1426
|
+
for (let page = 0; page < 100; page++) {
|
|
1427
|
+
const batch = await hlInfo(config.network, "userFillsByTime", {
|
|
1428
|
+
user: config.masterAddress,
|
|
1429
|
+
startTime,
|
|
1430
|
+
endTime
|
|
1431
|
+
});
|
|
1432
|
+
fills.push(...batch);
|
|
1433
|
+
if (batch.length < 2e3) return fills;
|
|
1434
|
+
const last = Math.max(...batch.map((fill) => fill.time));
|
|
1435
|
+
if (last <= startTime)
|
|
1436
|
+
throw new Error("Fill backfill cannot advance; inspect gap");
|
|
1437
|
+
startTime = last;
|
|
1438
|
+
}
|
|
1439
|
+
throw new Error("Fill backfill exceeded pagination bound");
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
async function startSubscriptions(runner, emit) {
|
|
1444
|
+
const subscriptions = new Subscriptions(
|
|
1445
|
+
runner,
|
|
1446
|
+
sdkBackend(runner.config),
|
|
1447
|
+
emit
|
|
1448
|
+
);
|
|
1449
|
+
await subscriptions.start();
|
|
1450
|
+
return subscriptions;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/commands/run.ts
|
|
1454
|
+
import { z as z10 } from "zod";
|
|
1455
|
+
|
|
1456
|
+
// src/paper/engine.ts
|
|
1457
|
+
import { SubscriptionClient as SubscriptionClient2, WebSocketTransport as WebSocketTransport2 } from "@bloxwap/hyperliquid";
|
|
1458
|
+
import WebSocket2 from "ws";
|
|
1459
|
+
var PaperEngine = class {
|
|
1460
|
+
constructor(runner, store, backend, emit, data = paperData(), builderFee = runner.config.builder.fee) {
|
|
1461
|
+
this.runner = runner;
|
|
1462
|
+
this.store = store;
|
|
1463
|
+
this.backend = backend;
|
|
1464
|
+
this.emit = emit;
|
|
1465
|
+
this.data = data;
|
|
1466
|
+
this.builderFee = builderFee;
|
|
1467
|
+
}
|
|
1468
|
+
runner;
|
|
1469
|
+
store;
|
|
1470
|
+
backend;
|
|
1471
|
+
emit;
|
|
1472
|
+
data;
|
|
1473
|
+
builderFee;
|
|
1474
|
+
state = "connecting";
|
|
1475
|
+
lastMessage = Date.now();
|
|
1476
|
+
lastFill = null;
|
|
1477
|
+
books = /* @__PURE__ */ new Map();
|
|
1478
|
+
lastTrade = /* @__PURE__ */ new Map();
|
|
1479
|
+
subscriptions = [];
|
|
1480
|
+
queue = Promise.resolve();
|
|
1481
|
+
timer;
|
|
1482
|
+
async start() {
|
|
1483
|
+
this.backend.onState((state) => {
|
|
1484
|
+
const reconnect = this.state !== "connecting" && state === "connected";
|
|
1485
|
+
this.state = state;
|
|
1486
|
+
this.lastMessage = Date.now();
|
|
1487
|
+
if (reconnect)
|
|
1488
|
+
this.enqueue(async () => {
|
|
1489
|
+
await this.refreshSubscriptions();
|
|
1490
|
+
await this.emit("runner_state", { wsState: state, paper: true });
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
await this.refreshSubscriptions();
|
|
1494
|
+
this.timer = setInterval(() => {
|
|
1495
|
+
this.enqueue(() => this.tick());
|
|
1496
|
+
}, 3e4);
|
|
1497
|
+
this.runner.onShutdown(async () => {
|
|
1498
|
+
if (this.timer) clearInterval(this.timer);
|
|
1499
|
+
await this.queue;
|
|
1500
|
+
await Promise.all(
|
|
1501
|
+
this.subscriptions.map((subscription) => subscription.handle.unsubscribe())
|
|
1502
|
+
);
|
|
1503
|
+
this.subscriptions = [];
|
|
1504
|
+
await this.backend.close();
|
|
1505
|
+
});
|
|
1506
|
+
this.enqueue(() => this.tick());
|
|
1507
|
+
}
|
|
1508
|
+
enqueue(operation) {
|
|
1509
|
+
this.queue = this.queue.then(operation).catch(async (error) => {
|
|
1510
|
+
await this.emit("error", normalizeError(error).toJSON());
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
async activeCoins() {
|
|
1514
|
+
const state = await this.store.load();
|
|
1515
|
+
return [
|
|
1516
|
+
.../* @__PURE__ */ new Set([
|
|
1517
|
+
...this.runner.config.allowedMarkets,
|
|
1518
|
+
...state.orders.map((order) => order.coin),
|
|
1519
|
+
...state.positions.map((position) => position.coin)
|
|
1520
|
+
])
|
|
1521
|
+
];
|
|
1522
|
+
}
|
|
1523
|
+
async refreshSubscriptions() {
|
|
1524
|
+
const coins = await this.activeCoins();
|
|
1525
|
+
for (const subscription of [...this.subscriptions]) {
|
|
1526
|
+
if (!coins.includes(subscription.coin)) {
|
|
1527
|
+
await subscription.handle.unsubscribe();
|
|
1528
|
+
this.subscriptions = this.subscriptions.filter(
|
|
1529
|
+
(candidate) => candidate !== subscription
|
|
1530
|
+
);
|
|
1531
|
+
this.books.delete(subscription.coin);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
for (const coin of coins) {
|
|
1535
|
+
for (const type of ["l2Book", "trades", "bbo"]) {
|
|
1536
|
+
if (this.subscriptions.some((s) => s.coin === coin && s.type === type))
|
|
1537
|
+
continue;
|
|
1538
|
+
const handle = await this.backend.subscribe(
|
|
1539
|
+
type,
|
|
1540
|
+
{ coin },
|
|
1541
|
+
(message) => {
|
|
1542
|
+
this.lastMessage = Date.now();
|
|
1543
|
+
this.enqueue(() => this.handleMessage(type, coin, message));
|
|
1544
|
+
}
|
|
1545
|
+
);
|
|
1546
|
+
this.subscriptions.push({ coin, type, handle });
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
async handleMessage(type, coin, message) {
|
|
1551
|
+
if (type === "trades") {
|
|
1552
|
+
for (const trade of message.trades ?? []) {
|
|
1553
|
+
if (trade.coin === coin) this.lastTrade.set(coin, trade.px);
|
|
1554
|
+
}
|
|
1555
|
+
await this.match(coin);
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
if (type === "l2Book") this.applyBookMessage(coin, message);
|
|
1559
|
+
await this.match(coin);
|
|
1560
|
+
}
|
|
1561
|
+
applyBookMessage(coin, message) {
|
|
1562
|
+
let book = this.books.get(coin) ?? { bids: [], asks: [] };
|
|
1563
|
+
if (message.side) {
|
|
1564
|
+
const side = message.side === "B" ? "bids" : "asks";
|
|
1565
|
+
for (const level of message.levels[0] ?? []) {
|
|
1566
|
+
const index = book[side].findIndex((existing) => existing.px === level.px);
|
|
1567
|
+
if (Number(level.sz) === 0) {
|
|
1568
|
+
if (index >= 0) book[side].splice(index, 1);
|
|
1569
|
+
} else if (index >= 0) book[side][index].sz = level.sz;
|
|
1570
|
+
else book[side].push({ px: level.px, sz: level.sz });
|
|
1571
|
+
}
|
|
1572
|
+
book.bids.sort((a, b) => Number(b.px) - Number(a.px));
|
|
1573
|
+
book.asks.sort((a, b) => Number(a.px) - Number(b.px));
|
|
1574
|
+
} else if (Array.isArray(message.levels)) {
|
|
1575
|
+
book = { bids: message.levels[0] ?? [], asks: message.levels[1] ?? [] };
|
|
1576
|
+
} else return;
|
|
1577
|
+
this.books.set(coin, book);
|
|
1578
|
+
}
|
|
1579
|
+
async match(coin) {
|
|
1580
|
+
const book = this.books.get(coin) ?? null;
|
|
1581
|
+
const marks = await this.data.marks();
|
|
1582
|
+
const result = await this.store.update(
|
|
1583
|
+
(state) => matchOrders(
|
|
1584
|
+
state,
|
|
1585
|
+
coin,
|
|
1586
|
+
{
|
|
1587
|
+
bestBid: book?.bids[0]?.px,
|
|
1588
|
+
bestAsk: book?.asks[0]?.px,
|
|
1589
|
+
lastTradePx: this.lastTrade.get(coin),
|
|
1590
|
+
markPx: marks[coin]
|
|
1591
|
+
},
|
|
1592
|
+
book,
|
|
1593
|
+
this.builderFee,
|
|
1594
|
+
Date.now()
|
|
1595
|
+
)
|
|
1596
|
+
);
|
|
1597
|
+
await this.report(result);
|
|
1598
|
+
}
|
|
1599
|
+
async report(result) {
|
|
1600
|
+
for (const trade of result.fills) {
|
|
1601
|
+
this.lastFill = Math.max(this.lastFill ?? 0, trade.time);
|
|
1602
|
+
await this.emit("fill", { ...trade, paper: true });
|
|
1603
|
+
}
|
|
1604
|
+
for (const oid of [...result.filledOids, ...result.triggeredOids])
|
|
1605
|
+
await this.emit("order_update", { oid, status: "filled", paper: true });
|
|
1606
|
+
for (const oid of result.cancelledOids)
|
|
1607
|
+
await this.emit("order_update", { oid, status: "canceled", paper: true });
|
|
1608
|
+
if (result.fills.length)
|
|
1609
|
+
await this.emit("position_change", { fills: result.fills.length, paper: true });
|
|
1610
|
+
}
|
|
1611
|
+
async tick() {
|
|
1612
|
+
await this.refreshSubscriptions();
|
|
1613
|
+
const [marks, rates] = await Promise.all([
|
|
1614
|
+
this.data.marks(),
|
|
1615
|
+
this.data.fundingRates()
|
|
1616
|
+
]);
|
|
1617
|
+
const now = Date.now();
|
|
1618
|
+
const payments = await this.store.update(
|
|
1619
|
+
(state2) => accrueFunding(state2, rates, marks, now)
|
|
1620
|
+
);
|
|
1621
|
+
for (const payment of payments)
|
|
1622
|
+
await this.emit("position_change", { type: "funding", ...payment, paper: true });
|
|
1623
|
+
const state = await this.store.load();
|
|
1624
|
+
const maxLeverage = /* @__PURE__ */ new Map();
|
|
1625
|
+
for (const coin of new Set(state.positions.map((position) => position.coin)))
|
|
1626
|
+
maxLeverage.set(coin, await this.data.maxLeverageOf(coin));
|
|
1627
|
+
const liquidated = await this.store.update(
|
|
1628
|
+
(latest) => liquidate(latest, marks, (coin) => maxLeverage.get(coin) ?? 1, this.builderFee, now)
|
|
1629
|
+
);
|
|
1630
|
+
for (const trade of liquidated) {
|
|
1631
|
+
await this.emit("fill", { ...trade, paper: true });
|
|
1632
|
+
await this.emit("error", {
|
|
1633
|
+
code: "INSUFFICIENT_MARGIN",
|
|
1634
|
+
message: `Paper liquidation on ${trade.coin}`,
|
|
1635
|
+
paper: true
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
await this.emit("runner_state", { ...health(this.runner), paper: true });
|
|
1639
|
+
}
|
|
1640
|
+
async flush() {
|
|
1641
|
+
await this.queue;
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
function paperBackend() {
|
|
1645
|
+
const transport = new WebSocketTransport2({
|
|
1646
|
+
isTestnet: false,
|
|
1647
|
+
// paper trading always reads mainnet public data
|
|
1648
|
+
resubscribe: true,
|
|
1649
|
+
webSocketFactory: (url, protocols) => new WebSocket2(url, protocols)
|
|
1650
|
+
});
|
|
1651
|
+
const client = new SubscriptionClient2({ transport });
|
|
1652
|
+
return {
|
|
1653
|
+
subscribe: (type, params, listener) => client[type](params, listener),
|
|
1654
|
+
onState: (listener) => transport.events.addEventListener(
|
|
1655
|
+
"connectionstatechange",
|
|
1656
|
+
(event) => listener(event.detail)
|
|
1657
|
+
),
|
|
1658
|
+
close: async () => {
|
|
1659
|
+
await transport.close();
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
async function startPaperEngine(runner, emit, backend = paperBackend()) {
|
|
1664
|
+
const engine = new PaperEngine(
|
|
1665
|
+
runner,
|
|
1666
|
+
new PaperStore(),
|
|
1667
|
+
backend,
|
|
1668
|
+
emit
|
|
1669
|
+
);
|
|
1670
|
+
await engine.start();
|
|
1671
|
+
return engine;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// src/commands/run.ts
|
|
1675
|
+
register("run", {
|
|
1676
|
+
description: "Start persistent signing runner (foreground or detach)",
|
|
1677
|
+
schema: z10.object({
|
|
1678
|
+
detach: z10.boolean().default(false),
|
|
1679
|
+
daemon: z10.boolean().default(false),
|
|
1680
|
+
paper: z10.boolean().default(false)
|
|
1681
|
+
}),
|
|
1682
|
+
execute: async (input, context) => {
|
|
1683
|
+
const config = await commandConfig(context);
|
|
1684
|
+
const paper = input.paper || config.mode === "paper";
|
|
1685
|
+
if (input.detach || input.daemon)
|
|
1686
|
+
return {
|
|
1687
|
+
paper,
|
|
1688
|
+
...await detachRunner(
|
|
1689
|
+
[
|
|
1690
|
+
...context.configPath ? ["--config", context.configPath] : [],
|
|
1691
|
+
...context.network ? ["--network", context.network] : [],
|
|
1692
|
+
...paper ? ["--paper"] : []
|
|
1693
|
+
],
|
|
1694
|
+
runnerPidPath(paper ? "paper" : "live")
|
|
1695
|
+
)
|
|
1696
|
+
};
|
|
1697
|
+
if (paper) {
|
|
1698
|
+
const runner2 = new Runner(config, paperDir());
|
|
1699
|
+
const info2 = await runner2.start(async () => void 0);
|
|
1700
|
+
if (!info2.existing) {
|
|
1701
|
+
setActiveRunner(runner2);
|
|
1702
|
+
runner2.signals();
|
|
1703
|
+
runner2.onShutdown(() => paperEvents.flush());
|
|
1704
|
+
try {
|
|
1705
|
+
await rpcServer(runner2, context);
|
|
1706
|
+
await startPaperEngine(runner2, async (type, data) => {
|
|
1707
|
+
if (eventTypes.includes(type))
|
|
1708
|
+
await paperEvents.append(type, data);
|
|
1709
|
+
runner2.logger.log(type === "error" ? "error" : "info", type, {
|
|
1710
|
+
data
|
|
1711
|
+
});
|
|
1712
|
+
});
|
|
1713
|
+
await runner2.markReady();
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
await runner2.shutdown();
|
|
1716
|
+
throw error;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return {
|
|
1720
|
+
paper: true,
|
|
1721
|
+
...info2,
|
|
1722
|
+
ready: info2.existing ? info2.ready : runner2.info.ready
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
const runner = new Runner(config);
|
|
1726
|
+
const info = await runner.start();
|
|
1727
|
+
if (!info.existing) {
|
|
1728
|
+
setActiveRunner(runner);
|
|
1729
|
+
runner.signals();
|
|
1730
|
+
runner.onShutdown(() => events.flush());
|
|
1731
|
+
try {
|
|
1732
|
+
await rpcServer(runner, context);
|
|
1733
|
+
const subscriptions = await startSubscriptions(
|
|
1734
|
+
runner,
|
|
1735
|
+
async (type, data) => {
|
|
1736
|
+
if (eventTypes.includes(type))
|
|
1737
|
+
await events.append(type, data);
|
|
1738
|
+
runner.logger.log(type === "error" ? "error" : "info", type, {
|
|
1739
|
+
data
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
);
|
|
1743
|
+
startHeartbeat(runner, subscriptions);
|
|
1744
|
+
await runner.markReady();
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
await runner.shutdown();
|
|
1747
|
+
throw error;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return {
|
|
1751
|
+
paper: false,
|
|
1752
|
+
...info,
|
|
1753
|
+
ready: info.existing ? info.ready : runner.info.ready
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
// src/commands/events.ts
|
|
1759
|
+
import { z as z11 } from "zod";
|
|
1760
|
+
import { join as join3 } from "path";
|
|
1761
|
+
register("events", {
|
|
1762
|
+
description: "Replay or follow JSON-line runner events",
|
|
1763
|
+
schema: z11.object({
|
|
1764
|
+
follow: z11.boolean().default(false),
|
|
1765
|
+
since: z11.number().int().nonnegative().default(0),
|
|
1766
|
+
types: z11.array(z11.enum(eventTypes)).default([])
|
|
1767
|
+
}),
|
|
1768
|
+
execute: async (input, context) => {
|
|
1769
|
+
const paper = await resolveMode(context, "events") === "paper";
|
|
1770
|
+
const path = paper ? join3(paperDir(), "events.jsonl") : join3(stateDir(), "events.jsonl");
|
|
1771
|
+
if (!input.follow)
|
|
1772
|
+
return readEvents(
|
|
1773
|
+
path,
|
|
1774
|
+
input.since,
|
|
1775
|
+
input.types
|
|
1776
|
+
);
|
|
1777
|
+
context.streaming = true;
|
|
1778
|
+
const abort = new AbortController();
|
|
1779
|
+
const stop = () => abort.abort();
|
|
1780
|
+
process.once("SIGINT", stop);
|
|
1781
|
+
process.once("SIGTERM", stop);
|
|
1782
|
+
try {
|
|
1783
|
+
await followEvents({
|
|
1784
|
+
path,
|
|
1785
|
+
since: input.since,
|
|
1786
|
+
types: input.types,
|
|
1787
|
+
signal: abort.signal,
|
|
1788
|
+
write: (event) => process.stdout.write(JSON.stringify(event) + "\n")
|
|
1789
|
+
});
|
|
1790
|
+
} finally {
|
|
1791
|
+
process.off("SIGINT", stop);
|
|
1792
|
+
process.off("SIGTERM", stop);
|
|
1793
|
+
}
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
// src/commands/logs.ts
|
|
1799
|
+
import { z as z12 } from "zod";
|
|
1800
|
+
import { readdir, readFile as readFile4 } from "fs/promises";
|
|
1801
|
+
import { join as join4 } from "path";
|
|
1802
|
+
async function readLogs(options = {}) {
|
|
1803
|
+
const directory = options.directory ?? join4(stateDir(), "logs");
|
|
1804
|
+
let files;
|
|
1805
|
+
try {
|
|
1806
|
+
files = await readdir(directory);
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
if (error.code === "ENOENT") return [];
|
|
1809
|
+
throw error;
|
|
1810
|
+
}
|
|
1811
|
+
const lines = (await Promise.all(
|
|
1812
|
+
files.filter((f) => /^runner.*\.jsonl$/.test(f)).map((f) => readFile4(join4(directory, f), "utf8"))
|
|
1813
|
+
)).flatMap(
|
|
1814
|
+
(content) => content.split("\n").slice(0, -1).filter(Boolean).map((line) => JSON.parse(line))
|
|
1815
|
+
);
|
|
1816
|
+
return lines.filter(
|
|
1817
|
+
(line) => (!options.level || levels[line.level] >= levels[options.level]) && (!options.since || String(line.time) >= options.since) && (!options.requestId || line.requestId === options.requestId)
|
|
1818
|
+
).sort((a, b) => String(a.time).localeCompare(String(b.time))).map(redact);
|
|
1819
|
+
}
|
|
1820
|
+
register("logs", {
|
|
1821
|
+
description: "Read/follow redacted JSON-line runner logs",
|
|
1822
|
+
schema: z12.object({
|
|
1823
|
+
follow: z12.boolean().default(false),
|
|
1824
|
+
level: z12.enum(["debug", "info", "warn", "error"]).optional(),
|
|
1825
|
+
since: z12.string().datetime().optional(),
|
|
1826
|
+
requestId: z12.string().optional()
|
|
1827
|
+
}),
|
|
1828
|
+
execute: async (input, context) => {
|
|
1829
|
+
context.streaming = true;
|
|
1830
|
+
const paper = await resolveMode(context, "logs") === "paper";
|
|
1831
|
+
const directory = paper ? join4(paperDir(), "logs") : join4(stateDir(), "logs");
|
|
1832
|
+
let cursor = 0;
|
|
1833
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1834
|
+
const abort = new AbortController();
|
|
1835
|
+
const stop = () => abort.abort();
|
|
1836
|
+
process.once("SIGINT", stop);
|
|
1837
|
+
process.once("SIGTERM", stop);
|
|
1838
|
+
try {
|
|
1839
|
+
do {
|
|
1840
|
+
const logs = await readLogs({ ...input, directory });
|
|
1841
|
+
for (const log of logs) {
|
|
1842
|
+
const key = JSON.stringify(log);
|
|
1843
|
+
if (!seen.has(key)) {
|
|
1844
|
+
seen.add(key);
|
|
1845
|
+
process.stdout.write(key + "\n");
|
|
1846
|
+
cursor++;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (input.follow && !abort.signal.aborted)
|
|
1850
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1851
|
+
} while (input.follow && !abort.signal.aborted);
|
|
1852
|
+
} finally {
|
|
1853
|
+
process.off("SIGINT", stop);
|
|
1854
|
+
process.off("SIGTERM", stop);
|
|
1855
|
+
}
|
|
1856
|
+
return { lines: cursor };
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
// src/commands/rotate.ts
|
|
1861
|
+
import { z as z13 } from "zod";
|
|
1862
|
+
import { join as join5 } from "path";
|
|
1863
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1864
|
+
async function rotationHandoff(rotation, dependencies) {
|
|
1865
|
+
if (!await dependencies.verify())
|
|
1866
|
+
throw new AgentCliError(
|
|
1867
|
+
"AGENT_NOT_APPROVED",
|
|
1868
|
+
"New agent approval not visible; old agent remains active"
|
|
1869
|
+
);
|
|
1870
|
+
if (!rotation.swapped) {
|
|
1871
|
+
await dependencies.swap(rotation.next);
|
|
1872
|
+
rotation.swapped = true;
|
|
1873
|
+
await dependencies.persist();
|
|
1874
|
+
}
|
|
1875
|
+
return rotation.revoke;
|
|
1876
|
+
}
|
|
1877
|
+
register("rotate", {
|
|
1878
|
+
description: "Approve new agent, hot-swap signer, then revoke old agent",
|
|
1879
|
+
schema: z13.object({
|
|
1880
|
+
signature: z13.string().optional(),
|
|
1881
|
+
revokeSignature: z13.string().optional(),
|
|
1882
|
+
eoaKeyRef: z13.string().regex(/^env:[A-Z_][A-Z0-9_]*$|^file:.+/).optional()
|
|
1883
|
+
}),
|
|
1884
|
+
execute: async (input, context) => {
|
|
1885
|
+
const current = await commandConfig(context);
|
|
1886
|
+
assertLiveMode(current, "rotate");
|
|
1887
|
+
assertApprovedNetwork(current);
|
|
1888
|
+
if (!current.masterAddress || !current.agentAddress)
|
|
1889
|
+
throw new AgentCliError("AGENT_NOT_APPROVED", "Initialize first");
|
|
1890
|
+
const path = join5(
|
|
1891
|
+
stateDir(),
|
|
1892
|
+
`rotation-${current.network}-${current.masterAddress}.json`
|
|
1893
|
+
);
|
|
1894
|
+
let rotation;
|
|
1895
|
+
const client = current.signerType === "privy" ? await privyClient(current) : void 0;
|
|
1896
|
+
try {
|
|
1897
|
+
rotation = JSON.parse(await readFile5(path, "utf8"));
|
|
1898
|
+
if (rotation.revoked && !input.signature && !input.revokeSignature) {
|
|
1899
|
+
await atomicWrite(
|
|
1900
|
+
`${path}.${Date.now()}.previous`,
|
|
1901
|
+
JSON.stringify(rotation)
|
|
1902
|
+
);
|
|
1903
|
+
throw Object.assign(new Error("Begin next rotation"), {
|
|
1904
|
+
code: "ENOENT"
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
if (error.code !== "ENOENT") throw error;
|
|
1909
|
+
const next = {
|
|
1910
|
+
...structuredClone(current),
|
|
1911
|
+
privyWalletId: void 0,
|
|
1912
|
+
agentAddress: void 0,
|
|
1913
|
+
authorizationKeyRef: void 0,
|
|
1914
|
+
eoaKeyRef: void 0,
|
|
1915
|
+
agentExpiry: Date.now() + 179 * 864e5,
|
|
1916
|
+
approved: false,
|
|
1917
|
+
revoked: false
|
|
1918
|
+
};
|
|
1919
|
+
if (current.signerType === "eoa") {
|
|
1920
|
+
await prepareEoaAgent(
|
|
1921
|
+
next,
|
|
1922
|
+
join5(stateDir(), "rotation-next.json"),
|
|
1923
|
+
input.eoaKeyRef
|
|
1924
|
+
);
|
|
1925
|
+
} else {
|
|
1926
|
+
await createAgent(
|
|
1927
|
+
next,
|
|
1928
|
+
client,
|
|
1929
|
+
join5(stateDir(), "rotation-next.json"),
|
|
1930
|
+
current.policyId
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
if (next.agentAddress === current.agentAddress)
|
|
1934
|
+
throw new AgentCliError(
|
|
1935
|
+
"CONFIG_INVALID",
|
|
1936
|
+
"Rotation requires a different EOA agent key"
|
|
1937
|
+
);
|
|
1938
|
+
next.agentName = `blox-${next.agentAddress.slice(-8)}`;
|
|
1939
|
+
const now = Date.now();
|
|
1940
|
+
rotation = {
|
|
1941
|
+
old: current,
|
|
1942
|
+
next,
|
|
1943
|
+
approval: await newApproval(current, "approveAgent", {
|
|
1944
|
+
agentAddress: next.agentAddress,
|
|
1945
|
+
agentName: `${next.agentName} valid_until ${next.agentExpiry}`
|
|
1946
|
+
}),
|
|
1947
|
+
revoke: await newApproval(current, "approveAgent", {
|
|
1948
|
+
agentAddress: "0x" + "0".repeat(40),
|
|
1949
|
+
agentName: current.agentName
|
|
1950
|
+
}),
|
|
1951
|
+
approvalCreatedAt: now,
|
|
1952
|
+
approvalExpiresAt: now + APPROVAL_MAX_AGE_MS,
|
|
1953
|
+
revokeCreatedAt: now,
|
|
1954
|
+
revokeExpiresAt: now + APPROVAL_MAX_AGE_MS,
|
|
1955
|
+
swapped: false,
|
|
1956
|
+
revoked: false
|
|
1957
|
+
};
|
|
1958
|
+
await atomicWrite(path, JSON.stringify(rotation));
|
|
1959
|
+
}
|
|
1960
|
+
if (!rotation.swapped && !isFreshApproval(rotation.approvalCreatedAt)) {
|
|
1961
|
+
const now = Date.now();
|
|
1962
|
+
rotation.approval = await newApproval(current, "approveAgent", {
|
|
1963
|
+
agentAddress: rotation.next.agentAddress,
|
|
1964
|
+
agentName: `${rotation.next.agentName} valid_until ${rotation.next.agentExpiry}`
|
|
1965
|
+
});
|
|
1966
|
+
rotation.approvalCreatedAt = now;
|
|
1967
|
+
rotation.approvalExpiresAt = now + APPROVAL_MAX_AGE_MS;
|
|
1968
|
+
await atomicWrite(path, JSON.stringify(rotation));
|
|
1969
|
+
if (input.signature)
|
|
1970
|
+
throw new AgentCliError(
|
|
1971
|
+
"CONFIG_INVALID",
|
|
1972
|
+
"Approval payload expired; sign the regenerated payload",
|
|
1973
|
+
{ expiresAt: rotation.approvalExpiresAt, approval: rotation.approval }
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
if (!rotation.revoked && !isFreshApproval(rotation.revokeCreatedAt)) {
|
|
1977
|
+
const now = Date.now();
|
|
1978
|
+
rotation.revoke = await newApproval(current, "approveAgent", {
|
|
1979
|
+
agentAddress: "0x" + "0".repeat(40),
|
|
1980
|
+
agentName: rotation.old.agentName
|
|
1981
|
+
});
|
|
1982
|
+
rotation.revokeCreatedAt = now;
|
|
1983
|
+
rotation.revokeExpiresAt = now + APPROVAL_MAX_AGE_MS;
|
|
1984
|
+
await atomicWrite(path, JSON.stringify(rotation));
|
|
1985
|
+
if (input.revokeSignature)
|
|
1986
|
+
throw new AgentCliError(
|
|
1987
|
+
"CONFIG_INVALID",
|
|
1988
|
+
"Revoke payload expired; sign the regenerated payload",
|
|
1989
|
+
{
|
|
1990
|
+
expiresAt: rotation.revokeExpiresAt,
|
|
1991
|
+
revokeApproval: rotation.revoke
|
|
1992
|
+
}
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
const refreshSigned = async (kind, signature) => {
|
|
1996
|
+
try {
|
|
1997
|
+
await submitApproval(current.network, rotation[kind], signature);
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
if (!(error instanceof AgentCliError) || !error.details?.nonceRejected)
|
|
2000
|
+
throw error;
|
|
2001
|
+
const now = Date.now();
|
|
2002
|
+
rotation[kind] = await newApproval(current, "approveAgent", {
|
|
2003
|
+
agentAddress: kind === "approval" ? rotation.next.agentAddress : "0x" + "0".repeat(40),
|
|
2004
|
+
agentName: kind === "approval" ? `${rotation.next.agentName} valid_until ${rotation.next.agentExpiry}` : rotation.old.agentName
|
|
2005
|
+
});
|
|
2006
|
+
if (kind === "approval") {
|
|
2007
|
+
rotation.approvalCreatedAt = now;
|
|
2008
|
+
rotation.approvalExpiresAt = now + APPROVAL_MAX_AGE_MS;
|
|
2009
|
+
} else {
|
|
2010
|
+
rotation.revokeCreatedAt = now;
|
|
2011
|
+
rotation.revokeExpiresAt = now + APPROVAL_MAX_AGE_MS;
|
|
2012
|
+
}
|
|
2013
|
+
await atomicWrite(path, JSON.stringify(rotation));
|
|
2014
|
+
throw new AgentCliError(
|
|
2015
|
+
"CONFIG_INVALID",
|
|
2016
|
+
"Approval nonce was rejected; sign the regenerated payload",
|
|
2017
|
+
{
|
|
2018
|
+
expiresAt: kind === "approval" ? rotation.approvalExpiresAt : rotation.revokeExpiresAt,
|
|
2019
|
+
approval: rotation[kind]
|
|
2020
|
+
}
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
if (input.signature) {
|
|
2025
|
+
await refreshSigned("approval", input.signature);
|
|
2026
|
+
await rotationHandoff(rotation, {
|
|
2027
|
+
verify: async () => {
|
|
2028
|
+
const agents = await hlInfo(current.network, "extraAgents", { user: current.masterAddress });
|
|
2029
|
+
return agents.some(
|
|
2030
|
+
(a) => a.address.toLowerCase() === rotation.next.agentAddress?.toLowerCase() && a.validUntil >= rotation.next.agentExpiry
|
|
2031
|
+
);
|
|
2032
|
+
},
|
|
2033
|
+
swap: async (nextConfig) => {
|
|
2034
|
+
const saved = await updateConfig(
|
|
2035
|
+
context.configPath ?? configPath(),
|
|
2036
|
+
(latest) => {
|
|
2037
|
+
copyAgentFields(latest, nextConfig);
|
|
2038
|
+
latest.approved = true;
|
|
2039
|
+
latest.revoked = false;
|
|
2040
|
+
}
|
|
2041
|
+
);
|
|
2042
|
+
const signer = await openSigner(saved);
|
|
2043
|
+
if (activeRunner) {
|
|
2044
|
+
activeRunner.signer = signer;
|
|
2045
|
+
copyAgentFields(activeRunner.config, saved);
|
|
2046
|
+
activeRunner.config.approved = true;
|
|
2047
|
+
activeRunner.config.revoked = false;
|
|
2048
|
+
}
|
|
2049
|
+
await events.append("runner_state", {
|
|
2050
|
+
rotation: {
|
|
2051
|
+
old: rotation.old.agentAddress,
|
|
2052
|
+
next: nextConfig.agentAddress
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
},
|
|
2056
|
+
persist: () => atomicWrite(path, JSON.stringify(rotation))
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
if (input.revokeSignature) {
|
|
2060
|
+
if (!rotation.swapped)
|
|
2061
|
+
throw new AgentCliError(
|
|
2062
|
+
"POLICY_REJECTED",
|
|
2063
|
+
"Approve and swap new agent before revoking old"
|
|
2064
|
+
);
|
|
2065
|
+
await refreshSigned("revoke", input.revokeSignature);
|
|
2066
|
+
rotation.revoked = true;
|
|
2067
|
+
await atomicWrite(path, JSON.stringify(rotation));
|
|
2068
|
+
}
|
|
2069
|
+
return {
|
|
2070
|
+
oldAgent: rotation.old.agentAddress,
|
|
2071
|
+
newAgent: rotation.next.agentAddress,
|
|
2072
|
+
swapped: rotation.swapped,
|
|
2073
|
+
oldRevoked: rotation.revoked,
|
|
2074
|
+
requiresSignature: !rotation.swapped,
|
|
2075
|
+
approvalExpiresAt: rotation.approvalExpiresAt ?? null,
|
|
2076
|
+
revokeExpiresAt: rotation.swapped ? rotation.revokeExpiresAt ?? null : void 0,
|
|
2077
|
+
approval: rotation.approval,
|
|
2078
|
+
...rotation.swapped ? { revokeApproval: rotation.revoke } : {}
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
// src/commands/revoke.ts
|
|
2084
|
+
import { z as z14 } from "zod";
|
|
2085
|
+
import { join as join6 } from "path";
|
|
2086
|
+
function markRevoked(config) {
|
|
2087
|
+
config.revoked = true;
|
|
2088
|
+
config.approved = false;
|
|
2089
|
+
return config;
|
|
2090
|
+
}
|
|
2091
|
+
register("revoke", {
|
|
2092
|
+
description: "Disable local agent and prepare master-signed exchange revocation",
|
|
2093
|
+
schema: z14.object({
|
|
2094
|
+
cancelOrders: z14.boolean().default(false),
|
|
2095
|
+
signature: z14.string().optional()
|
|
2096
|
+
}),
|
|
2097
|
+
execute: async (input, context) => {
|
|
2098
|
+
const config = await commandConfig(context);
|
|
2099
|
+
assertLiveMode(config, "revoke");
|
|
2100
|
+
assertApprovedNetwork(config);
|
|
2101
|
+
if (!config.agentAddress)
|
|
2102
|
+
throw new AgentCliError("AGENT_NOT_APPROVED", "No agent to revoke");
|
|
2103
|
+
if (input.cancelOrders && !config.revoked)
|
|
2104
|
+
await cancelOrders(config, cancelSchema.parse({ all: true }));
|
|
2105
|
+
const path = join6(
|
|
2106
|
+
stateDir(),
|
|
2107
|
+
`revoke-${config.network}-${config.agentAddress}.json`
|
|
2108
|
+
);
|
|
2109
|
+
const refresh = () => newApproval(config, "approveAgent", {
|
|
2110
|
+
agentAddress: "0x" + "0".repeat(40),
|
|
2111
|
+
agentName: config.agentName
|
|
2112
|
+
});
|
|
2113
|
+
const pending = await resolvePendingApproval(path, refresh);
|
|
2114
|
+
let exchangeRevoked = false;
|
|
2115
|
+
if (input.signature) {
|
|
2116
|
+
await submitFreshApproval(
|
|
2117
|
+
config.network,
|
|
2118
|
+
path,
|
|
2119
|
+
pending,
|
|
2120
|
+
input.signature,
|
|
2121
|
+
refresh
|
|
2122
|
+
);
|
|
2123
|
+
exchangeRevoked = true;
|
|
2124
|
+
config.authorizationKeyRef = void 0;
|
|
2125
|
+
config.privyWalletId = void 0;
|
|
2126
|
+
config.eoaKeyRef = void 0;
|
|
2127
|
+
}
|
|
2128
|
+
markRevoked(config);
|
|
2129
|
+
await saveConfig(config, context.configPath);
|
|
2130
|
+
if (activeRunner) {
|
|
2131
|
+
activeRunner.signer = void 0;
|
|
2132
|
+
Object.assign(activeRunner.config, config);
|
|
2133
|
+
}
|
|
2134
|
+
return {
|
|
2135
|
+
agentAddress: config.agentAddress,
|
|
2136
|
+
locallyRevoked: true,
|
|
2137
|
+
exchangeRevoked,
|
|
2138
|
+
requiresSignature: !exchangeRevoked,
|
|
2139
|
+
expiresAt: pending.expiresAt,
|
|
2140
|
+
approval: pending.approval
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
// src/commands/audit.ts
|
|
2146
|
+
import { z as z15 } from "zod";
|
|
2147
|
+
register("audit", {
|
|
2148
|
+
description: "Verify and query the hash-chained trading audit",
|
|
2149
|
+
schema: z15.object({
|
|
2150
|
+
intent: z15.string().optional(),
|
|
2151
|
+
intentId: z15.string().optional(),
|
|
2152
|
+
cloid: z15.string().optional()
|
|
2153
|
+
}),
|
|
2154
|
+
execute: async (input, context) => {
|
|
2155
|
+
const entries = await readAudit(
|
|
2156
|
+
auditStoreFor(await resolveMode(context)).path
|
|
2157
|
+
);
|
|
2158
|
+
verifyAudit(entries);
|
|
2159
|
+
return entries.filter(
|
|
2160
|
+
(entry) => (!input.intent && !input.intentId || entry.intentId === (input.intentId ?? input.intent)) && (!input.cloid || entry.cloid === input.cloid)
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
// src/commands/skill.ts
|
|
2166
|
+
import { z as z16 } from "zod";
|
|
2167
|
+
import { homedir } from "os";
|
|
2168
|
+
import { join as join7 } from "path";
|
|
2169
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2170
|
+
|
|
2171
|
+
// src/core/skill-content.ts
|
|
2172
|
+
var cliVersion = "0.4.0";
|
|
2173
|
+
var skillContent = '---\nname: agent-cli\ndescription: Operate the agent-cli Hyperliquid trading CLI, including managed-agent onboarding, paper trading against real market data, funding, orders, event monitoring, rotation, and normalized error recovery.\nmetadata:\n version: "0.4.0"\n---\n\nUse `agent-cli` for the user\'s authorized Hyperliquid account operations. Obtain\ncommand schemas with `agent-cli schema COMMAND`; arguments can be flags or\n`--json \'{...}\'`, or a JSON object on stdin. Ordinary commands print exactly one\nJSON object: `{ok:true,data,mode,paper}` or\n`{ok:false,code,message,retryable,details,mode,paper}`; `mode` is `"live"` or\n`"paper"` and `paper` is `true` in paper mode.\nLogs/QR appear on stderr. `events --follow` and `logs` explicitly stream JSON lines.\nUse `--network testnet` during testing; never silently change to mainnet.\n\n## Paper first, then fund and go live\n\nStart every strategy in paper mode: `agent-cli --paper ...`, `config set mode\npaper`, or `run --paper`. Paper mode runs against real mainnet Hyperliquid\npublic market data (info + WS l2Book/trades/bbo, meta, funding) and never signs\nor submits anything \u2014 no Privy session, `approveAgent`, or deposit is needed.\n`order`/`cancel`/`modify`/`leverage` keep the same validation, risk limits,\nbuilder fee and audit trail; a local simulator fills them (book-walking market\norders, resting limits, reduce-only, TP/SL, fees, funding, margin/liquidation\napproximation). State is restartable under `~/.bloxwap/paper/`; `paper summary`\nreports PnL/fees/funding, `paper reset --yes` clears it. Envelopes and events\ncarry `mode`/`paper`, paper state never mixes with live state, and going live\nrequires `config set mode live --yes`. Only after the paper results are proven,\ncontinue with live onboarding below and real funding.\n\n## Onboarding sequence\n\n1. The user owns all settings and chooses their own Privy app or dedicated EOA\n agent; no wallet or credentials are bundled. For Privy, set `signerType` to\n `privy` and configure the app ID and `privyAppSecretRef`. For EOA, set\n `signerType` to `eoa` and configure `eoaKeyRef`, e.g. `env:BLOXWAP_EOA_KEY`\n or a mode-0600 `file:` path.\n The user creates the key and sets the secret outside the conversation on their\n own machine or runner. Never accept raw keys in CLI input or use the master EOA\n as the agent. Master-wallet approval always remains in the user\'s external wallet.\n The default Bloxwap builder is `0x71b09A08257078A4d642F7Dd7315e656c837329a`,\n with `f=0` (no builder fee). The default referral code is `BLOXWAP`; override\n with `config set referralCode` or `BLOXWAP_REFERRAL_CODE`. Optional non-zero\n builder fees use `config set builder.address` / `config set builder.fee` or\n `BLOXWAP_BUILDER_ADDRESS` / `BLOXWAP_BUILDER_FEE`.\n2. Run `init --master-address 0x... --network testnet`. Show the returned approval\n payload/deeplink/QR. The user signs in their master wallet; submit only the\n resulting signature with `init --signature 0x...`. After agent approval, init\n applies the configured referral if the account has none and leaves existing\n referrals unchanged. Builder approval is needed only for a non-zero configured\n fee. Continue only after `approved:true`.\n3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address,\n origin chain/token, minimum, fees, expected received amount and expiry. On\n testnet, do not treat the default Arbitrum mainnet chain/token as a test route:\n use an explicitly supported test chain/token and `originDecimals`, or the\n credential-gated CI harness with mocked Relay and mocked Hyperliquid orders.\n If no test route is known, stop funding and report the missing route. The user\n transfers the quoted origin token. If `signatureSteps` require signatures,\n show the exact payload and submit each via `deposit` in returned order.\n4. Poll `status` until each active deposit is `settled`, for at most 12 polls\n or 5 minutes, whichever comes first. Start with 1-second delays, double to a\n 15-second cap, and stop with an explicit pending report if the budget expires.\n Settlement requires Relay success and a positive Hyperliquid ledger deposit\n (`userNonFundingLedgerUpdates`), not an unchanged equity balance. Stop on\n failed, refunded, expired, or abandoned deposits and explain their status.\n Quotes that never receive funds become `expired` 15 minutes after `expiresAt`.\n `deposit --abandon` marks an unfinished deposit `abandoned`. Do not place\n risk-increasing orders while a deposit is awaiting funds or pending. `cancel`\n and reduce-only orders still run.\n5. Run `run --detach`, then monitor `events --follow --since SEQ`. Persist the\n last handled sequence; replay after reconnect. An unfilled or rejected order\n remains incomplete: do not claim the requested trade succeeded. A fill updates the trading\n plan, an error follows the catalog below, and expiry warnings prompt rotation\n using external master-wallet approval.\n\n## Completion and approval boundaries\n\nRead this skill and query schemas before acting. Keep a progress record with the\nnetwork, current stage, deposit IDs, last event sequence and order cloid/oid;\nrecord public IDs only. Missing selected-signer settings, a wallet signature,\nfunding route or settled balance is a blocked stage. Report the exact next action\nwithout requesting any secret in chat or moving to the next stage. Repeat `init`\nto resume persisted approvals rather than creating another agent. Approval and\nrevoke payloads include `expiresAt` and are regenerated after 24 hours, or when\nHyperliquid rejects the nonce; sign the regenerated payload. An `ok:true`\ntransport envelope does not imply exchange acceptance. Every live or paper\n`order` carries a `cloid` (generated when omitted) on the success and on the\nerror. A mutation timeout or disconnect is `NETWORK_ERROR` with `retryable:\nfalse` and `details.outcome: "unknown"`. Retry that same `intentId` or `cloid`;\nthe CLI reuses the cloid and will not place a second order. Do not mint a new\ncloid for the same intent.\n\nFor EOA rotation, the user sets a new local key and supplies only its reference\nwith `rotate --eoa-key-ref env:NEW_EOA_KEY`. For either signer, verify\n`swapped:true` before asking the external wallet to sign old agent revocation. Require `oldRevoked:true` before claiming rotation complete.\n`revoke` immediately disables local signing; only `exchangeRevoked:true` confirms\nthat the signed exchange revocation was submitted. Inspect `status` to verify the\nagent is absent from exchange approvals. Cancel outstanding orders before final\nrevocation, and report any cancellation or cleanup failure explicitly.\n\n## Commands and example envelopes\n\nExamples show representative `data`, not live balances or service promises.\nFull inputs/outputs are discoverable with `schema`; amounts/prices are strings.\n\n| Input | Example output |\n| --- | --- |\n| `--help` | `{"ok":true,"data":{"commands":[{"name":"status","description":"Read account status"}],"globalFlags":["--json","--config","--network","--pretty"]}}` |\n| `schema order` | `{"ok":true,"data":{"input":{"type":"object"},"output":{"oneOf":[]}}}` (actual schemas include properties) |\n| `config get network` | `{"ok":true,"data":"testnet"}` |\n| `config set limits.maxLeverage 5` | `{"ok":true,"data":{"version":1,"limits":{"maxLeverage":5}}}` |\n| `config validate` | `{"ok":true,"data":{"valid":true,"version":1}}` |\n| `init --master-address 0x...` | `{"ok":true,"data":{"approved":false,"requiresSignature":true,"approvals":{"agent":{"typedData":{}}},"qrAscii":"..."}}` |\n| `fund --amount 10 --no-qr` | `{"ok":true,"data":{"depositId":"...","depositAddress":"0x...","expectedReceived":"100000000","builderFeeRate":"0%","builderAddress":"0x71b09A08257078A4d642F7Dd7315e656c837329a","state":"awaiting_funds","qrAscii":null}}` |\n| `deposit --deposit-id ID` | `{"ok":true,"data":{"depositId":"ID","steps":[{"id":"step:0","requiresSignature":true,"payload":{}}]}}` |\n| `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{"ok":true,"data":{"depositId":"ID","steps":[]}}` |\n| `status` | `{"ok":true,"data":{"balance":{"accountValue":"100"},"agent":{"approved":true,"daysRemaining":"179"},"deposits":[],"policy":{"id":"...","version":"2"},"runner":{"pid":"123"}}}` |\n| `balance` | `{"ok":true,"data":{"accountValue":"100","accountValueUnits":"10000000000","withdrawable":"96","marginUsed":"4"}}` |\n| `market --coin BTC` | `{"ok":true,"data":{"coin":"BTC","asset":"0","szDecimals":"5","maxLeverage":"40","mid":"20000"}}` |\n| `positions` | `{"ok":true,"data":[{"coin":"BTC","size":"0.001","entryPrice":"20000","leverage":"5","marginMode":"cross"}]}` |\n| `orders` | `{"ok":true,"data":[{"coin":"BTC","oid":"123","sz":"0.001","limitPx":"20000","side":"B"}]}` |\n| `order --json \'{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc","cloid":"0x11111111111111111111111111111111"}\'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"resting":{"oid":123}}]}}},"builderFeeRate":"0%","builderAddress":"0x71b09A08257078A4d642F7Dd7315e656c837329a","cloid":"0x..."}}` |\n| `order --json \'{"coin":"BTC","side":"sell","size":"0.001","type":"market","slippage":0.01,"reduceOnly":true}\'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"filled":{"oid":123}}]}}}}}` |\n| `cancel --oid 123` or `cancel --all` | `{"ok":true,"data":{"cancelled":["123"],"result":{"status":"ok"}}}` |\n| `modify --json \'{"modifications":[{"oid":123,"price":"20100","size":"0.001"}]}\'` | `{"ok":true,"data":{"status":"ok"}}` |\n| `leverage --coin BTC --leverage 5 --mode isolated` | `{"ok":true,"data":{"status":"ok"}}` |\n| `run --detach` | `{"ok":true,"data":{"pid":123,"startedAt":1700000000000,"network":"testnet"}}` |\n| `events --since 12 --types \'["fill"]\'` | `{"ok":true,"data":[{"seq":13,"timestamp":"2026-01-01T00:00:00.000Z","type":"fill","data":{"tid":1}}]}` |\n| `events --follow` | `{"seq":13,"timestamp":"2026-01-01T00:00:00.000Z","type":"fill","data":{"tid":1}}` (one line per event) |\n| `logs --follow --level warn` | `{"level":"warn","requestId":"...","message":"Risk rejected","time":"..."}` (JSON lines) |\n| `paper reset --yes` | `{"ok":true,"data":{"paper":true,"mode":"paper","reset":true,"startingBalance":"10000"},"mode":"paper","paper":true}` |\n| `paper summary` | `{"ok":true,"data":{"paper":true,"mode":"paper","equity":"10000","netPnl":"0","feesPaid":"0","fundingPaid":"0","trades":0,"positions":[]}}` |\n| `order --paper --json \'{"coin":"BTC","side":"buy","size":"0.001","type":"market"}\'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"filled":{"oid":1,"totalSz":"0.001","avgPx":"20000","status":"filled"}}]}}},"builderFeeRate":"0%","builderAddress":"0x71b09A08257078A4d642F7Dd7315e656c837329a","cloid":"0x22222222222222222222222222222222"},"mode":"paper","paper":true}` |\n| `run --paper --detach` | `{"ok":true,"data":{"pid":123,"startedAt":1700000000000,"network":"mainnet","paper":true}}` |\n| `rotate` then `rotate --signature 0x...` | `{"ok":true,"data":{"oldAgent":"0x...","newAgent":"0x...","swapped":true,"oldRevoked":false,"revokeApproval":{}}}` |\n| `rotate --revoke-signature 0x...` | `{"ok":true,"data":{"swapped":true,"oldRevoked":true}}` |\n| `revoke --cancel-orders` | `{"ok":true,"data":{"locallyRevoked":true,"exchangeRevoked":false,"requiresSignature":true,"approval":{}}}` |\n| `revoke --signature 0x...` | `{"ok":true,"data":{"locallyRevoked":true,"exchangeRevoked":true}}` |\n| `audit --intent ID` | `{"ok":true,"data":[{"intentId":"ID","stage":"decision","data":{"allowed":true},"previousHash":"...","hash":"..."}]}` |\n| `skill install --target codex` | `{"ok":true,"data":{"target":"codex","path":".../agent-cli/SKILL.md","version":"0.4.0"}}` |\n\nBefore an order, query `market --coin BTC` for `mid`, `szDecimals`, asset and\nmarket leverage ceiling. Compute size/notional with decimal strings/bigints,\nusing the user-approved limit or the bounded IOC execution price. A valid schema\nalone does not establish that an order is tradable or within the budget.\n\nUse `--intent-id ID --intent "reason"` on trading actions to connect the user\'s\nreasoning to the audit. The CLI binds that intent to one cloid. Risk checks use\nthe account leverage Hyperliquid will apply and fail closed if it cannot be\nread. Live and paper mutations are serialized per account. `--live` and\n`--paper` select the mode explicitly; a running runner rejects a command whose\nmode does not match the runner or the saved config (`CONFIG_INVALID`) until the\nrunner is restarted. Trading, rotate, and revoke reject a network other than\n`approvedNetwork`.\nFor TP/SL use `type:"tp"|"sl"` and `triggerPrice` with size/price/reduceOnly;\nmarket orders use an explicit slippage-bounded IOC, not an unbounded market order.\n\n## Error recovery\n\n| Code | Action |\n| --- | --- |\n| RATE_LIMIT, NETWORK_ERROR | At most 3 read retries with exponential backoff. A mutation with `details.outcome` `"unknown"` is not retryable: retry the same `intentId` or `cloid` only, which the CLI reconciles. Stop if that result is still unknown |\n| NONCE_COLLISION | Retry using a newly issued nonce. A stale approval nonce is not retryable; sign the regenerated payload. A lock held by a live process is `INTERNAL` and is not retryable |\n| DEPOSIT_PENDING | Poll status until settled, expired, or abandoned. `cancel` and reduce-only orders still run. `deposit --abandon` stops a quote you will not fund |\n| RUNNER_NOT_RUNNING | Start runner or use configured direct fallback |\n| INSUFFICIENT_MARGIN | Reduce exposure or request authorized funding |\n| POLICY_REJECTED | Stop and revise intent within limits; do not bypass policy |\n| AGENT_EXPIRED | Rotate with master-wallet approval |\n| AGENT_NOT_APPROVED | Complete init/approval or explain revocation |\n| INVALID_SIZE, INVALID_PRICE | Correct input using market precision |\n| UNKNOWN_MARKET | Refresh metadata; use an allowed market |\n| ORDER_NOT_FOUND | Refresh orders before changing intent. An unknown or expired agent is `AGENT_NOT_APPROVED` or `AGENT_EXPIRED`, not this code |\n| CONFIG_INVALID | Correct configuration/input; never insert secrets |\n| UPSTREAM_ERROR, INTERNAL | Stop, inspect redacted details/logs and report failure |\n\n## Gotchas\n\nHyperCore perps USDC uses **8 decimals**; EVM USDC uses **6**. The quote\'s origin\ndecimals must match `originDecimals` or `fund` returns `CONFIG_INVALID`. Preserve\nstrings and bigints. Some deposits require **two distinct signatures**; resume\nstored steps rather than restarting the route. Agent expiry is at most **180 days**.\nFills in `events.jsonl` are delivered once per exchange `tid`, including\nfills that happen while the runner is down; dedupe by `tid` as well as `seq`.\nNever\nask for, paste, log or import a master private key or seed. EOA agent keys are\naccepted only through user-configured local references, never command input.\nThe agent is trade-only; local limits include markets, size/notional, leverage,\ndaily loss and open orders \u2014 in paper mode these limits are enforced exactly the\nsame way. Privy sees an L1 action hash, so it cannot independently inspect\nnotional/action contents; EOA agents have no remote Privy policy. Hyperliquid\nagent permissions and local checks enforce those boundaries for both signers. State is always `~/.bloxwap`, env prefix `BLOXWAP_`; paper\nstate is `~/.bloxwap/paper/` and is never read by live commands (or vice\nversa). Envelopes and events always state `mode`/`paper`.\n\n\n## Verification boundaries\n\nOffline skill tests validate the documented command examples against CLI schemas.\nMocked lifecycle tests exercise resumable approvals, pending funding, events\nand failure handling. These checks do **not** prove a live agent or\nexchange run. The nightly testnet lifecycle uses only a company-owned disposable\nCI master account and separate test Privy app, live Privy and Hyperliquid\napprovals/revocation, and explicitly mocked Relay plus mocked Hyperliquid orders\nthat never reach the exchange. The dedicated master account still needs a positive\ntest USDC balance because live approvals require prior test collateral.\nThe same harness supports a caller-controlled external wallet approval adapter\non a clean QA runner; its private key stays in that wallet. It never transfers\nreal funds. Missing CI credentials fail the job; a skipped or\nmocked test must never be reported as live validation. Record the workflow run,\ncommit, lifecycle report, event and audit artifacts before claiming a testnet pass.\nSee `docs/agent-workflow.md` and `docs/testnet.md` in the repository for setup.\n';
|
|
2174
|
+
|
|
2175
|
+
// src/commands/skill.ts
|
|
2176
|
+
async function installSkill(target, options = {}) {
|
|
2177
|
+
const home = options.home ?? homedir();
|
|
2178
|
+
const root = target === "codex" ? options.codexHome ?? process.env.CODEX_HOME ?? join7(home, ".codex") : join7(home, ".claude");
|
|
2179
|
+
const path = join7(root, "skills", "agent-cli", "SKILL.md");
|
|
2180
|
+
try {
|
|
2181
|
+
const existing = await readFile6(path, "utf8");
|
|
2182
|
+
if (existing === skillContent)
|
|
2183
|
+
return { target, path, version: cliVersion, changed: false };
|
|
2184
|
+
if (!options.force)
|
|
2185
|
+
throw new AgentCliError(
|
|
2186
|
+
"CONFIG_INVALID",
|
|
2187
|
+
"Skill already exists with different content; use --force to replace"
|
|
2188
|
+
);
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
if (error.code !== "ENOENT") throw error;
|
|
2191
|
+
}
|
|
2192
|
+
await atomicWrite(path, skillContent);
|
|
2193
|
+
return { target, path, version: cliVersion, changed: true };
|
|
2194
|
+
}
|
|
2195
|
+
register("skill", {
|
|
2196
|
+
description: "Install packaged version-matched skill for Claude Code or Codex",
|
|
2197
|
+
schema: z16.object({
|
|
2198
|
+
args: z16.array(z16.string()).default(["install"]),
|
|
2199
|
+
target: z16.enum(["claude", "codex"]).default("codex"),
|
|
2200
|
+
force: z16.boolean().default(false)
|
|
2201
|
+
}),
|
|
2202
|
+
execute: async (input) => {
|
|
2203
|
+
if (input.args[0] !== "install")
|
|
2204
|
+
throw new AgentCliError(
|
|
2205
|
+
"CONFIG_INVALID",
|
|
2206
|
+
"Use skill install --target claude|codex"
|
|
2207
|
+
);
|
|
2208
|
+
return installSkill(input.target, {
|
|
2209
|
+
force: input.force
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
// src/commands/market.ts
|
|
2215
|
+
import { z as z17 } from "zod";
|
|
2216
|
+
register("market", {
|
|
2217
|
+
description: "Discover market asset, precision, leverage ceiling and current mid",
|
|
2218
|
+
schema: z17.object({ coin: z17.string() }),
|
|
2219
|
+
execute: async (input, context) => {
|
|
2220
|
+
const config = await commandConfig(context);
|
|
2221
|
+
try {
|
|
2222
|
+
const [market, mids] = await Promise.all([
|
|
2223
|
+
createClients(config.network).meta.lookup(input.coin),
|
|
2224
|
+
hlInfo(config.network, "allMids")
|
|
2225
|
+
]);
|
|
2226
|
+
const mid = mids[market.name];
|
|
2227
|
+
if (!mid)
|
|
2228
|
+
throw new AgentCliError("UNKNOWN_MARKET", "Market has no mid price");
|
|
2229
|
+
return {
|
|
2230
|
+
coin: market.name,
|
|
2231
|
+
asset: String(market.asset),
|
|
2232
|
+
szDecimals: String(market.szDecimals),
|
|
2233
|
+
maxLeverage: String(market.maxLeverage),
|
|
2234
|
+
mid
|
|
2235
|
+
};
|
|
2236
|
+
} catch (error) {
|
|
2237
|
+
throw mapHyperliquidError(error);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
// src/commands/paper.ts
|
|
2243
|
+
import { z as z18 } from "zod";
|
|
2244
|
+
async function paperSummary(store, data = paperData()) {
|
|
2245
|
+
const [state, marks] = await Promise.all([store.load(), data.marks()]);
|
|
2246
|
+
rolloverDay(state, marks, Date.now());
|
|
2247
|
+
const maxLeverageOf = async (coin) => data.maxLeverageOf(coin);
|
|
2248
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
2249
|
+
for (const coin of new Set(state.positions.map((p) => p.coin)))
|
|
2250
|
+
resolved.set(coin, await maxLeverageOf(coin));
|
|
2251
|
+
const equity = accountEquity(state, marks);
|
|
2252
|
+
const account = paperAccountState(state, marks, (coin) => resolved.get(coin) ?? 1);
|
|
2253
|
+
const unrealized = account.assetPositions.reduce(
|
|
2254
|
+
(sum, p) => sum + parseUsdc(p.position.unrealizedPnl, 18),
|
|
2255
|
+
0n
|
|
2256
|
+
);
|
|
2257
|
+
return {
|
|
2258
|
+
paper: true,
|
|
2259
|
+
mode: "paper",
|
|
2260
|
+
dataNetwork: paperDataNetwork,
|
|
2261
|
+
startingBalance: state.startingBalance,
|
|
2262
|
+
balance: state.balance,
|
|
2263
|
+
equity: formatUsdc(equity, 18),
|
|
2264
|
+
netPnl: formatUsdc(equity - parseUsdc(state.startingBalance, 18), 18),
|
|
2265
|
+
realizedPnl: state.realizedPnl,
|
|
2266
|
+
unrealizedPnl: formatUsdc(unrealized, 18),
|
|
2267
|
+
feesPaid: state.feesPaid,
|
|
2268
|
+
fundingPaid: state.fundingPaid,
|
|
2269
|
+
trades: state.trades.length,
|
|
2270
|
+
liquidations: state.liquidations,
|
|
2271
|
+
openOrders: state.orders.length,
|
|
2272
|
+
positions: account.assetPositions.map(({ position: p }) => ({
|
|
2273
|
+
coin: p.coin,
|
|
2274
|
+
size: p.szi,
|
|
2275
|
+
entryPrice: p.entryPx,
|
|
2276
|
+
markPrice: marks[p.coin] ?? null,
|
|
2277
|
+
unrealizedPnl: p.unrealizedPnl,
|
|
2278
|
+
liquidationPrice: liquidationPx(
|
|
2279
|
+
state.positions.find((position) => position.coin === p.coin),
|
|
2280
|
+
resolved.get(p.coin) ?? 1
|
|
2281
|
+
),
|
|
2282
|
+
leverage: String(p.leverage.value),
|
|
2283
|
+
marginMode: p.leverage.type
|
|
2284
|
+
}))
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
register("paper", {
|
|
2288
|
+
description: "Reset or summarize the local paper trading account",
|
|
2289
|
+
schema: z18.object({
|
|
2290
|
+
args: z18.array(z18.string()).default([]),
|
|
2291
|
+
yes: z18.boolean().default(false),
|
|
2292
|
+
balance: z18.number().positive().optional()
|
|
2293
|
+
}),
|
|
2294
|
+
execute: async (input, context) => {
|
|
2295
|
+
const [action = "summary"] = input.args;
|
|
2296
|
+
const config = await loadConfig(context.configPath);
|
|
2297
|
+
if (context.paper !== void 0)
|
|
2298
|
+
config.mode = context.paper ? "paper" : "live";
|
|
2299
|
+
const store = new PaperStore();
|
|
2300
|
+
if (action === "reset") {
|
|
2301
|
+
if (!input.yes)
|
|
2302
|
+
throw new AgentCliError(
|
|
2303
|
+
"CONFIG_INVALID",
|
|
2304
|
+
"paper reset clears all paper state; re-run with --yes to confirm"
|
|
2305
|
+
);
|
|
2306
|
+
const starting = input.balance ?? config.paper.startingBalance;
|
|
2307
|
+
await store.reset(starting);
|
|
2308
|
+
await atomicWrite(paperEvents.path, "");
|
|
2309
|
+
return {
|
|
2310
|
+
paper: true,
|
|
2311
|
+
mode: "paper",
|
|
2312
|
+
reset: true,
|
|
2313
|
+
startingBalance: String(starting)
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
if (action !== "summary")
|
|
2317
|
+
throw new AgentCliError(
|
|
2318
|
+
"CONFIG_INVALID",
|
|
2319
|
+
"Use paper summary or paper reset"
|
|
2320
|
+
);
|
|
2321
|
+
return paperSummary(store);
|
|
2322
|
+
}
|
|
2323
|
+
});
|
|
2324
|
+
|
|
2325
|
+
// src/cli.ts
|
|
2326
|
+
setRouting(routeToRunner);
|
|
2327
|
+
try {
|
|
2328
|
+
let stdin = "";
|
|
2329
|
+
if (!process.stdin.isTTY)
|
|
2330
|
+
for await (const chunk of process.stdin) {
|
|
2331
|
+
stdin += String(chunk);
|
|
2332
|
+
if (Buffer.byteLength(stdin) > 1048576)
|
|
2333
|
+
throw new AgentCliError("CONFIG_INVALID", "JSON input exceeds 1 MiB");
|
|
2334
|
+
}
|
|
2335
|
+
const result = await dispatch(process.argv.slice(2), stdin);
|
|
2336
|
+
if (!result.context.streaming || !result.body.ok)
|
|
2337
|
+
process.stdout.write(
|
|
2338
|
+
JSON.stringify(result.body, null, result.context.pretty ? 2 : void 0) + "\n"
|
|
2339
|
+
);
|
|
2340
|
+
process.exitCode = result.exitCode;
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
const normalized = normalizeError(error);
|
|
2343
|
+
process.stdout.write(JSON.stringify(normalized.toJSON()) + "\n");
|
|
2344
|
+
process.exitCode = normalized.exitCode;
|
|
2345
|
+
}
|