@candledottv/cli 0.11.23 → 0.11.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/dist/index.js +474 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @candledottv/cli
|
|
2
2
|
|
|
3
|
-
The Candle CLI (current version 0.11.
|
|
3
|
+
The Candle CLI (current version 0.11.25): authorize a device from your browser, keep your own keys
|
|
4
4
|
in an encrypted vault on this machine, hand an agent a TEE wallet it can trade, and manage API
|
|
5
5
|
keys, wallets, and setup health from the terminal. Zero runtime dependencies; the whole thing is
|
|
6
6
|
one self-contained `dist/index.js` that runs under plain Node.
|
|
@@ -122,7 +122,8 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
|
|
|
122
122
|
| `candle swap <from> <to> --amount <n>\|--percent <n> --wallet <tee> [--to <tee>] [--wait]` | Quote, confirm and swap through the TEE wallet's bound key; the first buy after a launch is this command, unless `candle launch --buy` made it. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana). SOL or USDC to ETH or USDG, or back, is a bridge through Relay into this key's own TEE wallet on the other chain (`--to` names it when there are several); `--wait` follows it for up to ten minutes. |
|
|
123
123
|
| `candle swap status <id> [--kind trade\|swap\|launch] [--wait]` | Read an operation without resending it; `--wait` follows a bridge until it ends. |
|
|
124
124
|
| `candle transfer --to <address\|wallet name\|vault> --asset <SOL\|USDC\|CNDL\|ETH\|USDG>\|--mint <mint>\|--token <0x...> --amount <decimal\|max> [--wallet <tee>] [--rpc-url <url>] [--yes]` | Move funds out of a TEE wallet through its bound Withdraw key, to the wallet's own pinned vault (`--to vault`, any token, `max` allowed, one SOL transfer at most the server's backstop ceiling, 50 SOL by default) or to another of the account's wallets you linked while signed in or marked trusted (a base asset, counted against the key's caps, which must include a per-asset cap and a USD `--tx-limit`). The asset decides the chain: ETH, USDG or `--token` moves from a Hood TEE wallet. The destination and its kind are shown before you confirm; Candle builds, the relay signs, Candle broadcasts. |
|
|
125
|
-
| `candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--buy <amount>] [--quote-asset <asset>] [--dex-version v3\|v4]` | Create a token from a TEE wallet. Without `--buy` it makes no first buy. `--buy <amount>` adds one from the same wallet, in whole units of the quote asset, reserved against the key's spend window before signing: in the launch transaction on Solana, as a last leg on Hood. It needs the server's `TEE_LAUNCH_BUYS_ENABLED`, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs `launch:write`, `allowLaunch` turned on for the wallet by the account owner, and the Pro or Max plan. On Free, launch from the embedded wallet instead, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood): the MCP tool `candle_launch_and_seed` or the SDK's `launch()`. The wallet decides the chain: a Hood TEE wallet launches on Hood and needs `--dex-version`. `--quote-asset` is sol, usdc or cndl on Solana, eth or usdg on Hood. |
|
|
125
|
+
| `candle launch --name <name> --symbol <symbol> --image <path>\|--image-url <url> --wallet <tee> [--buy <amount>] [--quote-asset <asset>] [--dex-version v3\|v4]` | Create a token from a TEE wallet. The image is `--image-url` (an `https://` URL) or `--image <path>` (a PNG, JPEG, GIF or WebP file on this machine, at most 10MB), never both; `--image` uploads the file before the confirm prompt, which makes it public and permanent, and launches with the URL that comes back. Without `--buy` it makes no first buy. `--buy <amount>` adds one from the same wallet, in whole units of the quote asset, reserved against the key's spend window before signing: in the launch transaction on Solana, as a last leg on Hood. It needs the server's `TEE_LAUNCH_BUYS_ENABLED`, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs `launch:write`, `allowLaunch` turned on for the wallet by the account owner, and the Pro or Max plan. On Free, launch from the embedded wallet instead, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood): the MCP tool `candle_launch_and_seed` or the SDK's `launch()`. The wallet decides the chain: a Hood TEE wallet launches on Hood and needs `--dex-version`. `--quote-asset` is sol, usdc or cndl on Solana, eth or usdg on Hood. |
|
|
126
|
+
| `candle image upload <path>` | Uploads a PNG, JPEG, GIF or WebP file, at most 10MB, and prints the `https://` URL Candle hosts it at, ready to be a launch's `imageUrl`. The type is read from the file's first bytes. The file becomes public and permanent. Needs `launch:write`; spends nothing and does not count against the launch cap. `--json` adds the content type and the size. |
|
|
126
127
|
| `candle pnl [--profile <name>]` | P&L: realized net, fees, unrealized and total, then open positions, the same figures the console shows. Without `--profile`, the account's books (every profile, the web app and the CLI, one ledger; needs the Read scope), with a By agent table (each key, then Manual, then the account total). With `--profile <name>`, that profile's key's share of the same P&L: realized on its sales and the open positions that belong to it, with a row per wallet that sums to the total. Each wallet is its own average-cost pool, and a recorded move between the account's wallets carries its cost. Both show a Solana block, a Hood block, then the combined figure, the closed positions as money made, minus money lost, plus partial sells, and a `CHAIN` column (and `WALLET`, where the API names it) on positions. Unpriced positions are never valued at zero; dust (worth under one cent) is marked and still counted. A bridge between Solana and Hood is not a trade, and bridge and Relay costs are not netted. DAMM v2 LP positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis) and vs holding (what the deposited tokens would be worth held); `Total` stays the console's token figure and `Total with LP` covers tokens and LP. |
|
|
127
128
|
| `candle portfolio [<filter>] [--chain solana\|hood] [--rpc-url <url>] [--evm-rpc-url <url>]` | Vault, TEE and embedded wallets on Solana and Hood in one table with chain, amount, price and value, subtotals per group and per chain, and a total. Solana vault and external balances are read over your own Solana RPC; Candle is sent only the mints, for prices. EVM vault keys are read over your own Hood RPC (`--evm-rpc-url`, else `CANDLE_EVM_RPC_URL`, else the built-in Hood RPC) for ETH and USDG only, priced by asset, never by address; an RPC on another chain is refused. An unpriced Hood token shows why (`no-market-row`, `stale-mark`, ...). `--chain` shows one chain. Each TEE wallet's DAMM v2 LP positions follow the tokens, valued by Candle at the same marks, and count in the total; one whose pool could not be read is shown as not read and the total says it is partial. |
|
|
128
129
|
| `candle plans` | The plans the API serves (`GET /api/v1/agent/plans`): each plan's monthly price, agent trade fee, Hyperliquid perps builder fee, the limits a new key gets, and one row per capability (buying and selling tokens not launched on Candle, self-launch, atomic launch, linked wallets, perps, limit orders). Every number is the server's; the CLI bundles none. Needs no key and sends none. `--json` is the server's body. Your own plan is `candle doctor`'s Plan row. |
|
|
@@ -134,7 +135,7 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
|
|
|
134
135
|
| `candle profile rename <old> <new>` | Renames a profile. |
|
|
135
136
|
| `candle profile remove <name> --yes` | Deletes a profile and its stored credentials. |
|
|
136
137
|
| `candle profile set <name> --rpc-url <url>\|--clear-rpc-url` | Sets or clears this profile's Solana RPC, used when `--rpc-url` and `CANDLE_SOLANA_RPC_URL` are not given. Only its host is ever shown. |
|
|
137
|
-
| `candle mcp [--tools <a,b,c>] [--read-only] [--print-config]` | Runs the Candle MCP server (built into this binary) with this CLI's stored API key and API URL in its environment, so an MCP client config is just `{"mcpServers": {"candle": {"command": "/Users/you/.local/bin/candle", "args": ["mcp"]}}}` -- the absolute path, because GUI hosts launch servers with the app's environment and never see your PATH. Run `--print-config` to print that block filled in for this install. `--read-only` starts it with no key and only the six keyless read tools; `--tools` pins an explicit allowlist. The server is bundled into the binary, so the host needs nothing else installed. |
|
|
138
|
+
| `candle mcp [--tools <a,b,c>] [--read-only] [--print-config]` | Runs the Candle MCP server (built into this binary) with this CLI's stored API key and API URL in its environment, so an MCP client config is just `{"mcpServers": {"candle": {"command": "/Users/you/.local/bin/candle", "args": ["mcp"]}}}` -- the absolute path, because GUI hosts launch servers with the app's environment and never see your PATH. Run `--print-config` to print that block filled in for this install. `--read-only` starts it with no key and only the six keyless read tools; `--tools` pins an explicit allowlist. The server is bundled into the binary, so the host needs nothing else installed. With `CANDLE_IMAGE_DIR` set to an absolute directory path in its environment, the two launch tools accept `imagePath` for an image file inside that directory; the upload is public and permanent, on a dry run too. |
|
|
138
139
|
| `candle doctor` | Runs a full health check (runtime, backend, credentials, API reachability, credential validity, wallet delegation, the install method, the account's plan and its trade fee, and whether the `candle-fido2` security key helper is beside the binary) as a PASS/FAIL/SKIP table meant for pasting into a bug report. Exits nonzero on any FAIL. |
|
|
139
140
|
| `candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]` | Verifies a release asset's Sigstore bundle against the trusted root compiled into this binary. No network, no credentials, and nothing else installed: the bundle carries the certificate and the transparency-log entry. `--identity` defaults to the release identity for the version in a `latest.json` sitting beside the bundle; `--issuer` defaults to GitHub Actions'. Prints `verified: <identity>` and exits 0, or the reason on stderr and exits 1. |
|
|
140
141
|
| `candle update [--check] [--to <tag>]` | Replaces this binary, and the `candle-fido2` helper beside it, with the latest signed release. Nothing is renamed until both downloads pass; the binary is renamed over the running binary only after its SHA-256 matches both SHA256SUMS and `latest.json` AND its Sigstore bundle verifies in process against that exact version's release workflow. `--check` reports what is available and installs nothing; `--to <tag>` pins a release (an older one installs, with a warning). A Homebrew or npm install is left alone, with the command that owns it printed instead. |
|
|
@@ -412,10 +413,21 @@ candle swap SOL USDC --amount 0.1 --wallet trading --client-trade-id lunch-1
|
|
|
412
413
|
candle swap <mint> SOL --percent 25 --wallet trading --rpc-url https://your-solana-rpc --yes --json
|
|
413
414
|
candle launch --name Example --symbol EX --image-url https://example.com/token.png \
|
|
414
415
|
--wallet trading --rpc-url https://your-solana-rpc --client-trade-id example-launch
|
|
416
|
+
candle launch --name Example --symbol EX --image ./token.png --wallet trading
|
|
417
|
+
candle image upload ./token.png
|
|
415
418
|
candle swap status lunch-1
|
|
416
419
|
candle swap status example-launch --kind launch --json
|
|
417
420
|
```
|
|
418
421
|
|
|
422
|
+
`--image <path>` launches with an image file on this machine: the CLI checks it (a PNG, JPEG,
|
|
423
|
+
GIF or WebP by its first bytes, at most 10MB) before any request, uploads it to Candle, and
|
|
424
|
+
builds the launch with the URL that comes back. The upload happens once, before the confirm
|
|
425
|
+
prompt and before the launch id is claimed, and it is a publication: the file is pinned to public
|
|
426
|
+
storage and stays there, with whatever metadata it carries, even if you decline or the launch
|
|
427
|
+
fails. A failed upload leaves the launch id unused, and re-running a started launch uploads
|
|
428
|
+
nothing. `candle image upload <path>` does the upload alone and prints the URL, for a launch made
|
|
429
|
+
somewhere else. `--image-url` takes only an `https://` URL.
|
|
430
|
+
|
|
419
431
|
`candle lp` (Meteora DAMM v2) follows the same shape: the server builds and quotes, you confirm,
|
|
420
432
|
the relay signs, and this machine broadcasts over `--rpc-url` before `/confirm` writes the ledger.
|
|
421
433
|
The bound key needs the opt-in `lp:write` scope. An open
|
package/dist/index.js
CHANGED
|
@@ -98,7 +98,10 @@ function resolveApiUrl(configuredApiUrl, env = process.env) {
|
|
|
98
98
|
return trimTrailingSlashes(resolved);
|
|
99
99
|
}
|
|
100
100
|
function buildHeaders(opts) {
|
|
101
|
-
const headers = {
|
|
101
|
+
const headers = {
|
|
102
|
+
"content-type": opts.raw?.contentType ?? "application/json",
|
|
103
|
+
accept: "application/json"
|
|
104
|
+
};
|
|
102
105
|
if (opts.auth === "device" && opts.credentials.deviceToken) {
|
|
103
106
|
headers.authorization = `Bearer ${opts.credentials.deviceToken}`;
|
|
104
107
|
} else if (opts.auth === "key" && opts.credentials.apiKey) {
|
|
@@ -156,7 +159,7 @@ async function apiRequest(path, opts) {
|
|
|
156
159
|
return { ok: false, status: 0, code: "INSECURE_API_URL", message: insecure, raw: undefined };
|
|
157
160
|
}
|
|
158
161
|
const headers = buildHeaders(opts);
|
|
159
|
-
const body = opts.body === undefined ? undefined : JSON.stringify(opts.body);
|
|
162
|
+
const body = opts.raw ? opts.raw.bytes : opts.body === undefined ? undefined : JSON.stringify(opts.body);
|
|
160
163
|
const doFetch = opts.fetch ?? fetch;
|
|
161
164
|
let response;
|
|
162
165
|
try {
|
|
@@ -61132,7 +61135,7 @@ var init_convert = __esm(() => {
|
|
|
61132
61135
|
});
|
|
61133
61136
|
|
|
61134
61137
|
// ../mcp/src/version.ts
|
|
61135
|
-
var SERVER_VERSION = "0.10.
|
|
61138
|
+
var SERVER_VERSION = "0.10.12";
|
|
61136
61139
|
|
|
61137
61140
|
// ../mcp/src/update-notice.ts
|
|
61138
61141
|
function newer(a, b) {
|
|
@@ -61167,6 +61170,213 @@ var init_update_notice = __esm(() => {
|
|
|
61167
61170
|
PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
|
|
61168
61171
|
});
|
|
61169
61172
|
|
|
61173
|
+
// ../mcp/src/image.ts
|
|
61174
|
+
import { constants as constants5 } from "node:fs";
|
|
61175
|
+
import { lstat as lstat2, open as open5, realpath } from "node:fs/promises";
|
|
61176
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, relative, sep as sep2 } from "node:path";
|
|
61177
|
+
function configuredImageDir(env) {
|
|
61178
|
+
return env.CANDLE_IMAGE_DIR?.trim() || undefined;
|
|
61179
|
+
}
|
|
61180
|
+
function sniffImageType2(bytes) {
|
|
61181
|
+
const at = (offset, signature) => signature.every((byte, i) => bytes[offset + i] === byte);
|
|
61182
|
+
if (at(0, [137, 80, 78, 71, 13, 10, 26, 10]))
|
|
61183
|
+
return "image/png";
|
|
61184
|
+
if (at(0, [255, 216, 255]))
|
|
61185
|
+
return "image/jpeg";
|
|
61186
|
+
if (at(0, [71, 73, 70, 56]) && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97)
|
|
61187
|
+
return "image/gif";
|
|
61188
|
+
if (at(0, [82, 73, 70, 70]) && at(8, [87, 69, 66, 80]))
|
|
61189
|
+
return "image/webp";
|
|
61190
|
+
return;
|
|
61191
|
+
}
|
|
61192
|
+
function isInside(dir, path) {
|
|
61193
|
+
const rel = relative(dir, path);
|
|
61194
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute2(rel);
|
|
61195
|
+
}
|
|
61196
|
+
function errno(error2) {
|
|
61197
|
+
return error2?.code;
|
|
61198
|
+
}
|
|
61199
|
+
async function realImageDir(imageDir, fs) {
|
|
61200
|
+
const invalid = () => new ImageRefusal("IMAGE_DIR_INVALID", `CANDLE_IMAGE_DIR must be an absolute path to an existing directory, and not the filesystem root. ${NOTHING_UPLOADED2}`);
|
|
61201
|
+
if (!isAbsolute2(imageDir))
|
|
61202
|
+
throw invalid();
|
|
61203
|
+
try {
|
|
61204
|
+
const real = await fs.realpath(imageDir);
|
|
61205
|
+
if (dirname11(real) === real || !(await fs.lstat(real, { bigint: true })).isDirectory())
|
|
61206
|
+
throw invalid();
|
|
61207
|
+
return real;
|
|
61208
|
+
} catch {
|
|
61209
|
+
throw invalid();
|
|
61210
|
+
}
|
|
61211
|
+
}
|
|
61212
|
+
async function missingInside(dirReal, path, fs) {
|
|
61213
|
+
const absent = (error2) => errno(error2) === "ENOENT" || errno(error2) === "ENOTDIR";
|
|
61214
|
+
for (let current = path;; ) {
|
|
61215
|
+
try {
|
|
61216
|
+
await fs.lstat(current, { bigint: true });
|
|
61217
|
+
return false;
|
|
61218
|
+
} catch (error2) {
|
|
61219
|
+
if (!absent(error2))
|
|
61220
|
+
return false;
|
|
61221
|
+
}
|
|
61222
|
+
const parent = dirname11(current);
|
|
61223
|
+
if (parent === current)
|
|
61224
|
+
return false;
|
|
61225
|
+
try {
|
|
61226
|
+
const real = await fs.realpath(parent);
|
|
61227
|
+
return real === dirReal || isInside(dirReal, real);
|
|
61228
|
+
} catch (error2) {
|
|
61229
|
+
if (!absent(error2))
|
|
61230
|
+
return false;
|
|
61231
|
+
current = parent;
|
|
61232
|
+
}
|
|
61233
|
+
}
|
|
61234
|
+
}
|
|
61235
|
+
async function readImageInDir(imageDir, imagePath, fs = nodeFs) {
|
|
61236
|
+
if (!isAbsolute2(imagePath))
|
|
61237
|
+
throw new ImageRefusal("MCP_VALIDATION", `imagePath must be an absolute path inside CANDLE_IMAGE_DIR; this server's working directory is not something a caller can know. ${NOTHING_UPLOADED2}`);
|
|
61238
|
+
const dirReal = await realImageDir(imageDir, fs);
|
|
61239
|
+
const outside = () => new ImageRefusal("IMAGE_OUTSIDE_DIR", OUTSIDE_IMAGE_DIR);
|
|
61240
|
+
const unreadable2 = (why) => new ImageRefusal("IMAGE_UNREADABLE", `Could not read the image at imagePath: ${why}. ${NOTHING_UPLOADED2}`);
|
|
61241
|
+
const tooLarge = () => new ImageRefusal("IMAGE_TOO_LARGE", `The file at imagePath is over 10MB, the largest image Candle hosts. ${NOTHING_UPLOADED2}`);
|
|
61242
|
+
let fileReal;
|
|
61243
|
+
try {
|
|
61244
|
+
fileReal = await fs.realpath(imagePath);
|
|
61245
|
+
} catch {
|
|
61246
|
+
throw await missingInside(dirReal, imagePath, fs) ? unreadable2("there is no file at that path") : outside();
|
|
61247
|
+
}
|
|
61248
|
+
if (!isInside(dirReal, fileReal))
|
|
61249
|
+
throw outside();
|
|
61250
|
+
let handle;
|
|
61251
|
+
try {
|
|
61252
|
+
handle = await fs.open(fileReal, constants5.O_RDONLY | (constants5.O_NOFOLLOW ?? 0) | (constants5.O_NONBLOCK ?? 0));
|
|
61253
|
+
} catch (error2) {
|
|
61254
|
+
throw errno(error2) === "ELOOP" ? outside() : unreadable2("it could not be opened");
|
|
61255
|
+
}
|
|
61256
|
+
let bytes;
|
|
61257
|
+
try {
|
|
61258
|
+
const opened = await handle.stat({ bigint: true });
|
|
61259
|
+
let named;
|
|
61260
|
+
try {
|
|
61261
|
+
if (await fs.realpath(fileReal) !== fileReal || !isInside(await realImageDir(imageDir, fs), fileReal))
|
|
61262
|
+
throw outside();
|
|
61263
|
+
named = await fs.lstat(fileReal, { bigint: true });
|
|
61264
|
+
} catch {
|
|
61265
|
+
throw outside();
|
|
61266
|
+
}
|
|
61267
|
+
if (named.dev !== opened.dev || named.ino !== opened.ino)
|
|
61268
|
+
throw outside();
|
|
61269
|
+
if (!opened.isFile())
|
|
61270
|
+
throw unreadable2("it is not a regular file");
|
|
61271
|
+
const chunks = [];
|
|
61272
|
+
let total = 0;
|
|
61273
|
+
try {
|
|
61274
|
+
while (total <= MAX_IMAGE_BYTES2) {
|
|
61275
|
+
const want = Math.min(READ_CHUNK_BYTES, MAX_IMAGE_BYTES2 + 1 - total);
|
|
61276
|
+
const chunk = new Uint8Array(want);
|
|
61277
|
+
const { bytesRead } = await handle.read(chunk, 0, want, null);
|
|
61278
|
+
if (bytesRead === 0)
|
|
61279
|
+
break;
|
|
61280
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
61281
|
+
total += bytesRead;
|
|
61282
|
+
}
|
|
61283
|
+
} catch {
|
|
61284
|
+
throw unreadable2("the read failed");
|
|
61285
|
+
}
|
|
61286
|
+
if (total > MAX_IMAGE_BYTES2)
|
|
61287
|
+
throw tooLarge();
|
|
61288
|
+
bytes = new Uint8Array(total);
|
|
61289
|
+
let offset = 0;
|
|
61290
|
+
for (const chunk of chunks) {
|
|
61291
|
+
bytes.set(chunk, offset);
|
|
61292
|
+
offset += chunk.length;
|
|
61293
|
+
}
|
|
61294
|
+
} finally {
|
|
61295
|
+
await handle.close().catch(() => {});
|
|
61296
|
+
}
|
|
61297
|
+
const contentType = sniffImageType2(bytes);
|
|
61298
|
+
if (!contentType)
|
|
61299
|
+
throw new ImageRefusal("IMAGE_TYPE_UNSUPPORTED", `The file at imagePath is not a PNG, JPEG, GIF or WebP image. The type is read from the file's first bytes, not its name. ${NOTHING_UPLOADED2}`);
|
|
61300
|
+
return { bytes, contentType };
|
|
61301
|
+
}
|
|
61302
|
+
async function resolveLaunchImage(args, imageDir, cfg, doFetch, fs = nodeFs) {
|
|
61303
|
+
const { imagePath, ...rest } = args;
|
|
61304
|
+
const hasPath = typeof imagePath === "string" && imagePath.length > 0;
|
|
61305
|
+
const hasUrl = typeof rest.imageUrl === "string" && rest.imageUrl.length > 0;
|
|
61306
|
+
const invalid = (message) => ({ refused: { code: "MCP_VALIDATION", message } });
|
|
61307
|
+
if (imageDir === undefined) {
|
|
61308
|
+
if (imagePath !== undefined)
|
|
61309
|
+
return invalid(`imagePath is not available on this server: no CANDLE_IMAGE_DIR is configured. Pass imageUrl, an https URL. ${NOTHING_UPLOADED2}`);
|
|
61310
|
+
return { args: rest };
|
|
61311
|
+
}
|
|
61312
|
+
if (hasPath && hasUrl)
|
|
61313
|
+
return invalid(`Pass exactly one of imageUrl or imagePath, not both. ${NOTHING_UPLOADED2}`);
|
|
61314
|
+
if (!hasPath && !hasUrl)
|
|
61315
|
+
return invalid("Pass imageUrl (an https URL) or imagePath (an absolute path to an image file inside CANDLE_IMAGE_DIR).");
|
|
61316
|
+
if (!hasPath)
|
|
61317
|
+
return { args: rest };
|
|
61318
|
+
if (!cfg.apiKey)
|
|
61319
|
+
return invalid(`CANDLE_AGENT_API_KEY is required to upload an image; the key needs the launch:write scope. ${NOTHING_UPLOADED2}`);
|
|
61320
|
+
let image;
|
|
61321
|
+
try {
|
|
61322
|
+
image = await readImageInDir(imageDir, imagePath, fs);
|
|
61323
|
+
} catch (error2) {
|
|
61324
|
+
if (error2 instanceof ImageRefusal)
|
|
61325
|
+
return { refused: { code: error2.code, message: error2.message } };
|
|
61326
|
+
throw error2;
|
|
61327
|
+
}
|
|
61328
|
+
let res;
|
|
61329
|
+
try {
|
|
61330
|
+
res = await doFetch(`${cfg.apiUrl.replace(/\/$/, "")}/api/v1/uploads/agent-image`, {
|
|
61331
|
+
method: "POST",
|
|
61332
|
+
headers: { "Content-Type": image.contentType, "x-api-key": cfg.apiKey },
|
|
61333
|
+
body: image.bytes
|
|
61334
|
+
});
|
|
61335
|
+
} catch (error2) {
|
|
61336
|
+
return {
|
|
61337
|
+
refused: {
|
|
61338
|
+
code: "MCP_TRANSPORT",
|
|
61339
|
+
message: `The image upload failed in transit (${error2 instanceof Error ? error2.message : String(error2)}). ` + "No launch was attempted. The upload may or may not have reached Candle; if it did, the file is public."
|
|
61340
|
+
}
|
|
61341
|
+
};
|
|
61342
|
+
}
|
|
61343
|
+
noteVersionHeaders(res);
|
|
61344
|
+
const text = await res.text();
|
|
61345
|
+
if (!res.ok)
|
|
61346
|
+
return { uploadFailed: text };
|
|
61347
|
+
let uploaded;
|
|
61348
|
+
try {
|
|
61349
|
+
uploaded = JSON.parse(text)?.imageUrl;
|
|
61350
|
+
} catch {
|
|
61351
|
+
uploaded = undefined;
|
|
61352
|
+
}
|
|
61353
|
+
if (typeof uploaded !== "string" || !uploaded.startsWith("https://"))
|
|
61354
|
+
return {
|
|
61355
|
+
refused: {
|
|
61356
|
+
code: "MCP_TRANSPORT",
|
|
61357
|
+
message: "Candle did not return an https URL for the uploaded image. No launch was attempted."
|
|
61358
|
+
}
|
|
61359
|
+
};
|
|
61360
|
+
return { args: { ...rest, imageUrl: uploaded }, uploaded };
|
|
61361
|
+
}
|
|
61362
|
+
function imageRefusalBody(refused, extra) {
|
|
61363
|
+
return JSON.stringify({ ...extra, success: false, error: { code: refused.code, message: refused.message } });
|
|
61364
|
+
}
|
|
61365
|
+
var MAX_IMAGE_BYTES2, OUTSIDE_IMAGE_DIR = "Image files must be inside the configured CANDLE_IMAGE_DIR.", NOTHING_UPLOADED2 = "Nothing was uploaded.", READ_CHUNK_BYTES, ImageRefusal, nodeFs, UPLOADED_IMAGE_NOTE = "imagePath was uploaded to imageUrl, which is public and permanent. To retry, or to launch after a dry run, pass this imageUrl instead of imagePath, so the file is not uploaded again.";
|
|
61366
|
+
var init_image = __esm(() => {
|
|
61367
|
+
init_update_notice();
|
|
61368
|
+
MAX_IMAGE_BYTES2 = 10 * 1024 * 1024;
|
|
61369
|
+
READ_CHUNK_BYTES = 256 * 1024;
|
|
61370
|
+
ImageRefusal = class ImageRefusal extends Error {
|
|
61371
|
+
code;
|
|
61372
|
+
constructor(code, message) {
|
|
61373
|
+
super(message);
|
|
61374
|
+
this.code = code;
|
|
61375
|
+
}
|
|
61376
|
+
};
|
|
61377
|
+
nodeFs = { realpath, lstat: lstat2, open: open5 };
|
|
61378
|
+
});
|
|
61379
|
+
|
|
61170
61380
|
// ../mcp/src/orchestrate.ts
|
|
61171
61381
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
61172
61382
|
function requireApiKey(cfg) {
|
|
@@ -61416,10 +61626,10 @@ async function executeTrade(args, cfg, doFetch) {
|
|
|
61416
61626
|
const wrapped = JSON.stringify({ clientTradeId, resolved, api: posted.body });
|
|
61417
61627
|
return posted.ok ? { text: wrapped } : { text: wrapped, isError: true };
|
|
61418
61628
|
}
|
|
61419
|
-
async function executeLaunchAndSeed(args, cfg, doFetch) {
|
|
61629
|
+
async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
|
|
61420
61630
|
const apiKey = requireApiKey(cfg);
|
|
61421
61631
|
const clientLaunchId = args.clientLaunchId ?? randomUUID6();
|
|
61422
|
-
const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...
|
|
61632
|
+
const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...imageFields } = args;
|
|
61423
61633
|
let buyAmount;
|
|
61424
61634
|
if (devBuy !== undefined) {
|
|
61425
61635
|
const q = quoteIdDecimals(args.quoteAsset, args.chain, { clientLaunchId });
|
|
@@ -61430,19 +61640,34 @@ async function executeLaunchAndSeed(args, cfg, doFetch) {
|
|
|
61430
61640
|
return converted.err;
|
|
61431
61641
|
buyAmount = converted.raw;
|
|
61432
61642
|
}
|
|
61643
|
+
const image = await resolveLaunchImage(imageFields, imageDir, cfg, doFetch, imageFs);
|
|
61644
|
+
if ("refused" in image)
|
|
61645
|
+
return { text: imageRefusalBody(image.refused, { clientLaunchId }), isError: true };
|
|
61646
|
+
if ("uploadFailed" in image) {
|
|
61647
|
+
let api2;
|
|
61648
|
+
try {
|
|
61649
|
+
api2 = JSON.parse(image.uploadFailed);
|
|
61650
|
+
} catch {
|
|
61651
|
+
api2 = image.uploadFailed;
|
|
61652
|
+
}
|
|
61653
|
+
return { text: JSON.stringify({ clientLaunchId, api: api2 }), isError: true };
|
|
61654
|
+
}
|
|
61655
|
+
const uploaded = image.uploaded === undefined ? {} : { imageUrl: image.uploaded, imageNote: UPLOADED_IMAGE_NOTE };
|
|
61433
61656
|
const path = dryRun ? "/api/v1/launch/headless/dry-run" : "/api/v1/launch/headless";
|
|
61434
61657
|
const posted = await postJson(`${base(cfg)}${path}`, apiKey, {
|
|
61435
|
-
...
|
|
61658
|
+
...image.args,
|
|
61436
61659
|
clientLaunchId,
|
|
61437
61660
|
...buyAmount !== undefined ? { buyAmount } : {}
|
|
61438
61661
|
}, doFetch);
|
|
61439
|
-
if ("thrown" in posted)
|
|
61440
|
-
|
|
61662
|
+
if ("thrown" in posted) {
|
|
61663
|
+
const failed = transportError("clientLaunchId", clientLaunchId, posted.thrown);
|
|
61664
|
+
return image.uploaded === undefined ? failed : { ...failed, text: JSON.stringify({ ...JSON.parse(failed.text), ...uploaded }) };
|
|
61665
|
+
}
|
|
61441
61666
|
if (!posted.ok) {
|
|
61442
|
-
return { text: JSON.stringify({ clientLaunchId, api: posted.body }), isError: true };
|
|
61667
|
+
return { text: JSON.stringify({ clientLaunchId, ...uploaded, api: posted.body }), isError: true };
|
|
61443
61668
|
}
|
|
61444
61669
|
if (dryRun) {
|
|
61445
|
-
return { text: JSON.stringify({ clientLaunchId, dryRun: true, api: posted.body }) };
|
|
61670
|
+
return { text: JSON.stringify({ clientLaunchId, dryRun: true, ...uploaded, api: posted.body }) };
|
|
61446
61671
|
}
|
|
61447
61672
|
const launch2 = posted.body;
|
|
61448
61673
|
let market = null;
|
|
@@ -61463,7 +61688,7 @@ async function executeLaunchAndSeed(args, cfg, doFetch) {
|
|
|
61463
61688
|
note = "launch confirmed; the follow-up market read failed, read it via candle_get_market";
|
|
61464
61689
|
}
|
|
61465
61690
|
return {
|
|
61466
|
-
text: JSON.stringify({ clientLaunchId, launch: launch2, market, ...note ? { note } : {} })
|
|
61691
|
+
text: JSON.stringify({ clientLaunchId, ...uploaded, launch: launch2, market, ...note ? { note } : {} })
|
|
61467
61692
|
};
|
|
61468
61693
|
}
|
|
61469
61694
|
async function executeSweep(args, cfg, doFetch) {
|
|
@@ -61554,7 +61779,7 @@ async function executionStatus(cfg, doFetch) {
|
|
|
61554
61779
|
get("/api/v1/agent/tier"),
|
|
61555
61780
|
get("/api/v1/agent/keys/self/limits")
|
|
61556
61781
|
]);
|
|
61557
|
-
const
|
|
61782
|
+
const unreadable2 = [
|
|
61558
61783
|
["wallets", wallets2],
|
|
61559
61784
|
["tier", tier],
|
|
61560
61785
|
["limits", limits]
|
|
@@ -61563,20 +61788,21 @@ async function executionStatus(cfg, doFetch) {
|
|
|
61563
61788
|
return {
|
|
61564
61789
|
text: JSON.stringify({
|
|
61565
61790
|
success: true,
|
|
61566
|
-
ready:
|
|
61791
|
+
ready: unreadable2.length === 0 ? true : undefined,
|
|
61567
61792
|
notice: tierBody?.maxExpired ? tierBody.maxExpiredNotice : undefined,
|
|
61568
61793
|
updateAvailable: updateAvailable() ?? undefined,
|
|
61569
|
-
unreadable:
|
|
61794
|
+
unreadable: unreadable2.length > 0 ? unreadable2.map(([name2]) => name2) : undefined,
|
|
61570
61795
|
wallets: wallets2.body,
|
|
61571
61796
|
tier: tier.body,
|
|
61572
61797
|
limits: limits.body
|
|
61573
61798
|
}, null, 2),
|
|
61574
|
-
...
|
|
61799
|
+
...unreadable2.length > 0 ? { isError: true } : {}
|
|
61575
61800
|
};
|
|
61576
61801
|
}
|
|
61577
61802
|
var SWEEP_BASE_ASSETS;
|
|
61578
61803
|
var init_orchestrate = __esm(() => {
|
|
61579
61804
|
init_convert();
|
|
61805
|
+
init_image();
|
|
61580
61806
|
init_update_notice();
|
|
61581
61807
|
SWEEP_BASE_ASSETS = {
|
|
61582
61808
|
solana: ["USDC", "CNDL", "SOL"],
|
|
@@ -62691,11 +62917,24 @@ function registerToolSubset(server, options) {
|
|
|
62691
62917
|
return;
|
|
62692
62918
|
server.registerTool(name2, { ...definition, ...options.metadata?.[name2] }, handler);
|
|
62693
62919
|
};
|
|
62920
|
+
const imageDir = configuredImageDir(env);
|
|
62694
62921
|
register("candle_launch_token", {
|
|
62695
62922
|
title: "Launch a token on Candle",
|
|
62696
|
-
description: "Launch a new token via the Candle headless launch API, from the account's embedded wallet. Works on every plan, Free included. Set dryRun: true to validate without spending anything.",
|
|
62697
|
-
inputSchema: launchTokenShape
|
|
62698
|
-
}, async (args) =>
|
|
62923
|
+
description: "Launch a new token via the Candle headless launch API, from the account's embedded wallet. Works on every plan, Free included. Set dryRun: true to validate without spending anything." + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
|
|
62924
|
+
inputSchema: imageDir ? localLaunchTokenShape : launchTokenShape
|
|
62925
|
+
}, async (args) => {
|
|
62926
|
+
const image = await resolveLaunchImage(args, imageDir, getConfig(), doFetch);
|
|
62927
|
+
if ("refused" in image) {
|
|
62928
|
+
return { content: [{ type: "text", text: imageRefusalBody(image.refused) }], isError: true };
|
|
62929
|
+
}
|
|
62930
|
+
if ("uploadFailed" in image)
|
|
62931
|
+
return { content: [{ type: "text", text: image.uploadFailed }], isError: true };
|
|
62932
|
+
const relayed = await callAndRelay("candle_launch_token", image.args, getConfig(), doFetch);
|
|
62933
|
+
if (image.uploaded === undefined)
|
|
62934
|
+
return relayed;
|
|
62935
|
+
const uploaded = JSON.stringify({ imageUrl: image.uploaded, note: UPLOADED_IMAGE_NOTE });
|
|
62936
|
+
return { ...relayed, content: [...relayed.content, { type: "text", text: uploaded }] };
|
|
62937
|
+
});
|
|
62699
62938
|
register("candle_get_market", {
|
|
62700
62939
|
title: "Get market state",
|
|
62701
62940
|
description: "Read Candle and indexed external markets, including indexed-but-not-routable tokens. No key needed. " + EXTERNAL_MARKET_DISCLOSURE + " " + "Read candleLaunched, launchpad, venue and trade.routable; jupiterOk and discovery flags are distinct. " + "Routability is stored eligibility, not a quote or permission. General quotes use POST /api/v1/trade/agent/quote. " + "Curve quotes and lifecycle describe Candle launches. MARKET_NOT_FOUND is a legacy code: read " + "error.routing.reason, error.discovery and sibling error.retryable. A curve-only 404 does not mean untradeable.",
|
|
@@ -62869,10 +63108,10 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
62869
63108
|
});
|
|
62870
63109
|
register("candle_launch_and_seed", {
|
|
62871
63110
|
title: "Launch a token and seed it",
|
|
62872
|
-
description: "Launch a new token from the account's embedded wallet, with an optional dev-buy seed (in the " + "launch transaction on solana, a best-effort follow-up on hood). Works on every plan, Free included (this is not the atomic launch). Then " + "return the fresh market state and token links in one result. MOVES REAL FUNDS unless " + "dryRun. Seeds above the platform dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH); " + "launch, then top up with candle_trade.",
|
|
62873
|
-
inputSchema: launchAndSeedShape
|
|
63111
|
+
description: "Launch a new token from the account's embedded wallet, with an optional dev-buy seed (in the " + "launch transaction on solana, a best-effort follow-up on hood). Works on every plan, Free included (this is not the atomic launch). Then " + "return the fresh market state and token links in one result. MOVES REAL FUNDS unless " + "dryRun. Seeds above the platform dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH); " + "launch, then top up with candle_trade." + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
|
|
63112
|
+
inputSchema: imageDir ? localLaunchAndSeedShape : launchAndSeedShape
|
|
62874
63113
|
}, async (args) => {
|
|
62875
|
-
const result = await executeLaunchAndSeed(args, getConfig(), doFetch);
|
|
63114
|
+
const result = await executeLaunchAndSeed(args, getConfig(), doFetch, imageDir);
|
|
62876
63115
|
return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
|
|
62877
63116
|
});
|
|
62878
63117
|
const perpsTool = (tool) => async (args) => {
|
|
@@ -62924,10 +63163,11 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
62924
63163
|
return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
|
|
62925
63164
|
});
|
|
62926
63165
|
}
|
|
62927
|
-
var TOOL_NAMES, launchTokenShape, getMarketShape, tokenForensicsShape, FEED_LIMIT_MESSAGE = "limit must be a whole number from 1 to 200", feedLimit, getFeedShape, reportActivityShape, getAgentProfileShape, getOperationShape, getFeesShape, claimFeesShape, resolveTokenShape, profileWalletsShape, profilePnlShape, profileTradesShape, setProfileWalletsShape, swapShape, tradeShape, _rawBuyAmount, seedableLaunchShape, launchAndSeedShape, transferShape, sweepShape;
|
|
63166
|
+
var TOOL_NAMES, launchTokenShape, getMarketShape, tokenForensicsShape, FEED_LIMIT_MESSAGE = "limit must be a whole number from 1 to 200", feedLimit, getFeedShape, reportActivityShape, getAgentProfileShape, getOperationShape, getFeesShape, claimFeesShape, resolveTokenShape, profileWalletsShape, profilePnlShape, profileTradesShape, setProfileWalletsShape, swapShape, tradeShape, _rawBuyAmount, seedableLaunchShape, seedShape, launchAndSeedShape, localImageShape, localLaunchTokenShape, localLaunchAndSeedShape, IMAGE_PATH_DISCLOSURE, transferShape, sweepShape;
|
|
62928
63167
|
var init_tools = __esm(() => {
|
|
62929
63168
|
init_zod();
|
|
62930
63169
|
init_convert();
|
|
63170
|
+
init_image();
|
|
62931
63171
|
init_market_disclosure();
|
|
62932
63172
|
init_orchestrate();
|
|
62933
63173
|
init_perps();
|
|
@@ -63062,11 +63302,18 @@ var init_tools = __esm(() => {
|
|
|
63062
63302
|
paper: exports_external.preprocess((value) => value === "true" || value === 1 || value === "1" ? true : value === "false" || value === 0 || value === "0" ? false : value, exports_external.boolean().optional().describe("Rehearse instead of trading. The request passes every admission rule a live trade passes " + "-- the same planner, spend gate, key cap and loss limits -- and records the quote, but " + "nothing is ever broadcast and no funds move. Use it to check that a strategy is admitted " + "before risking anything on it. A paper fill is optimistic by construction: it books the " + "quoted price, so the gap between a paper arm and a live one IS the execution cost. " + "A sell of a mint this key already paper-bought also closes that paper book when the " + "live wallet is empty, even if this flag is omitted."))
|
|
63063
63303
|
};
|
|
63064
63304
|
({ buyAmount: _rawBuyAmount, ...seedableLaunchShape } = launchTokenShape);
|
|
63065
|
-
|
|
63066
|
-
...seedableLaunchShape,
|
|
63305
|
+
seedShape = {
|
|
63067
63306
|
clientLaunchId: exports_external.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result."),
|
|
63068
63307
|
devBuy: exports_external.string().optional().describe('Seed buy in DECIMAL units of the quote asset this launch selects (e.g. "0.25" SOL, or ' + "ETH on hood), bundled into the launch transaction itself on solana and sent as a follow-up " + "transaction on hood. Paid from the account's embedded wallet. Follows quoteAsset, which " + "defaults to sol on solana and eth on hood. Capped by the platform dev-buy ceiling; for " + "a larger seed, launch then follow with candle_trade.")
|
|
63069
63308
|
};
|
|
63309
|
+
launchAndSeedShape = { ...seedableLaunchShape, ...seedShape };
|
|
63310
|
+
localImageShape = {
|
|
63311
|
+
imageUrl: launchTokenShape.imageUrl.optional().describe(`${launchTokenShape.imageUrl.description}. Pass this or imagePath, never both`),
|
|
63312
|
+
imagePath: exports_external.string().optional().describe("An absolute image path inside CANDLE_IMAGE_DIR. The file is uploaded and becomes public and permanent, " + "including on a dry run. PNG, JPEG, GIF or WebP, at most 10MB, read from the file's first bytes. " + "Uploading spends no funds and does not count against the launch cap; only the upload limit applies. " + "Pass this or imageUrl, never both. The result carries the imageUrl used: pass that on a retry, or " + "after a dry run, instead of uploading the file again. bannerUrl takes no file")
|
|
63313
|
+
};
|
|
63314
|
+
localLaunchTokenShape = { ...launchTokenShape, ...localImageShape };
|
|
63315
|
+
localLaunchAndSeedShape = { ...seedableLaunchShape, ...localImageShape, ...seedShape };
|
|
63316
|
+
IMAGE_PATH_DISCLOSURE = " With imagePath, the image file is uploaded first and becomes public and permanent, on a dry run too; " + "nothing about a dry run keeps that file private.";
|
|
63070
63317
|
transferShape = {
|
|
63071
63318
|
chain: exports_external.enum(["solana", "hood"]).describe("Which chain the transfer executes on"),
|
|
63072
63319
|
asset: exports_external.enum(["SOL", "USDC", "CNDL", "ETH", "USDG"]).optional().describe("A base asset key. Pass exactly one of asset or mint."),
|
|
@@ -63159,7 +63406,7 @@ Full reference, error catalogue and end-to-end recipes: https://docs.candle.tv
|
|
|
63159
63406
|
// src/index.ts
|
|
63160
63407
|
import { spawn as spawn3 } from "node:child_process";
|
|
63161
63408
|
import { realpathSync } from "node:fs";
|
|
63162
|
-
import { chmod as chmod8, readFile as readFile10, realpath, rename as rename6, unlink, writeFile as writeFile7 } from "node:fs/promises";
|
|
63409
|
+
import { chmod as chmod8, readFile as readFile10, realpath as realpath2, rename as rename6, unlink, writeFile as writeFile7 } from "node:fs/promises";
|
|
63163
63410
|
import { homedir as homedir6, hostname } from "node:os";
|
|
63164
63411
|
import { pathToFileURL } from "node:url";
|
|
63165
63412
|
|
|
@@ -63208,7 +63455,7 @@ init_render();
|
|
|
63208
63455
|
init_secret_store();
|
|
63209
63456
|
|
|
63210
63457
|
// src/version.ts
|
|
63211
|
-
var CLI_VERSION = "0.11.
|
|
63458
|
+
var CLI_VERSION = "0.11.25";
|
|
63212
63459
|
|
|
63213
63460
|
// src/commands/auth.ts
|
|
63214
63461
|
init_keys_embedded_wallet();
|
|
@@ -63879,15 +64126,19 @@ var HELP = {
|
|
|
63879
64126
|
launch: {
|
|
63880
64127
|
group: "Trade",
|
|
63881
64128
|
summary: "Create a token on Solana or Hood, with an optional first buy",
|
|
63882
|
-
description: "Creates a token from a TEE wallet. Without --buy it makes no first buy, so the launch and the position are two decisions, and the buy is a separate candle swap. --buy <amount> adds a first buy from the same wallet, in whole units of the quote asset (SOL, USDC or CNDL on Solana, ETH on Hood; not USDG), within the dev-buy ceilings: on Solana it rides in the launch transaction, and on Hood it is a last leg after the curve and the fee. Candle reserves the buy against the key's spend window before anything is signed. --buy needs a server with TEE_LAUNCH_BUYS_ENABLED on, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs the launch:write scope and allowLaunch on the wallet, which only the account owner can turn on (candle wallets allow-launch <wallet>, over the device token; an agent key cannot). The wallet decides the chain: a Hood TEE wallet launches on Hood, needs --dex-version, and signs one leg at a time (the curve, then the fee, then the buy with --buy), each only after the one before it landed. Launching from a TEE wallet needs Pro or Max (TIER_REQUIRED otherwise; candle plans). Every plan, Free included, can launch from its embedded wallet with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through the MCP tool candle_launch_and_seed or the SDK's launch(); this command does not use that path.",
|
|
64129
|
+
description: "Creates a token from a TEE wallet. Without --buy it makes no first buy, so the launch and the position are two decisions, and the buy is a separate candle swap. --buy <amount> adds a first buy from the same wallet, in whole units of the quote asset (SOL, USDC or CNDL on Solana, ETH on Hood; not USDG), within the dev-buy ceilings: on Solana it rides in the launch transaction, and on Hood it is a last leg after the curve and the fee. Candle reserves the buy against the key's spend window before anything is signed. --buy needs a server with TEE_LAUNCH_BUYS_ENABLED on, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs the launch:write scope and allowLaunch on the wallet, which only the account owner can turn on (candle wallets allow-launch <wallet>, over the device token; an agent key cannot). The wallet decides the chain: a Hood TEE wallet launches on Hood, needs --dex-version, and signs one leg at a time (the curve, then the fee, then the buy with --buy), each only after the one before it landed. Launching from a TEE wallet needs Pro or Max (TIER_REQUIRED otherwise; candle plans). Every plan, Free included, can launch from its embedded wallet with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through the MCP tool candle_launch_and_seed or the SDK's launch(); this command does not use that path. The image is one of two flags, never both: --image-url <https-url> for one already hosted, or --image <path> for a PNG, JPEG, GIF or WebP file on this machine, at most 10MB. With --image the file is uploaded to Candle first and the launch is made with the URL that comes back, which --json reports as imageUrl. The upload happens before the confirm prompt and cannot be undone: the file becomes public and permanent even if you then decline or the launch fails. Re-running with the same --client-trade-id resumes the launch without uploading again. --image-url takes only an https:// URL.",
|
|
63883
64130
|
usage: [
|
|
63884
|
-
"candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--buy <amount>] [--quote-asset <asset>] [--dex-version v3|v4]"
|
|
64131
|
+
"candle launch --name <name> --symbol <symbol> --image <path>|--image-url <url> --wallet <tee> [--buy <amount>] [--quote-asset <asset>] [--dex-version v3|v4]"
|
|
63885
64132
|
],
|
|
63886
64133
|
rows: [],
|
|
63887
64134
|
flags: [
|
|
63888
64135
|
{ invocation: "--name <name>", description: "The token's name" },
|
|
63889
64136
|
{ invocation: "--symbol <symbol>", description: "The token's ticker" },
|
|
63890
|
-
{
|
|
64137
|
+
{
|
|
64138
|
+
invocation: "--image <path>",
|
|
64139
|
+
description: "The token image as a file on this machine (PNG, JPEG, GIF or WebP, at most 10MB). Uploaded before the confirm prompt; it becomes public and permanent"
|
|
64140
|
+
},
|
|
64141
|
+
{ invocation: "--image-url <url>", description: "The token image, already hosted at an https:// URL" },
|
|
63891
64142
|
{ invocation: "--wallet <tee>", description: "The TEE wallet that creates it" },
|
|
63892
64143
|
{
|
|
63893
64144
|
invocation: "--buy <amount>",
|
|
@@ -63900,12 +64151,27 @@ var HELP = {
|
|
|
63900
64151
|
}
|
|
63901
64152
|
],
|
|
63902
64153
|
examples: [
|
|
64154
|
+
"candle launch --name Demo --symbol DEMO --image ./logo.png --wallet AgentOne",
|
|
63903
64155
|
"candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne",
|
|
63904
64156
|
"candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet HoodAgent --dex-version v4",
|
|
63905
64157
|
"candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --buy 0.1"
|
|
63906
64158
|
],
|
|
63907
64159
|
env: ENV_API
|
|
63908
64160
|
},
|
|
64161
|
+
image: {
|
|
64162
|
+
group: "Trade",
|
|
64163
|
+
summary: "Host an image file from this machine and print its https URL",
|
|
64164
|
+
description: "Uploads a PNG, JPEG, GIF or WebP file, at most 10MB, to Candle and prints the https URL it is hosted at, ready to be a launch's image. The type is read from the file's first bytes, not its name, and a file that is none of the four or is too large is refused before any request. The upload is a publication: Candle pins the file to public storage, the URL cannot be withdrawn, and whatever the file carries (metadata included) goes with it, so upload only an image meant to be public. It spends no funds and does not count against the daily launch cap; the key's uploads-per-minute limit applies. Needs an API key with the launch:write scope. candle launch --image <path> does this upload itself; use this command for the link alone, or to launch through the hosted connector, which cannot read a file on your machine. Candle crops the image to a square, so this hosts a token image and not a banner.",
|
|
64165
|
+
usage: ["candle image upload <path> [--json]"],
|
|
64166
|
+
rows: [
|
|
64167
|
+
{
|
|
64168
|
+
invocation: "upload <path>",
|
|
64169
|
+
description: "Upload the file and print its URL; --json adds the content type and the size in bytes"
|
|
64170
|
+
}
|
|
64171
|
+
],
|
|
64172
|
+
examples: ["candle image upload ./logo.png", "candle image upload /Users/me/art/logo.webp --json"],
|
|
64173
|
+
env: ENV_API
|
|
64174
|
+
},
|
|
63909
64175
|
portfolio: {
|
|
63910
64176
|
group: "Account",
|
|
63911
64177
|
summary: "Every wallet's holdings, prices and value on Solana and Hood (vault read over your own RPCs)",
|
|
@@ -69950,6 +70216,121 @@ async function help(args, ctx) {
|
|
|
69950
70216
|
return 0;
|
|
69951
70217
|
}
|
|
69952
70218
|
|
|
70219
|
+
// src/commands/image.ts
|
|
70220
|
+
init_args();
|
|
70221
|
+
init_deps();
|
|
70222
|
+
init_render();
|
|
70223
|
+
init_trading();
|
|
70224
|
+
init_swap();
|
|
70225
|
+
import { open as open4 } from "node:fs/promises";
|
|
70226
|
+
var UPLOAD_USAGE = "Usage: candle image upload <path> [--json]";
|
|
70227
|
+
var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
70228
|
+
var IMAGE_TYPES = "PNG, JPEG, GIF or WebP";
|
|
70229
|
+
var NOTHING_UPLOADED = "Nothing was uploaded.";
|
|
70230
|
+
function sniffImageType(bytes) {
|
|
70231
|
+
const at = (offset, signature) => signature.every((byte, i) => bytes[offset + i] === byte);
|
|
70232
|
+
if (at(0, [137, 80, 78, 71, 13, 10, 26, 10]))
|
|
70233
|
+
return "image/png";
|
|
70234
|
+
if (at(0, [255, 216, 255]))
|
|
70235
|
+
return "image/jpeg";
|
|
70236
|
+
if (at(0, [71, 73, 70, 56]) && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97)
|
|
70237
|
+
return "image/gif";
|
|
70238
|
+
if (at(0, [82, 73, 70, 70]) && at(8, [87, 69, 66, 80]))
|
|
70239
|
+
return "image/webp";
|
|
70240
|
+
return;
|
|
70241
|
+
}
|
|
70242
|
+
function unreadable(path, error) {
|
|
70243
|
+
const code = error.code;
|
|
70244
|
+
const why = code === "ENOENT" || code === "ENOTDIR" ? "there is no file at that path" : code === "EACCES" || code === "EPERM" ? "this user may not read it" : code === "EISDIR" ? "it is not a file" : `the read failed${code ? ` (${code})` : ""}`;
|
|
70245
|
+
return new TradingError("IMAGE_UNREADABLE", `Could not read ${safeText(path)}: ${why}. ${NOTHING_UPLOADED}`);
|
|
70246
|
+
}
|
|
70247
|
+
async function readLocalImage(path) {
|
|
70248
|
+
const tooLarge = () => new TradingError("IMAGE_TOO_LARGE", `${safeText(path)} is over 10MB, the largest image Candle hosts. ${NOTHING_UPLOADED}`);
|
|
70249
|
+
let bytes;
|
|
70250
|
+
try {
|
|
70251
|
+
const file = await open4(path, "r");
|
|
70252
|
+
try {
|
|
70253
|
+
const stat4 = await file.stat();
|
|
70254
|
+
if (!stat4.isFile())
|
|
70255
|
+
throw Object.assign(new Error("not a regular file"), { code: "EISDIR" });
|
|
70256
|
+
if (stat4.size > MAX_IMAGE_BYTES)
|
|
70257
|
+
throw tooLarge();
|
|
70258
|
+
const buffer = new Uint8Array(MAX_IMAGE_BYTES + 1);
|
|
70259
|
+
let total = 0;
|
|
70260
|
+
while (total < buffer.length) {
|
|
70261
|
+
const { bytesRead } = await file.read(buffer, total, buffer.length - total, null);
|
|
70262
|
+
if (bytesRead === 0)
|
|
70263
|
+
break;
|
|
70264
|
+
total += bytesRead;
|
|
70265
|
+
}
|
|
70266
|
+
bytes = buffer.slice(0, total);
|
|
70267
|
+
} finally {
|
|
70268
|
+
await file.close();
|
|
70269
|
+
}
|
|
70270
|
+
} catch (error) {
|
|
70271
|
+
throw error instanceof TradingError ? error : unreadable(path, error);
|
|
70272
|
+
}
|
|
70273
|
+
if (bytes.length > MAX_IMAGE_BYTES)
|
|
70274
|
+
throw tooLarge();
|
|
70275
|
+
const contentType = sniffImageType(bytes);
|
|
70276
|
+
if (!contentType)
|
|
70277
|
+
throw new TradingError("IMAGE_TYPE_UNSUPPORTED", `${safeText(path)} is not a ${IMAGE_TYPES} image. The type is read from the file's first bytes, not its name. ${NOTHING_UPLOADED}`);
|
|
70278
|
+
return { path, bytes, contentType };
|
|
70279
|
+
}
|
|
70280
|
+
function uploadNotice(image) {
|
|
70281
|
+
return `Uploading ${safeText(image.path)} (${image.contentType}, ${image.bytes.length} bytes). The file becomes public and permanent: Candle pins it, and the URL cannot be withdrawn.
|
|
70282
|
+
`;
|
|
70283
|
+
}
|
|
70284
|
+
async function uploadImage(ctx, key, image) {
|
|
70285
|
+
const result = await apiRequest("/api/v1/uploads/agent-image", {
|
|
70286
|
+
apiUrl: ctx.apiUrl,
|
|
70287
|
+
credentials: { apiKey: key },
|
|
70288
|
+
auth: "key",
|
|
70289
|
+
method: "POST",
|
|
70290
|
+
raw: { bytes: image.bytes, contentType: image.contentType },
|
|
70291
|
+
fetch: ctx.deps.fetch,
|
|
70292
|
+
env: ctx.deps.env
|
|
70293
|
+
});
|
|
70294
|
+
if (!result.ok)
|
|
70295
|
+
throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
|
|
70296
|
+
const imageUrl = result.body?.imageUrl;
|
|
70297
|
+
if (typeof imageUrl !== "string" || !imageUrl.startsWith("https://"))
|
|
70298
|
+
throw new TradingError("INVALID_RESPONSE", "Candle did not return an https URL for the uploaded image.");
|
|
70299
|
+
return imageUrl;
|
|
70300
|
+
}
|
|
70301
|
+
async function imageUpload(args, ctx) {
|
|
70302
|
+
const parsed = parseArgs(args, { pathPositionals: ["<path>"] });
|
|
70303
|
+
if ("error" in parsed) {
|
|
70304
|
+
writeUsageFailure(ctx.deps, parsed.error, ctx.json);
|
|
70305
|
+
return 2;
|
|
70306
|
+
}
|
|
70307
|
+
const [path, ...rest] = parsed.positionals;
|
|
70308
|
+
if (path === undefined || rest.length !== 0) {
|
|
70309
|
+
writeUsageFailure(ctx.deps, UPLOAD_USAGE, ctx.json);
|
|
70310
|
+
return 2;
|
|
70311
|
+
}
|
|
70312
|
+
try {
|
|
70313
|
+
const image = await readLocalImage(path);
|
|
70314
|
+
const key = await resolveApiKey(ctx.deps, ctx.profile);
|
|
70315
|
+
if (!key) {
|
|
70316
|
+
writeLocalFailure(ctx.deps, {
|
|
70317
|
+
code: "NO_API_KEY",
|
|
70318
|
+
message: `No API key available. ${NOTHING_UPLOADED}`,
|
|
70319
|
+
suggestion: "Run: candle auth login, or set CANDLE_API_KEY. The key needs the launch:write scope."
|
|
70320
|
+
}, ctx.json);
|
|
70321
|
+
return 1;
|
|
70322
|
+
}
|
|
70323
|
+
ctx.deps.stderr.write(uploadNotice(image));
|
|
70324
|
+
const imageUrl = safeText(await uploadImage(ctx, key, image));
|
|
70325
|
+
ctx.deps.stdout.write(ctx.json ? `${JSON.stringify({ success: true, imageUrl, contentType: image.contentType, bytes: image.bytes.length })}
|
|
70326
|
+
` : `${imageUrl}
|
|
70327
|
+
`);
|
|
70328
|
+
return 0;
|
|
70329
|
+
} catch (error) {
|
|
70330
|
+
return tradingFailure(ctx, error);
|
|
70331
|
+
}
|
|
70332
|
+
}
|
|
70333
|
+
|
|
69953
70334
|
// src/index.ts
|
|
69954
70335
|
init_keys();
|
|
69955
70336
|
|
|
@@ -70468,9 +70849,9 @@ init_render();
|
|
|
70468
70849
|
init_solana_endpoint();
|
|
70469
70850
|
init_solana_lite();
|
|
70470
70851
|
init_trading();
|
|
70471
|
-
init_swap();
|
|
70472
70852
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
70473
|
-
|
|
70853
|
+
init_swap();
|
|
70854
|
+
var USAGE2 = "Usage: candle launch --name <name> --symbol <symbol> --image <path>|--image-url <https-url> --wallet <tee> [--buy <amount>] [--client-trade-id <id>] [--quote-asset sol|usdc|cndl|eth|usdg] [--dex-version v3|v4] [--mode <mode>] [--rpc-url <url>] [--yes]. A Hood TEE wallet launches on Hood and needs --dex-version.";
|
|
70474
70855
|
var QUOTE_CHAIN = {
|
|
70475
70856
|
sol: "solana",
|
|
70476
70857
|
usdc: "solana",
|
|
@@ -70529,6 +70910,7 @@ async function launch(args, ctx) {
|
|
|
70529
70910
|
valueFlags: [
|
|
70530
70911
|
"--name",
|
|
70531
70912
|
"--symbol",
|
|
70913
|
+
"--image",
|
|
70532
70914
|
"--image-url",
|
|
70533
70915
|
"--description",
|
|
70534
70916
|
"--wallet",
|
|
@@ -70539,7 +70921,8 @@ async function launch(args, ctx) {
|
|
|
70539
70921
|
"--dex-version",
|
|
70540
70922
|
"--buy"
|
|
70541
70923
|
],
|
|
70542
|
-
booleanFlags: ["--yes"]
|
|
70924
|
+
booleanFlags: ["--yes"],
|
|
70925
|
+
pathFlags: ["--image"]
|
|
70543
70926
|
});
|
|
70544
70927
|
if ("error" in parsed) {
|
|
70545
70928
|
writeUsageFailure(ctx.deps, parsed.error, ctx.json);
|
|
@@ -70547,7 +70930,15 @@ async function launch(args, ctx) {
|
|
|
70547
70930
|
}
|
|
70548
70931
|
const flags = parsed.values;
|
|
70549
70932
|
const id = flags["--client-trade-id"] ?? `launch-${randomUUID2()}`;
|
|
70550
|
-
if (
|
|
70933
|
+
if (flags["--image"] !== undefined && flags["--image-url"] !== undefined) {
|
|
70934
|
+
writeUsageFailure(ctx.deps, `Pass --image <path> or --image-url <https-url>, not both: a launch has one image. ${USAGE2}`, ctx.json);
|
|
70935
|
+
return 2;
|
|
70936
|
+
}
|
|
70937
|
+
if (flags["--image-url"] !== undefined && !flags["--image-url"].startsWith("https://")) {
|
|
70938
|
+
writeUsageFailure(ctx.deps, "--image-url takes an https:// URL; for an image file on this machine, use --image <path> instead.", ctx.json);
|
|
70939
|
+
return 2;
|
|
70940
|
+
}
|
|
70941
|
+
if (parsed.positionals.length || !flags["--name"] || !flags["--symbol"] || flags["--image"] === undefined && flags["--image-url"] === undefined || !flags["--wallet"] || !validClientId(id) || flags["--dex-version"] !== undefined && flags["--dex-version"] !== "v3" && flags["--dex-version"] !== "v4") {
|
|
70551
70942
|
writeUsageFailure(ctx.deps, USAGE2, ctx.json);
|
|
70552
70943
|
return 2;
|
|
70553
70944
|
}
|
|
@@ -70559,6 +70950,13 @@ async function launch(args, ctx) {
|
|
|
70559
70950
|
writeUsageFailure(ctx.deps, `A Hood launch needs --dex-version v3|v4. ${USAGE2}`, ctx.json);
|
|
70560
70951
|
return 2;
|
|
70561
70952
|
}
|
|
70953
|
+
let image;
|
|
70954
|
+
try {
|
|
70955
|
+
if (flags["--image"] !== undefined)
|
|
70956
|
+
image = await readLocalImage(flags["--image"]);
|
|
70957
|
+
} catch (error) {
|
|
70958
|
+
return tradingFailure(ctx, error);
|
|
70959
|
+
}
|
|
70562
70960
|
const key = await tradingKey(ctx);
|
|
70563
70961
|
const prior = await lookupOperation(ctx, key, id, "launch");
|
|
70564
70962
|
if (prior) {
|
|
@@ -70576,14 +70974,27 @@ async function launch(args, ctx) {
|
|
|
70576
70974
|
return printTradingResult(ctx, prior);
|
|
70577
70975
|
}
|
|
70578
70976
|
const wallet = await tradingWallet(ctx, key, flags["--wallet"], "launch:write", hinted ?? "any");
|
|
70579
|
-
|
|
70580
|
-
|
|
70581
|
-
|
|
70582
|
-
|
|
70583
|
-
|
|
70977
|
+
const chain2 = wallet.chain === "evm" ? "hood" : "solana";
|
|
70978
|
+
if (chain2 === "hood" && flags["--dex-version"] === undefined)
|
|
70979
|
+
throw new TradingUsage(`${wallet.address} is a Hood TEE wallet, and a Hood launch needs --dex-version v3|v4.`);
|
|
70980
|
+
const buy = launchBuy(flags, chain2);
|
|
70981
|
+
const solana = chain2 === "solana" ? await tradingSolanaClient(ctx, flags["--rpc-url"]) : undefined;
|
|
70982
|
+
let imageUrl = flags["--image-url"] ?? "";
|
|
70983
|
+
if (image) {
|
|
70984
|
+
ctx.deps.stderr.write(uploadNotice(image));
|
|
70985
|
+
try {
|
|
70986
|
+
imageUrl = await uploadImage(ctx, key, image);
|
|
70987
|
+
} catch (error) {
|
|
70988
|
+
ctx.deps.stderr.write(`The launch was not started, and its id ${id} is unused.
|
|
70989
|
+
`);
|
|
70990
|
+
return tradingFailure(ctx, error);
|
|
70991
|
+
}
|
|
70992
|
+
ctx.deps.stderr.write(`Image: ${safeText(imageUrl)}
|
|
70993
|
+
`);
|
|
70584
70994
|
}
|
|
70585
|
-
const
|
|
70586
|
-
|
|
70995
|
+
const used = image ? { imageUrl: safeText(imageUrl) } : {};
|
|
70996
|
+
if (!solana)
|
|
70997
|
+
return await hoodLaunch(ctx, key, { flags, id, wallet, buy, imageUrl, used, yes: parsed.booleans.has("--yes") });
|
|
70587
70998
|
if (!await claimOperation(ctx, key, id, "launch"))
|
|
70588
70999
|
throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this launch id; no write was resent.");
|
|
70589
71000
|
ctx.deps.stderr.write(`Operation: ${id}
|
|
@@ -70594,7 +71005,7 @@ async function launch(args, ctx) {
|
|
|
70594
71005
|
buyAmount: buy ? buy.raw : 0,
|
|
70595
71006
|
name: flags["--name"],
|
|
70596
71007
|
symbol: flags["--symbol"],
|
|
70597
|
-
imageUrl
|
|
71008
|
+
imageUrl,
|
|
70598
71009
|
linkedWalletId: wallet.id,
|
|
70599
71010
|
...flags["--description"] ? { description: flags["--description"] } : {},
|
|
70600
71011
|
...flags["--quote-asset"] ? { quoteAsset: flags["--quote-asset"] } : {},
|
|
@@ -70616,7 +71027,14 @@ async function launch(args, ctx) {
|
|
|
70616
71027
|
};
|
|
70617
71028
|
ctx.deps.stderr.write(buyNotice(buy, "solana"));
|
|
70618
71029
|
if (!await confirmQuote(ctx, quote, parsed.booleans.has("--yes")))
|
|
70619
|
-
return printTradingResult(ctx, {
|
|
71030
|
+
return printTradingResult(ctx, {
|
|
71031
|
+
success: true,
|
|
71032
|
+
status: "cancelled",
|
|
71033
|
+
clientTradeId: id,
|
|
71034
|
+
kind: "launch",
|
|
71035
|
+
quote,
|
|
71036
|
+
...used
|
|
71037
|
+
});
|
|
70620
71038
|
if (!Number.isFinite(built.expiresAt) || built.expiresAt <= ctx.deps.now())
|
|
70621
71039
|
throw new TradingError("QUOTE_EXPIRED", "The launch build expired before signing.");
|
|
70622
71040
|
const signed = await relaySign(ctx, key, wallet, built.transaction);
|
|
@@ -70634,7 +71052,7 @@ async function launch(args, ctx) {
|
|
|
70634
71052
|
ctx.deps.stderr.write(`Launch signature: ${signature}
|
|
70635
71053
|
`);
|
|
70636
71054
|
const result = await request(ctx, key, "/api/v1/launch/self/confirm", { clientLaunchId: id, signature });
|
|
70637
|
-
return printTradingResult(ctx, { ...result, clientTradeId: id, kind: "launch", quote });
|
|
71055
|
+
return printTradingResult(ctx, { ...result, clientTradeId: id, kind: "launch", quote, ...used });
|
|
70638
71056
|
} catch (error) {
|
|
70639
71057
|
return tradingFailure(ctx, error, id);
|
|
70640
71058
|
}
|
|
@@ -70652,7 +71070,7 @@ async function hoodLaunch(ctx, key, args) {
|
|
|
70652
71070
|
buyAmount: buy ? buy.raw : 0,
|
|
70653
71071
|
name: flags["--name"],
|
|
70654
71072
|
symbol: flags["--symbol"],
|
|
70655
|
-
imageUrl:
|
|
71073
|
+
imageUrl: args.imageUrl,
|
|
70656
71074
|
linkedWalletId: wallet.id,
|
|
70657
71075
|
dexVersion: flags["--dex-version"],
|
|
70658
71076
|
...flags["--description"] ? { description: flags["--description"] } : {},
|
|
@@ -70698,7 +71116,8 @@ async function hoodLaunch(ctx, key, args) {
|
|
|
70698
71116
|
chain: "hood",
|
|
70699
71117
|
quote,
|
|
70700
71118
|
operationId: first.operationId,
|
|
70701
|
-
walletHeldUntil: first.expiresAt
|
|
71119
|
+
walletHeldUntil: first.expiresAt,
|
|
71120
|
+
...args.used
|
|
70702
71121
|
});
|
|
70703
71122
|
}
|
|
70704
71123
|
const confirmedPlan = buy ? first.plannedLegCount === 3 ? ["createCurve", "feeTransfer", "trade"] : ["createCurve", "trade"] : undefined;
|
|
@@ -70731,7 +71150,8 @@ async function hoodLaunch(ctx, key, args) {
|
|
|
70731
71150
|
quote,
|
|
70732
71151
|
wallet: safeText(wallet.address),
|
|
70733
71152
|
operationId: first.operationId,
|
|
70734
|
-
landedLegs: run2.landed
|
|
71153
|
+
landedLegs: run2.landed,
|
|
71154
|
+
...args.used
|
|
70735
71155
|
});
|
|
70736
71156
|
}
|
|
70737
71157
|
function hoodLaterLegsGas(count, buys) {
|
|
@@ -72096,10 +72516,10 @@ async function perpsOrders(args, ctx) {
|
|
|
72096
72516
|
if (ctx.json)
|
|
72097
72517
|
return printTradingResult(ctx, result);
|
|
72098
72518
|
const out = ctx.deps.stdout;
|
|
72099
|
-
const
|
|
72100
|
-
out.write(`Open on Hyperliquid (${
|
|
72519
|
+
const open5 = result.open ?? [];
|
|
72520
|
+
out.write(`Open on Hyperliquid (${open5.length}):
|
|
72101
72521
|
`);
|
|
72102
|
-
for (const o of
|
|
72522
|
+
for (const o of open5) {
|
|
72103
72523
|
out.write(` ${safeText(o.coin)} ${o.side === "B" ? "buy" : "sell"} ${safeText(o.sz)} at ${safeText(o.limitPx)}${o.reduceOnly ? " reduce-only" : ""}${o.isTrigger ? ` trigger ${safeText(o.triggerPx)}` : ""}, cloid ${safeText(o.cloid ?? "none")}
|
|
72104
72524
|
`);
|
|
72105
72525
|
}
|
|
@@ -73171,13 +73591,13 @@ function writeWalletEvents(ctx, books) {
|
|
|
73171
73591
|
function writeAgents(ctx, agents, all) {
|
|
73172
73592
|
if (!agents || agents.length === 0)
|
|
73173
73593
|
return;
|
|
73174
|
-
const
|
|
73594
|
+
const open5 = (count2, unmarked) => `${count2}${unmarked > 0 ? ` (${unmarked} unpriced)` : ""}`;
|
|
73175
73595
|
const rows = agents.map((a) => [
|
|
73176
73596
|
a.keyPrefix === null ? "Manual (web app)" : a.label && a.label !== a.keyPrefix ? `${a.label} (${a.keyPrefix})` : a.keyPrefix,
|
|
73177
73597
|
formatUsd(a.realizedNetUsd),
|
|
73178
73598
|
formatUsd(a.unrealizedUsd),
|
|
73179
73599
|
formatUsd(totalOf(a)),
|
|
73180
|
-
|
|
73600
|
+
open5(a.openPositions, a.unmarked),
|
|
73181
73601
|
String(a.closedRounds)
|
|
73182
73602
|
]);
|
|
73183
73603
|
rows.push([
|
|
@@ -73185,7 +73605,7 @@ function writeAgents(ctx, agents, all) {
|
|
|
73185
73605
|
formatUsd(all.realizedNetUsd),
|
|
73186
73606
|
formatUsd(all.unrealizedUsd),
|
|
73187
73607
|
formatUsd(totalOf(all)),
|
|
73188
|
-
|
|
73608
|
+
open5(all.openPositions, all.unmarked),
|
|
73189
73609
|
all.closedRounds !== undefined ? String(all.closedRounds) : "-"
|
|
73190
73610
|
]);
|
|
73191
73611
|
ctx.deps.stdout.write(`
|
|
@@ -84159,6 +84579,7 @@ var COMMANDS = {
|
|
|
84159
84579
|
swap: { bare: swap, subcommands: { status: swapStatus } },
|
|
84160
84580
|
transfer: { bare: transfer },
|
|
84161
84581
|
launch: { bare: launch },
|
|
84582
|
+
image: { subcommands: { upload: imageUpload } },
|
|
84162
84583
|
pnl: { bare: pnl },
|
|
84163
84584
|
portfolio: { bare: portfolio },
|
|
84164
84585
|
fees: { bare: feesList, subcommands: { claim: feesClaim } },
|
|
@@ -84578,7 +84999,7 @@ async function buildRealDeps() {
|
|
|
84578
84999
|
platformKey: platformKey(process.platform, process.arch),
|
|
84579
85000
|
platform: process.platform,
|
|
84580
85001
|
arch: process.arch,
|
|
84581
|
-
realpath: (path) =>
|
|
85002
|
+
realpath: (path) => realpath2(path),
|
|
84582
85003
|
spawnHelper: realSpawnHelper,
|
|
84583
85004
|
releasePolicy: RELEASE_POLICY,
|
|
84584
85005
|
writeBytes: async (path, bytes) => {
|
package/package.json
CHANGED