@candledottv/cli 0.11.24 → 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 +2 -2
- package/dist/index.js +265 -21
- 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.
|
|
@@ -135,7 +135,7 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
|
|
|
135
135
|
| `candle profile rename <old> <new>` | Renames a profile. |
|
|
136
136
|
| `candle profile remove <name> --yes` | Deletes a profile and its stored credentials. |
|
|
137
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. |
|
|
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. |
|
|
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. |
|
|
139
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. |
|
|
140
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. |
|
|
141
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. |
|
package/dist/index.js
CHANGED
|
@@ -61135,7 +61135,7 @@ var init_convert = __esm(() => {
|
|
|
61135
61135
|
});
|
|
61136
61136
|
|
|
61137
61137
|
// ../mcp/src/version.ts
|
|
61138
|
-
var SERVER_VERSION = "0.10.
|
|
61138
|
+
var SERVER_VERSION = "0.10.12";
|
|
61139
61139
|
|
|
61140
61140
|
// ../mcp/src/update-notice.ts
|
|
61141
61141
|
function newer(a, b) {
|
|
@@ -61170,6 +61170,213 @@ var init_update_notice = __esm(() => {
|
|
|
61170
61170
|
PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
|
|
61171
61171
|
});
|
|
61172
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
|
+
|
|
61173
61380
|
// ../mcp/src/orchestrate.ts
|
|
61174
61381
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
61175
61382
|
function requireApiKey(cfg) {
|
|
@@ -61419,10 +61626,10 @@ async function executeTrade(args, cfg, doFetch) {
|
|
|
61419
61626
|
const wrapped = JSON.stringify({ clientTradeId, resolved, api: posted.body });
|
|
61420
61627
|
return posted.ok ? { text: wrapped } : { text: wrapped, isError: true };
|
|
61421
61628
|
}
|
|
61422
|
-
async function executeLaunchAndSeed(args, cfg, doFetch) {
|
|
61629
|
+
async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
|
|
61423
61630
|
const apiKey = requireApiKey(cfg);
|
|
61424
61631
|
const clientLaunchId = args.clientLaunchId ?? randomUUID6();
|
|
61425
|
-
const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...
|
|
61632
|
+
const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...imageFields } = args;
|
|
61426
61633
|
let buyAmount;
|
|
61427
61634
|
if (devBuy !== undefined) {
|
|
61428
61635
|
const q = quoteIdDecimals(args.quoteAsset, args.chain, { clientLaunchId });
|
|
@@ -61433,19 +61640,34 @@ async function executeLaunchAndSeed(args, cfg, doFetch) {
|
|
|
61433
61640
|
return converted.err;
|
|
61434
61641
|
buyAmount = converted.raw;
|
|
61435
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 };
|
|
61436
61656
|
const path = dryRun ? "/api/v1/launch/headless/dry-run" : "/api/v1/launch/headless";
|
|
61437
61657
|
const posted = await postJson(`${base(cfg)}${path}`, apiKey, {
|
|
61438
|
-
...
|
|
61658
|
+
...image.args,
|
|
61439
61659
|
clientLaunchId,
|
|
61440
61660
|
...buyAmount !== undefined ? { buyAmount } : {}
|
|
61441
61661
|
}, doFetch);
|
|
61442
|
-
if ("thrown" in posted)
|
|
61443
|
-
|
|
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
|
+
}
|
|
61444
61666
|
if (!posted.ok) {
|
|
61445
|
-
return { text: JSON.stringify({ clientLaunchId, api: posted.body }), isError: true };
|
|
61667
|
+
return { text: JSON.stringify({ clientLaunchId, ...uploaded, api: posted.body }), isError: true };
|
|
61446
61668
|
}
|
|
61447
61669
|
if (dryRun) {
|
|
61448
|
-
return { text: JSON.stringify({ clientLaunchId, dryRun: true, api: posted.body }) };
|
|
61670
|
+
return { text: JSON.stringify({ clientLaunchId, dryRun: true, ...uploaded, api: posted.body }) };
|
|
61449
61671
|
}
|
|
61450
61672
|
const launch2 = posted.body;
|
|
61451
61673
|
let market = null;
|
|
@@ -61466,7 +61688,7 @@ async function executeLaunchAndSeed(args, cfg, doFetch) {
|
|
|
61466
61688
|
note = "launch confirmed; the follow-up market read failed, read it via candle_get_market";
|
|
61467
61689
|
}
|
|
61468
61690
|
return {
|
|
61469
|
-
text: JSON.stringify({ clientLaunchId, launch: launch2, market, ...note ? { note } : {} })
|
|
61691
|
+
text: JSON.stringify({ clientLaunchId, ...uploaded, launch: launch2, market, ...note ? { note } : {} })
|
|
61470
61692
|
};
|
|
61471
61693
|
}
|
|
61472
61694
|
async function executeSweep(args, cfg, doFetch) {
|
|
@@ -61580,6 +61802,7 @@ async function executionStatus(cfg, doFetch) {
|
|
|
61580
61802
|
var SWEEP_BASE_ASSETS;
|
|
61581
61803
|
var init_orchestrate = __esm(() => {
|
|
61582
61804
|
init_convert();
|
|
61805
|
+
init_image();
|
|
61583
61806
|
init_update_notice();
|
|
61584
61807
|
SWEEP_BASE_ASSETS = {
|
|
61585
61808
|
solana: ["USDC", "CNDL", "SOL"],
|
|
@@ -62694,11 +62917,24 @@ function registerToolSubset(server, options) {
|
|
|
62694
62917
|
return;
|
|
62695
62918
|
server.registerTool(name2, { ...definition, ...options.metadata?.[name2] }, handler);
|
|
62696
62919
|
};
|
|
62920
|
+
const imageDir = configuredImageDir(env);
|
|
62697
62921
|
register("candle_launch_token", {
|
|
62698
62922
|
title: "Launch a token on Candle",
|
|
62699
|
-
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.",
|
|
62700
|
-
inputSchema: launchTokenShape
|
|
62701
|
-
}, 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
|
+
});
|
|
62702
62938
|
register("candle_get_market", {
|
|
62703
62939
|
title: "Get market state",
|
|
62704
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.",
|
|
@@ -62872,10 +63108,10 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
62872
63108
|
});
|
|
62873
63109
|
register("candle_launch_and_seed", {
|
|
62874
63110
|
title: "Launch a token and seed it",
|
|
62875
|
-
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.",
|
|
62876
|
-
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
|
|
62877
63113
|
}, async (args) => {
|
|
62878
|
-
const result = await executeLaunchAndSeed(args, getConfig(), doFetch);
|
|
63114
|
+
const result = await executeLaunchAndSeed(args, getConfig(), doFetch, imageDir);
|
|
62879
63115
|
return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
|
|
62880
63116
|
});
|
|
62881
63117
|
const perpsTool = (tool) => async (args) => {
|
|
@@ -62927,10 +63163,11 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
62927
63163
|
return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
|
|
62928
63164
|
});
|
|
62929
63165
|
}
|
|
62930
|
-
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;
|
|
62931
63167
|
var init_tools = __esm(() => {
|
|
62932
63168
|
init_zod();
|
|
62933
63169
|
init_convert();
|
|
63170
|
+
init_image();
|
|
62934
63171
|
init_market_disclosure();
|
|
62935
63172
|
init_orchestrate();
|
|
62936
63173
|
init_perps();
|
|
@@ -63065,11 +63302,18 @@ var init_tools = __esm(() => {
|
|
|
63065
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."))
|
|
63066
63303
|
};
|
|
63067
63304
|
({ buyAmount: _rawBuyAmount, ...seedableLaunchShape } = launchTokenShape);
|
|
63068
|
-
|
|
63069
|
-
...seedableLaunchShape,
|
|
63305
|
+
seedShape = {
|
|
63070
63306
|
clientLaunchId: exports_external.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result."),
|
|
63071
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.")
|
|
63072
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.";
|
|
63073
63317
|
transferShape = {
|
|
63074
63318
|
chain: exports_external.enum(["solana", "hood"]).describe("Which chain the transfer executes on"),
|
|
63075
63319
|
asset: exports_external.enum(["SOL", "USDC", "CNDL", "ETH", "USDG"]).optional().describe("A base asset key. Pass exactly one of asset or mint."),
|
|
@@ -63162,7 +63406,7 @@ Full reference, error catalogue and end-to-end recipes: https://docs.candle.tv
|
|
|
63162
63406
|
// src/index.ts
|
|
63163
63407
|
import { spawn as spawn3 } from "node:child_process";
|
|
63164
63408
|
import { realpathSync } from "node:fs";
|
|
63165
|
-
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";
|
|
63166
63410
|
import { homedir as homedir6, hostname } from "node:os";
|
|
63167
63411
|
import { pathToFileURL } from "node:url";
|
|
63168
63412
|
|
|
@@ -63211,7 +63455,7 @@ init_render();
|
|
|
63211
63455
|
init_secret_store();
|
|
63212
63456
|
|
|
63213
63457
|
// src/version.ts
|
|
63214
|
-
var CLI_VERSION = "0.11.
|
|
63458
|
+
var CLI_VERSION = "0.11.25";
|
|
63215
63459
|
|
|
63216
63460
|
// src/commands/auth.ts
|
|
63217
63461
|
init_keys_embedded_wallet();
|
|
@@ -84755,7 +84999,7 @@ async function buildRealDeps() {
|
|
|
84755
84999
|
platformKey: platformKey(process.platform, process.arch),
|
|
84756
85000
|
platform: process.platform,
|
|
84757
85001
|
arch: process.arch,
|
|
84758
|
-
realpath: (path) =>
|
|
85002
|
+
realpath: (path) => realpath2(path),
|
|
84759
85003
|
spawnHelper: realSpawnHelper,
|
|
84760
85004
|
releasePolicy: RELEASE_POLICY,
|
|
84761
85005
|
writeBytes: async (path, bytes) => {
|
package/package.json
CHANGED