@candledottv/cli 0.6.0 → 0.6.2
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 +50 -4
- package/dist/index.js +55 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,11 +7,23 @@ health from the terminal. Zero runtime dependencies; the whole thing is one self
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
|
|
10
|
+
Install the Candle CLI (macOS or Linux):
|
|
11
|
+
|
|
12
|
+
curl -fsSL https://candle.tv/install.sh | bash
|
|
13
|
+
|
|
14
|
+
or with Homebrew:
|
|
15
|
+
|
|
16
|
+
brew install candledottv/tap/candle
|
|
17
|
+
|
|
18
|
+
Then: candle setup
|
|
11
19
|
```
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
`candle setup` authorizes this device from your browser, shows the agent wallets as funding
|
|
22
|
+
destinations, prints the skill and MCP install lines, and runs a full health check. `candle auth
|
|
23
|
+
login` on its own does just the authorization step.
|
|
24
|
+
|
|
25
|
+
The npm package `@candledottv/cli` stays published for CI, programmatic use, and Windows until
|
|
26
|
+
`install.ps1` ships; `npx -y @candledottv/cli@latest <command>` runs it once without installing.
|
|
15
27
|
|
|
16
28
|
### No-npm fallback
|
|
17
29
|
|
|
@@ -50,7 +62,7 @@ node packages/cli/dist/index.js auth login
|
|
|
50
62
|
| `candle profile use <name>` | Makes a profile the active one. |
|
|
51
63
|
| `candle profile rename <old> <new>` | Renames a profile. |
|
|
52
64
|
| `candle profile remove <name> --yes` | Deletes a profile and its stored credentials. |
|
|
53
|
-
| `candle mcp [--tools <a,b,c>] [--read-only] [--print-config]` | Runs the Candle MCP server (`npx @candledottv/mcp`) 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 four keyless read tools; `--tools` pins an explicit allowlist. |
|
|
65
|
+
| `candle mcp [--tools <a,b,c>] [--read-only] [--print-config]` | Runs the Candle MCP server (`npx @candledottv/mcp`) 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 four keyless read tools; `--tools` pins an explicit allowlist. MCP hosts also need Node 18+ on their own PATH, because `candle mcp` starts the server with `npx --yes @candledottv/mcp`. |
|
|
54
66
|
| `candle doctor` | Runs a full health check (runtime, backend, credentials, API reachability, credential validity, wallet delegation) as a PASS/FAIL/SKIP table. Exits nonzero on any FAIL. |
|
|
55
67
|
| `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. |
|
|
56
68
|
| `candle update [--check] [--to <tag>]` | Replaces this binary with the latest signed release. The download 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. |
|
|
@@ -66,6 +78,40 @@ Every command accepts these global options:
|
|
|
66
78
|
| `--help`, `-h` | Prints usage. |
|
|
67
79
|
| `--version`, `-v` | Prints the CLI version. |
|
|
68
80
|
|
|
81
|
+
## Verify a release
|
|
82
|
+
|
|
83
|
+
Every release on https://github.com/candledottv/agentic/releases is built and signed by that
|
|
84
|
+
repository's `release.yaml` workflow, and `install.sh` and `candle update` already check this for
|
|
85
|
+
you. To check a download by hand, three commands, in increasing strength:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
curl -fsSLO https://github.com/candledottv/agentic/releases/download/cli-v0.6.1/SHA256SUMS
|
|
89
|
+
curl -fsSLO https://github.com/candledottv/agentic/releases/download/cli-v0.6.1/candle-darwin-arm64
|
|
90
|
+
grep candle-darwin-arm64 SHA256SUMS | shasum -a 256 -c
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
gh attestation verify candle-darwin-arm64 --repo candledottv/agentic \
|
|
95
|
+
--signer-workflow candledottv/agentic/.github/workflows/release.yaml
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
curl -fsSLO https://github.com/candledottv/agentic/releases/download/cli-v0.6.1/candle-darwin-arm64.sigstore.json
|
|
100
|
+
cosign verify-blob --new-bundle-format --bundle candle-darwin-arm64.sigstore.json \
|
|
101
|
+
--certificate-identity-regexp '^https://github.com/candledottv/agentic/\.github/workflows/release\.yaml@refs/tags/cli-v' \
|
|
102
|
+
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
|
103
|
+
candle-darwin-arm64
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`--new-bundle-format` (cosign 2.2 or newer) says "expect a Sigstore bundle", which is what releases
|
|
107
|
+
from 0.6.1 onward are signed as; without it cosign also accepts an older bundle format of its own,
|
|
108
|
+
which `candle verify` cannot read.
|
|
109
|
+
|
|
110
|
+
No cosign or gh installed? `candle verify <file> --bundle <path>` (this CLI's own command, see the
|
|
111
|
+
table above) runs the same check against the trusted root compiled into the binary, no network
|
|
112
|
+
call required. Full walkthrough, including the installer script's own signature and the
|
|
113
|
+
transparency log: [Verify a Candle release](https://docs.candle.tv/developers/verify-a-candle-release).
|
|
114
|
+
|
|
69
115
|
## The `--json` contract
|
|
70
116
|
|
|
71
117
|
For agents and scripts, `--json` guarantees: **stdout carries exactly one JSON value** -- the
|
package/dist/index.js
CHANGED
|
@@ -4893,9 +4893,10 @@ async function fetchAccount(deps, apiUrl, apiKey) {
|
|
|
4893
4893
|
fetch: deps.fetch,
|
|
4894
4894
|
env: deps.env
|
|
4895
4895
|
});
|
|
4896
|
-
const
|
|
4896
|
+
const body = identity.ok ? identity.body : undefined;
|
|
4897
|
+
const account = body?.account;
|
|
4897
4898
|
if (account)
|
|
4898
|
-
return { account };
|
|
4899
|
+
return { account, ...body?.username ? { username: body.username } : {} };
|
|
4899
4900
|
return { failure: identity.ok ? "no account in the response" : identity.message };
|
|
4900
4901
|
}
|
|
4901
4902
|
|
|
@@ -5179,16 +5180,16 @@ function defaultProfileNameFor(apiUrl, existing) {
|
|
|
5179
5180
|
function credentialEnvOverrides(env) {
|
|
5180
5181
|
return ["CANDLE_API_KEY", "CANDLE_DEVICE_TOKEN"].filter((name) => env[name]?.trim());
|
|
5181
5182
|
}
|
|
5182
|
-
function identityLine(profile, account, apiUrl, overrides) {
|
|
5183
|
-
const shown = overrides?.length ? `unknown (${overrides.join(", ")} override)` : account ?? "unknown";
|
|
5183
|
+
function identityLine(profile, account, apiUrl, overrides, username) {
|
|
5184
|
+
const shown = overrides?.length ? `unknown (${overrides.join(", ")} override)` : username && account ? `${username} (${account})` : account ?? "unknown";
|
|
5184
5185
|
return `Profile: ${profile ?? "none"} Account: ${shown} at ${apiUrl}`;
|
|
5185
5186
|
}
|
|
5186
5187
|
async function printIdentity(ctx) {
|
|
5187
5188
|
if (ctx.json)
|
|
5188
5189
|
return;
|
|
5189
5190
|
const config = await ctx.deps.readConfig();
|
|
5190
|
-
const
|
|
5191
|
-
ctx.deps.stdout.write(`${identityLine(ctx.profile, account, ctx.apiUrl, credentialEnvOverrides(ctx.deps.env))}
|
|
5191
|
+
const fields = effectiveProfileFields(config, ctx.profile);
|
|
5192
|
+
ctx.deps.stdout.write(`${identityLine(ctx.profile, fields.account, ctx.apiUrl, credentialEnvOverrides(ctx.deps.env), fields.username)}
|
|
5192
5193
|
`);
|
|
5193
5194
|
}
|
|
5194
5195
|
function formatCacheAge(now, cachedAt) {
|
|
@@ -5384,7 +5385,7 @@ async function resolveApiKey(deps, profile) {
|
|
|
5384
5385
|
}
|
|
5385
5386
|
|
|
5386
5387
|
// src/version.ts
|
|
5387
|
-
var CLI_VERSION = "0.6.
|
|
5388
|
+
var CLI_VERSION = "0.6.2";
|
|
5388
5389
|
|
|
5389
5390
|
// src/commands/auth.ts
|
|
5390
5391
|
var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
|
|
@@ -5505,8 +5506,12 @@ async function finishLogin(rawBody, ctx, requested) {
|
|
|
5505
5506
|
if (body.apiKey)
|
|
5506
5507
|
await deps.store.set(profileSecretRef(profileName, "apiKey"), body.apiKey.key);
|
|
5507
5508
|
let account;
|
|
5508
|
-
|
|
5509
|
-
|
|
5509
|
+
let username;
|
|
5510
|
+
if (body.apiKey) {
|
|
5511
|
+
const lookup = await fetchAccount(deps, ctx.apiUrl, body.apiKey.key);
|
|
5512
|
+
account = lookup.account;
|
|
5513
|
+
username = lookup.username;
|
|
5514
|
+
}
|
|
5510
5515
|
const portalOrigin = portalOriginFrom(requested.verificationUri);
|
|
5511
5516
|
await deps.updateProfile(profileName, {
|
|
5512
5517
|
apiUrl: ctx.apiUrl,
|
|
@@ -5514,7 +5519,7 @@ async function finishLogin(rawBody, ctx, requested) {
|
|
|
5514
5519
|
...body.apiKey ? { keyPrefix: body.apiKey.keyPrefix, scopes: body.apiKey.scopes } : {},
|
|
5515
5520
|
...requested.label ? { label: requested.label } : {},
|
|
5516
5521
|
...portalOrigin ? { portalOrigin } : {},
|
|
5517
|
-
...account ? { account, accountCachedAt: deps.now() } : {}
|
|
5522
|
+
...account ? { account, accountCachedAt: deps.now(), username } : {}
|
|
5518
5523
|
});
|
|
5519
5524
|
if (!config.activeProfile)
|
|
5520
5525
|
await deps.writeConfig({ activeProfile: profileName });
|
|
@@ -5670,8 +5675,12 @@ async function authStatus(args, ctx) {
|
|
|
5670
5675
|
}));
|
|
5671
5676
|
}
|
|
5672
5677
|
let account;
|
|
5673
|
-
|
|
5674
|
-
|
|
5678
|
+
let username;
|
|
5679
|
+
if (apiKey) {
|
|
5680
|
+
const lookup = await fetchAccount(deps, apiUrl, apiKey);
|
|
5681
|
+
account = lookup.account;
|
|
5682
|
+
username = lookup.username;
|
|
5683
|
+
}
|
|
5675
5684
|
const exitCode = rows.some((row) => row.state === "FAIL") ? 1 : 0;
|
|
5676
5685
|
const configPath = configFilePathForDisplay(deps.env);
|
|
5677
5686
|
const fields = effectiveProfileFields(config, ctx.profile);
|
|
@@ -5692,7 +5701,9 @@ async function authStatus(args, ctx) {
|
|
|
5692
5701
|
`);
|
|
5693
5702
|
return exitCode;
|
|
5694
5703
|
}
|
|
5695
|
-
|
|
5704
|
+
const shownAccount = account ?? fields.account;
|
|
5705
|
+
const shownUsername = account !== undefined ? username : fields.username;
|
|
5706
|
+
deps.stdout.write(`${identityLine(ctx.profile, shownAccount, apiUrl, undefined, shownUsername)}
|
|
5696
5707
|
`);
|
|
5697
5708
|
if (mismatch) {
|
|
5698
5709
|
deps.stdout.write(`Profile ${ctx.profile} recorded ${cachedAccount}; this key belongs to ${account}. Run: candle profile use ${ctx.profile}
|
|
@@ -6218,8 +6229,8 @@ async function mcp(args, ctx) {
|
|
|
6218
6229
|
toolAllowlist = requested.join(",");
|
|
6219
6230
|
}
|
|
6220
6231
|
const identityConfig = await deps.readConfig();
|
|
6221
|
-
const
|
|
6222
|
-
deps.stderr.write(`${identityLine(ctx.profile,
|
|
6232
|
+
const identityFields = effectiveProfileFields(identityConfig, ctx.profile);
|
|
6233
|
+
deps.stderr.write(`${identityLine(ctx.profile, identityFields.account, apiUrl, credentialEnvOverrides(deps.env), identityFields.username)}
|
|
6223
6234
|
`);
|
|
6224
6235
|
if (parsed.booleans.has("--print-config")) {
|
|
6225
6236
|
const launchArgs = [
|
|
@@ -6364,11 +6375,13 @@ async function profileUse(args, ctx) {
|
|
|
6364
6375
|
const apiUrl = ctx.apiUrlFlag ?? resolveApiUrl(profile.apiUrl, deps.env);
|
|
6365
6376
|
const apiKey = await deps.store.get(profileSecretRef(name, "apiKey"));
|
|
6366
6377
|
let account = profile.account;
|
|
6378
|
+
let username = profile.username;
|
|
6367
6379
|
if (apiKey) {
|
|
6368
|
-
const { account: live, failure } = await fetchAccount(deps, apiUrl, apiKey);
|
|
6380
|
+
const { account: live, username: liveUsername, failure } = await fetchAccount(deps, apiUrl, apiKey);
|
|
6369
6381
|
if (live) {
|
|
6370
6382
|
account = live;
|
|
6371
|
-
|
|
6383
|
+
username = liveUsername;
|
|
6384
|
+
await deps.updateProfile(name, { account: live, username: liveUsername, accountCachedAt: deps.now() });
|
|
6372
6385
|
} else {
|
|
6373
6386
|
deps.stderr.write(`Could not refresh the account for ${name} (${failure}); keeping the cached value.
|
|
6374
6387
|
`);
|
|
@@ -6381,7 +6394,7 @@ async function profileUse(args, ctx) {
|
|
|
6381
6394
|
deps.stdout.write(`${JSON.stringify({ name, account, apiUrl })}
|
|
6382
6395
|
`);
|
|
6383
6396
|
else
|
|
6384
|
-
deps.stdout.write(`${identityLine(name, account, apiUrl)}
|
|
6397
|
+
deps.stdout.write(`${identityLine(name, account, apiUrl, undefined, username)}
|
|
6385
6398
|
`);
|
|
6386
6399
|
return 0;
|
|
6387
6400
|
}
|
|
@@ -6533,7 +6546,7 @@ async function setup(args, ctx) {
|
|
|
6533
6546
|
const solana = body.wallets?.solana ?? null;
|
|
6534
6547
|
const evm = body.wallets?.evm ?? null;
|
|
6535
6548
|
if (body.account)
|
|
6536
|
-
deps.stdout.write(`${identityLine(nextCtx.profile, body.account, apiUrl)}
|
|
6549
|
+
deps.stdout.write(`${identityLine(nextCtx.profile, body.account, apiUrl, undefined, body.username)}
|
|
6537
6550
|
`);
|
|
6538
6551
|
if (solana)
|
|
6539
6552
|
deps.stdout.write(`Solana (send SOL here): ${solana.address}
|
|
@@ -6545,6 +6558,8 @@ async function setup(args, ctx) {
|
|
|
6545
6558
|
`);
|
|
6546
6559
|
deps.stdout.write(`
|
|
6547
6560
|
Tell your agent (paste into its context):
|
|
6561
|
+
`);
|
|
6562
|
+
deps.stdout.write(` Install the Candle CLI: curl -fsSL https://candle.tv/install.sh | bash
|
|
6548
6563
|
`);
|
|
6549
6564
|
deps.stdout.write(` You operate a Candle agent account. API base URL: ${apiUrl} (send your API key in the x-api-key header).
|
|
6550
6565
|
`);
|
|
@@ -6762,6 +6777,13 @@ function getVerifier() {
|
|
|
6762
6777
|
return verifier;
|
|
6763
6778
|
}
|
|
6764
6779
|
var IN_TOTO_PAYLOAD_TYPE = "application/vnd.in-toto+json";
|
|
6780
|
+
function isLegacyCosignBundle(bundleJson) {
|
|
6781
|
+
if (typeof bundleJson !== "object" || bundleJson === null)
|
|
6782
|
+
return false;
|
|
6783
|
+
const candidate = bundleJson;
|
|
6784
|
+
return "base64Signature" in candidate && "rekorBundle" in candidate && !("mediaType" in candidate);
|
|
6785
|
+
}
|
|
6786
|
+
var LEGACY_BUNDLE_REASON = "legacy cosign bundle (base64Signature/rekorBundle); Candle releases are signed with cosign --new-bundle-format, so this is not a Candle release bundle or the release workflow regressed";
|
|
6765
6787
|
function exactIdentity(identityUri) {
|
|
6766
6788
|
return new RegExp(`^${identityUri.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`);
|
|
6767
6789
|
}
|
|
@@ -6772,6 +6794,8 @@ function withTrustRootHint(reason) {
|
|
|
6772
6794
|
return `${reason}; the trust root embedded in this candle may be out of date; reinstall with curl -fsSL https://candle.tv/install.sh | bash`;
|
|
6773
6795
|
}
|
|
6774
6796
|
function verifyReleaseAsset(bytes, bundleJson, identityUri, issuer) {
|
|
6797
|
+
if (isLegacyCosignBundle(bundleJson))
|
|
6798
|
+
return { ok: false, reason: LEGACY_BUNDLE_REASON };
|
|
6775
6799
|
try {
|
|
6776
6800
|
const bundle = import_bundle.bundleFromJSON(bundleJson);
|
|
6777
6801
|
const digest = createHash("sha256").update(bytes).digest();
|
|
@@ -10606,9 +10630,9 @@ Commands:
|
|
|
10606
10630
|
keys create [--scopes <a,b,c>] [--label <name>] Create an API key
|
|
10607
10631
|
[--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]
|
|
10608
10632
|
keys revoke <prefix> Revoke an API key
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10633
|
+
wallet Show launch and linked wallets (wallets is an alias)
|
|
10634
|
+
wallet import --chain <solana|evm> [options] Import a wallet you own (key via --key-file or hidden prompt)
|
|
10635
|
+
wallet revoke <wallet-id> Revoke a linked wallet
|
|
10612
10636
|
profile list Profiles on this machine, with cached accounts
|
|
10613
10637
|
profile add <name> --api-url <url> Create a profile before authenticating it
|
|
10614
10638
|
profile use <name> Make a profile the active one
|
|
@@ -10642,6 +10666,10 @@ var COMMANDS = {
|
|
|
10642
10666
|
update: { bare: update }
|
|
10643
10667
|
};
|
|
10644
10668
|
var ROUTED_COMMANDS = new Set(Object.keys(COMMANDS));
|
|
10669
|
+
var ALIASES = { wallet: "wallets" };
|
|
10670
|
+
function canonicalCommand(word) {
|
|
10671
|
+
return word !== undefined && Object.hasOwn(ALIASES, word) ? ALIASES[word] : word;
|
|
10672
|
+
}
|
|
10645
10673
|
var ROUTED_SUBCOMMANDS = Object.fromEntries(Object.entries(COMMANDS).filter(([, route]) => route.subcommands !== undefined).map(([word, route]) => [word, Object.keys(route.subcommands ?? {})]));
|
|
10646
10674
|
function routeFor(word) {
|
|
10647
10675
|
return word !== undefined && Object.hasOwn(COMMANDS, word) ? COMMANDS[word] : undefined;
|
|
@@ -10671,7 +10699,7 @@ async function run2(argv, deps) {
|
|
|
10671
10699
|
const { rest, flags } = extracted;
|
|
10672
10700
|
const tokens = rest[0] === "candle" ? rest.slice(1) : rest;
|
|
10673
10701
|
if (flags.version) {
|
|
10674
|
-
const versionWord = tokens[0];
|
|
10702
|
+
const versionWord = canonicalCommand(tokens[0]);
|
|
10675
10703
|
if (versionWord !== undefined && ROUTED_COMMANDS.has(versionWord)) {
|
|
10676
10704
|
const fix = "--version prints the CLI version; to pin a release use: candle update --to <tag>";
|
|
10677
10705
|
writeUsageFailure(deps, fix, flags.json);
|
|
@@ -10685,7 +10713,8 @@ async function run2(argv, deps) {
|
|
|
10685
10713
|
deps.stdout.write(HELP_TEXT);
|
|
10686
10714
|
return 0;
|
|
10687
10715
|
}
|
|
10688
|
-
const [
|
|
10716
|
+
const [rawCmd, sub, ...cmdArgs] = tokens;
|
|
10717
|
+
const cmd = canonicalCommand(rawCmd);
|
|
10689
10718
|
const config = await migrateProfiles(deps);
|
|
10690
10719
|
const isAuthLogin = cmd === "auth" && sub === "login";
|
|
10691
10720
|
const isProfileCommand = cmd === "profile";
|
|
@@ -10850,5 +10879,6 @@ export {
|
|
|
10850
10879
|
buildRealDeps,
|
|
10851
10880
|
ROUTED_SUBCOMMANDS,
|
|
10852
10881
|
ROUTED_COMMANDS,
|
|
10853
|
-
NEVER_GUARDED
|
|
10882
|
+
NEVER_GUARDED,
|
|
10883
|
+
ALIASES
|
|
10854
10884
|
};
|
package/package.json
CHANGED