@candledottv/cli 0.6.1 → 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.
Files changed (3) hide show
  1. package/README.md +50 -4
  2. package/dist/index.js +46 -25
  3. 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
- npx @candledottv/cli auth login
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
- This runs `auth login`, which opens your browser to approve this device. Install it permanently
14
- with `npm install -g @candledottv/cli` and the command is just `candle`.
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 account = identity.ok ? identity.body.account : undefined;
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 account = effectiveProfileFields(config, ctx.profile).account;
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.1";
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
- if (body.apiKey)
5509
- account = (await fetchAccount(deps, ctx.apiUrl, body.apiKey.key)).account;
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
- if (apiKey)
5674
- account = (await fetchAccount(deps, apiUrl, apiKey)).account;
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
- deps.stdout.write(`${identityLine(ctx.profile, account ?? fields.account, apiUrl)}
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 identityAccount = effectiveProfileFields(identityConfig, ctx.profile).account;
6222
- deps.stderr.write(`${identityLine(ctx.profile, identityAccount, apiUrl, credentialEnvOverrides(deps.env))}
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
- await deps.updateProfile(name, { account: live, accountCachedAt: deps.now() });
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
  `);
@@ -10615,9 +10630,9 @@ Commands:
10615
10630
  keys create [--scopes <a,b,c>] [--label <name>] Create an API key
10616
10631
  [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]
10617
10632
  keys revoke <prefix> Revoke an API key
10618
- wallets Show launch and linked wallets
10619
- wallets import --chain <solana|evm> [options] Import a wallet you own (key via --key-file or hidden prompt)
10620
- wallets revoke <wallet-id> Revoke a linked wallet
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
10621
10636
  profile list Profiles on this machine, with cached accounts
10622
10637
  profile add <name> --api-url <url> Create a profile before authenticating it
10623
10638
  profile use <name> Make a profile the active one
@@ -10651,6 +10666,10 @@ var COMMANDS = {
10651
10666
  update: { bare: update }
10652
10667
  };
10653
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
+ }
10654
10673
  var ROUTED_SUBCOMMANDS = Object.fromEntries(Object.entries(COMMANDS).filter(([, route]) => route.subcommands !== undefined).map(([word, route]) => [word, Object.keys(route.subcommands ?? {})]));
10655
10674
  function routeFor(word) {
10656
10675
  return word !== undefined && Object.hasOwn(COMMANDS, word) ? COMMANDS[word] : undefined;
@@ -10680,7 +10699,7 @@ async function run2(argv, deps) {
10680
10699
  const { rest, flags } = extracted;
10681
10700
  const tokens = rest[0] === "candle" ? rest.slice(1) : rest;
10682
10701
  if (flags.version) {
10683
- const versionWord = tokens[0];
10702
+ const versionWord = canonicalCommand(tokens[0]);
10684
10703
  if (versionWord !== undefined && ROUTED_COMMANDS.has(versionWord)) {
10685
10704
  const fix = "--version prints the CLI version; to pin a release use: candle update --to <tag>";
10686
10705
  writeUsageFailure(deps, fix, flags.json);
@@ -10694,7 +10713,8 @@ async function run2(argv, deps) {
10694
10713
  deps.stdout.write(HELP_TEXT);
10695
10714
  return 0;
10696
10715
  }
10697
- const [cmd, sub, ...cmdArgs] = tokens;
10716
+ const [rawCmd, sub, ...cmdArgs] = tokens;
10717
+ const cmd = canonicalCommand(rawCmd);
10698
10718
  const config = await migrateProfiles(deps);
10699
10719
  const isAuthLogin = cmd === "auth" && sub === "login";
10700
10720
  const isProfileCommand = cmd === "profile";
@@ -10859,5 +10879,6 @@ export {
10859
10879
  buildRealDeps,
10860
10880
  ROUTED_SUBCOMMANDS,
10861
10881
  ROUTED_COMMANDS,
10862
- NEVER_GUARDED
10882
+ NEVER_GUARDED,
10883
+ ALIASES
10863
10884
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal",
5
5
  "type": "module",
6
6
  "bin": {