@mauricode/token-derby 2.12.0 → 2.12.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/dist/bin.js CHANGED
@@ -26,19 +26,10 @@ import TextInput from "ink-text-input";
26
26
 
27
27
  // ../shared/dist/models.js
28
28
  var MODEL_KEYS = ["claude", "codex", "gemini"];
29
- var SECONDARY_WEIGHT = 0.1;
29
+ var SECONDARY_WEIGHT = 0.5;
30
30
  function isModelKey(v) {
31
31
  return typeof v === "string" && MODEL_KEYS.includes(v);
32
32
  }
33
- function weightFor(primary, key) {
34
- return key === primary ? 1 : SECONDARY_WEIGHT;
35
- }
36
- function weightedTotal(primary, perSource) {
37
- let total = 0;
38
- for (const key of MODEL_KEYS)
39
- total += perSource[key] * weightFor(primary, key);
40
- return total;
41
- }
42
33
 
43
34
  // ../shared/dist/constants.js
44
35
  var CLI_VERSION_HEADER = "x-cli-version";
@@ -208,6 +199,59 @@ var HATS = [
208
199
  // ../shared/dist/series-transform.js
209
200
  var PACE_WINDOW_MS = 15 * 6e4;
210
201
 
202
+ // ../shared/dist/sprite-grid.js
203
+ var ROWS = [
204
+ "................................",
205
+ "................................",
206
+ "..........................MMM...",
207
+ "..........................MMM...",
208
+ ".........................MBBEBB.",
209
+ ".........................MBBEBB.",
210
+ "........................MBBBBBBB",
211
+ "........................MBBBBBBB",
212
+ "..................MMMMMMMBBB....",
213
+ "..................MMMMMMMBBB....",
214
+ "....BBBBBBBBSSSSSSMMBBBBBB......",
215
+ "...BBBBBBBBBSSSSSSMMBBBBBB......",
216
+ ".TTBBBBBBBBBSSSSSSBBBBBBBB......",
217
+ ".TTBBBBBBBBBSSSSSSBBBBBBBB......",
218
+ "TTTBBBBBBBBBBBBBBBBBBBBBBB......",
219
+ "TTTBBBBBBBBBBBBBBBBBBBBB........",
220
+ "...BBB.BBB.....BBB.BBB..........",
221
+ "...BBB.BBB.....BBB.BBB..........",
222
+ "....BB..BB......BB..BB..........",
223
+ "....BB..BB......BB..BB..........",
224
+ "....BB..BB......BB..BB..........",
225
+ "....BB..BB......BB..BB..........",
226
+ "....BB..BB......BB..BB..........",
227
+ "...HHH.HHH.....HHH.HHH.........."
228
+ ];
229
+ var GRID = ROWS.map((row, y) => {
230
+ if (row.length !== 32)
231
+ throw new Error(`sprite row ${y} has length ${row.length}, expected 32`);
232
+ return [...row].map((c) => toTag(c, y));
233
+ });
234
+ function toTag(c, y) {
235
+ switch (c) {
236
+ case "B":
237
+ return "B";
238
+ case "M":
239
+ return "M";
240
+ case "T":
241
+ return "T";
242
+ case "S":
243
+ return "S";
244
+ case "H":
245
+ return "H";
246
+ case "E":
247
+ return "B";
248
+ case ".":
249
+ return null;
250
+ default:
251
+ throw new Error(`unknown sprite char '${c}' at y=${y}`);
252
+ }
253
+ }
254
+
211
255
  // src/ui/HorseSprite.tsx
212
256
  import { Box, Text } from "ink";
213
257
 
@@ -258,10 +302,10 @@ function parse(rows, width, height) {
258
302
  if (row.length !== width) {
259
303
  throw new Error(`sprite row ${y} has length ${row.length}, expected ${width}`);
260
304
  }
261
- return [...row].map((c) => toTag(c));
305
+ return [...row].map((c) => toTag2(c));
262
306
  });
263
307
  }
264
- function toTag(c) {
308
+ function toTag2(c) {
265
309
  switch (c) {
266
310
  case "B":
267
311
  return "B";
@@ -702,10 +746,40 @@ function renderHatLabel(hats, hatChoice, initialEquipped) {
702
746
  ] });
703
747
  }
704
748
 
749
+ // src/env/env.ts
750
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
751
+ import * as os from "os";
752
+ import * as path from "path";
753
+ var ENV_NAMES = ["prod", "staging"];
754
+ function baseDir() {
755
+ return process.env.TOKEN_DERBY_BASE ?? os.homedir();
756
+ }
757
+ function configFile() {
758
+ return path.join(baseDir(), ".token-derby", "config.json");
759
+ }
760
+ function selectedEnv() {
761
+ try {
762
+ const raw = readFileSync(configFile(), "utf8");
763
+ const parsed = JSON.parse(raw);
764
+ if (parsed.env === "prod" || parsed.env === "staging") return parsed.env;
765
+ return "prod";
766
+ } catch {
767
+ return "prod";
768
+ }
769
+ }
770
+ function setSelectedEnv(env) {
771
+ const file = configFile();
772
+ mkdirSync(path.dirname(file), { recursive: true });
773
+ writeFileSync(file, JSON.stringify({ env }, null, 2) + "\n", "utf8");
774
+ }
775
+
705
776
  // src/config.ts
706
- var DEFAULT_API_BASE = "https://token-derby.mauricode.co.uk/api";
777
+ var ENVIRONMENTS = {
778
+ prod: { apiBase: "https://token-derby.mauricode.co.uk/api" },
779
+ staging: { apiBase: "https://token-derby-staging.mauricode.co.uk/api" }
780
+ };
707
781
  function apiBase() {
708
- return process.env.TOKEN_DERBY_API_BASE ?? DEFAULT_API_BASE;
782
+ return process.env.TOKEN_DERBY_API_BASE ?? ENVIRONMENTS[selectedEnv()].apiBase;
709
783
  }
710
784
  var HEARTBEAT_INTERVAL_MS = 6e4;
711
785
  var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
@@ -713,8 +787,8 @@ var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
713
787
  // src/version.ts
714
788
  import { createRequire } from "module";
715
789
  function readVersion() {
716
- if ("2.12.0".length > 0) {
717
- return "2.12.0";
790
+ if ("2.12.2".length > 0) {
791
+ return "2.12.2";
718
792
  }
719
793
  try {
720
794
  const req = createRequire(import.meta.url);
@@ -728,31 +802,34 @@ var CLI_VERSION = readVersion();
728
802
 
729
803
  // src/identity/identity.ts
730
804
  import { promises as fs } from "fs";
731
- import * as path2 from "path";
805
+ import * as path3 from "path";
732
806
 
733
807
  // src/paths.ts
734
- import * as os from "os";
735
- import * as path from "path";
808
+ import * as os2 from "os";
809
+ import * as path2 from "path";
736
810
  function homeDir() {
737
- return process.env.TOKEN_DERBY_HOME ?? path.join(os.homedir(), ".token-derby");
811
+ const override = process.env.TOKEN_DERBY_HOME;
812
+ if (override) return override;
813
+ const dir = selectedEnv() === "staging" ? ".token-derby-staging" : ".token-derby";
814
+ return path2.join(baseDir(), dir);
738
815
  }
739
816
  function identityFile() {
740
- return path.join(homeDir(), "identity.json");
817
+ return path2.join(homeDir(), "identity.json");
741
818
  }
742
819
  function activeRaceFile(joinCode) {
743
- return path.join(homeDir(), "active-races", `${joinCode}.json`);
820
+ return path2.join(homeDir(), "active-races", `${joinCode}.json`);
744
821
  }
745
822
  function activeRacesDir() {
746
- return path.join(homeDir(), "active-races");
823
+ return path2.join(homeDir(), "active-races");
747
824
  }
748
825
  function claudeProjectsDir() {
749
- return process.env.TOKEN_DERBY_CLAUDE_DIR ?? path.join(os.homedir(), ".claude", "projects");
826
+ return process.env.TOKEN_DERBY_CLAUDE_DIR ?? path2.join(os2.homedir(), ".claude", "projects");
750
827
  }
751
828
  function codexSessionsDir() {
752
- return process.env.TOKEN_DERBY_CODEX_DIR ?? path.join(os.homedir(), ".codex");
829
+ return process.env.TOKEN_DERBY_CODEX_DIR ?? path2.join(os2.homedir(), ".codex");
753
830
  }
754
831
  function geminiTmpDir() {
755
- return process.env.TOKEN_DERBY_GEMINI_DIR ?? path.join(os.homedir(), ".gemini", "tmp");
832
+ return process.env.TOKEN_DERBY_GEMINI_DIR ?? path2.join(os2.homedir(), ".gemini", "tmp");
756
833
  }
757
834
 
758
835
  // src/identity/identity.ts
@@ -808,8 +885,8 @@ function getIdentity() {
808
885
  function _resetIdentityCacheForTests() {
809
886
  identityCache = null;
810
887
  }
811
- async function request(method, path7, body, horseAuthToken, fetchImpl = fetch) {
812
- const url = path7.startsWith("http") ? path7 : `${apiBase()}${path7}`;
888
+ async function request(method, path8, body, horseAuthToken, fetchImpl = fetch) {
889
+ const url = path8.startsWith("http") ? path8 : `${apiBase()}${path8}`;
813
890
  const headers = {};
814
891
  headers[CLI_VERSION_HEADER] = CLI_VERSION;
815
892
  headers["user-agent"] = `token-derby/${CLI_VERSION}`;
@@ -1310,7 +1387,7 @@ function PrimaryPicker({ onPick }) {
1310
1387
  else if (key.return) onPick(MODEL_KEYS[i]);
1311
1388
  });
1312
1389
  return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
1313
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Pick your primary model for this race (counts 1:1; the others count at 10%)." }),
1390
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Pick your primary model for this race (counts 1:1; the others count at 50%)." }),
1314
1391
  /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "This is locked for the whole race \u2014 you can't change it, even by rejoining." }),
1315
1392
  MODEL_KEYS.map((m, idx) => /* @__PURE__ */ jsxs4(Text6, { color: idx === i ? "cyan" : void 0, children: [
1316
1393
  idx === i ? "\u276F " : " ",
@@ -1321,7 +1398,7 @@ function PrimaryPicker({ onPick }) {
1321
1398
 
1322
1399
  // src/stable/active-race.ts
1323
1400
  import * as fs2 from "fs/promises";
1324
- import * as path3 from "path";
1401
+ import * as path4 from "path";
1325
1402
  async function saveActiveRace(active) {
1326
1403
  await fs2.mkdir(activeRacesDir(), { recursive: true });
1327
1404
  await fs2.writeFile(
@@ -1339,20 +1416,20 @@ import { Box as Box8, Text as Text8, useApp } from "ink";
1339
1416
  import { Box as Box7, Text as Text7 } from "ink";
1340
1417
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1341
1418
  var MODEL_LABELS = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
1342
- function TokenBreakdown(props) {
1343
- const { primaryModel, perSource, raceScore, primaryCapped } = props;
1344
- const primaryTag = primaryCapped ? "(primary \xB7 top 5/beat)" : "(primary)";
1345
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
1346
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Tokens by model (since join)" }),
1347
- MODEL_KEYS.map((m) => /* @__PURE__ */ jsxs5(Text7, { children: [
1348
- ` ${MODEL_LABELS[m].padEnd(10)} ${perSource[m].toLocaleString().padStart(12)} `,
1349
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: m === primaryModel ? primaryTag : "(10%)" })
1350
- ] }, m)),
1351
- /* @__PURE__ */ jsx7(Text7, { children: ` ${"Race score".padEnd(10)} ${Math.round(raceScore).toLocaleString().padStart(12)}` })
1352
- ] });
1419
+ function ModelList(props) {
1420
+ const { primaryModel } = props;
1421
+ const secondaryTag = ` (${Math.round(SECONDARY_WEIGHT * 100)}%)`;
1422
+ return /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
1423
+ "Models: ",
1424
+ MODEL_KEYS.map((m, i) => /* @__PURE__ */ jsxs5(Text7, { children: [
1425
+ i > 0 ? " \xB7 " : "",
1426
+ MODEL_LABELS[m],
1427
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: m === primaryModel ? " (primary)" : secondaryTag })
1428
+ ] }, m))
1429
+ ] }) });
1353
1430
  }
1354
1431
  function StatusScreen(props) {
1355
- const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason, primaryModel, perSource, primaryCapped } = props;
1432
+ const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason, primaryModel } = props;
1356
1433
  if (!race) {
1357
1434
  return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx7(Text7, { children: "Joining race\u2026" }) });
1358
1435
  }
@@ -1432,15 +1509,7 @@ function StatusScreen(props) {
1432
1509
  ". Your race continues."
1433
1510
  ] })
1434
1511
  ] }),
1435
- primaryModel && perSource && /* @__PURE__ */ jsx7(
1436
- TokenBreakdown,
1437
- {
1438
- primaryModel,
1439
- perSource,
1440
- raceScore: own?.current_tokens ?? weightedTotal(primaryModel, perSource),
1441
- primaryCapped
1442
- }
1443
- ),
1512
+ primaryModel && /* @__PURE__ */ jsx7(ModelList, { primaryModel }),
1444
1513
  /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
1445
1514
  ] });
1446
1515
  }
@@ -1512,11 +1581,11 @@ function runHeartbeatLoop(opts) {
1512
1581
 
1513
1582
  // src/tokens/transcripts.ts
1514
1583
  import * as fs3 from "fs/promises";
1515
- import * as path4 from "path";
1584
+ import * as path5 from "path";
1516
1585
  var MAX_PROJECT_DEPTH = 8;
1517
1586
  function conversationId(file, root) {
1518
- const rel = path4.relative(root, file);
1519
- const [project, session] = rel.split(path4.sep);
1587
+ const rel = path5.relative(root, file);
1588
+ const [project, session] = rel.split(path5.sep);
1520
1589
  if (project === void 0 || session === void 0) return rel.replace(/\.jsonl$/, "");
1521
1590
  return `${project}/${session.replace(/\.jsonl$/, "")}`;
1522
1591
  }
@@ -1548,7 +1617,7 @@ async function listJsonlFiles(root) {
1548
1617
  const projects = await fs3.readdir(root);
1549
1618
  const out = [];
1550
1619
  for (const project of projects) {
1551
- const projectDir = path4.join(root, project);
1620
+ const projectDir = path5.join(root, project);
1552
1621
  const stat3 = await fs3.stat(projectDir);
1553
1622
  if (!stat3.isDirectory()) continue;
1554
1623
  await collectJsonl(projectDir, MAX_PROJECT_DEPTH, out);
@@ -1560,9 +1629,9 @@ async function collectJsonl(dir, depth, out) {
1560
1629
  const entries = await fs3.readdir(dir);
1561
1630
  for (const entry of entries) {
1562
1631
  if (entry.endsWith(".jsonl")) {
1563
- out.push(path4.join(dir, entry));
1632
+ out.push(path5.join(dir, entry));
1564
1633
  } else if (depth > 1) {
1565
- const child = path4.join(dir, entry);
1634
+ const child = path5.join(dir, entry);
1566
1635
  const st = await fs3.stat(child);
1567
1636
  if (st.isDirectory()) await collectJsonl(child, depth - 1, out);
1568
1637
  }
@@ -1593,7 +1662,7 @@ async function sumFile(file) {
1593
1662
 
1594
1663
  // src/tokens/codex.ts
1595
1664
  import * as fs4 from "fs/promises";
1596
- import * as path5 from "path";
1665
+ import * as path6 from "path";
1597
1666
  function num(v) {
1598
1667
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
1599
1668
  }
@@ -1601,8 +1670,8 @@ async function sumCodexByConversation() {
1601
1670
  const root = codexSessionsDir();
1602
1671
  await fs4.stat(root);
1603
1672
  const files = [
1604
- ...await collectRollouts(path5.join(root, "sessions")),
1605
- ...await collectRollouts(path5.join(root, "archived_sessions"))
1673
+ ...await collectRollouts(path6.join(root, "sessions")),
1674
+ ...await collectRollouts(path6.join(root, "archived_sessions"))
1606
1675
  ];
1607
1676
  const byConv = /* @__PURE__ */ new Map();
1608
1677
  for (const file of files) {
@@ -1630,7 +1699,7 @@ async function collectRollouts(dir) {
1630
1699
  }
1631
1700
  const out = [];
1632
1701
  for (const entry of entries) {
1633
- const full = path5.join(dir, entry.name);
1702
+ const full = path6.join(dir, entry.name);
1634
1703
  if (entry.isDirectory()) out.push(...await collectRollouts(full));
1635
1704
  else if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) out.push(full);
1636
1705
  }
@@ -1665,7 +1734,7 @@ async function lastTokenCount(file) {
1665
1734
 
1666
1735
  // src/tokens/gemini.ts
1667
1736
  import * as fs5 from "fs/promises";
1668
- import * as path6 from "path";
1737
+ import * as path7 from "path";
1669
1738
  function num2(v) {
1670
1739
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
1671
1740
  }
@@ -1691,7 +1760,7 @@ async function listChatFiles(root) {
1691
1760
  const entries = await fs5.readdir(root);
1692
1761
  const out = [];
1693
1762
  for (const entry of entries) {
1694
- const chatsDir = path6.join(root, entry, "chats");
1763
+ const chatsDir = path7.join(root, entry, "chats");
1695
1764
  let files;
1696
1765
  try {
1697
1766
  files = await fs5.readdir(chatsDir);
@@ -1699,7 +1768,7 @@ async function listChatFiles(root) {
1699
1768
  continue;
1700
1769
  }
1701
1770
  for (const f of files) {
1702
- if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path6.join(chatsDir, f));
1771
+ if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path7.join(chatsDir, f));
1703
1772
  }
1704
1773
  }
1705
1774
  return out;
@@ -1925,8 +1994,6 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
1925
1994
  const ctrl = useRef(new AbortController());
1926
1995
  const [stalled, setStalled] = useState5(false);
1927
1996
  const [stallReason, setStallReason] = useState5(null);
1928
- const baselineRef = useRef(initialState.acked);
1929
- const [perSource, setPerSource] = useState5({ claude: 0, codex: 0, gemini: 0 });
1930
1997
  useEffect2(() => {
1931
1998
  const t = setInterval(() => setTickNow(/* @__PURE__ */ new Date()), 1e3);
1932
1999
  return () => clearInterval(t);
@@ -1956,12 +2023,6 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
1956
2023
  if (pendingRef.current && !isStall(reading)) tracker.reprime();
1957
2024
  setStalled(tracker.stalled);
1958
2025
  setStallReason(tracker.stalled ? tracker.stallReason : null);
1959
- const since = tracker.secondarySinceJoin(baselineRef.current);
1960
- const ps = { claude: 0, codex: 0, gemini: 0 };
1961
- for (const k of MODEL_KEYS) {
1962
- ps[k] = k === active.primary_model ? tracker.primaryCounted() : since[k];
1963
- }
1964
- setPerSource(ps);
1965
2026
  return tracker.nextBeat();
1966
2027
  },
1967
2028
  sendBeat: async (snapshot) => {
@@ -2029,9 +2090,7 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
2029
2090
  lastHeartbeatOk: lastHbOk,
2030
2091
  stalled,
2031
2092
  stallReason,
2032
- primaryModel: active.primary_model,
2033
- perSource,
2034
- primaryCapped: primaryConversationCap(active.primary_top5 ?? false) !== Infinity
2093
+ primaryModel: active.primary_model
2035
2094
  }
2036
2095
  ),
2037
2096
  achievements.length > 0 && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", marginTop: 1, children: [
@@ -2910,6 +2969,36 @@ async function webCommand(deps = {}) {
2910
2969
  return 0;
2911
2970
  }
2912
2971
 
2972
+ // src/commands/env.ts
2973
+ function warnOverrides() {
2974
+ if (process.env.TOKEN_DERBY_HOME) {
2975
+ console.error("Warning: TOKEN_DERBY_HOME overrides the per-env data directory; `env` selection does not change where the identity is stored.");
2976
+ }
2977
+ if (process.env.TOKEN_DERBY_API_BASE) {
2978
+ console.error("Warning: TOKEN_DERBY_API_BASE overrides the API base; `env` selection does not change which API is used.");
2979
+ }
2980
+ }
2981
+ function envCommand(arg) {
2982
+ if (!arg) {
2983
+ console.log(`Environment: ${selectedEnv()}`);
2984
+ console.log(`API base: ${apiBase()}`);
2985
+ console.log(`Data dir: ${homeDir()}`);
2986
+ warnOverrides();
2987
+ return 0;
2988
+ }
2989
+ if (!ENV_NAMES.includes(arg)) {
2990
+ console.error(`Unknown environment: ${arg}`);
2991
+ console.error(`Valid environments: ${ENV_NAMES.join(", ")}`);
2992
+ return 2;
2993
+ }
2994
+ setSelectedEnv(arg);
2995
+ console.log(`Switched to ${arg}.`);
2996
+ console.log(`API base: ${apiBase()}`);
2997
+ console.log(`Data dir: ${homeDir()}`);
2998
+ warnOverrides();
2999
+ return 0;
3000
+ }
3001
+
2913
3002
  // src/bin.ts
2914
3003
  var HELP = `token-derby v${CLI_VERSION}
2915
3004
 
@@ -2946,8 +3035,12 @@ Cosmetics:
2946
3035
  Earn rolls by leveling up horses.
2947
3036
 
2948
3037
  Environment:
2949
- TOKEN_DERBY_API_BASE Override API base URL (default: production)
2950
- TOKEN_DERBY_HOME Override identity/stable directory
3038
+ token-derby env Show the active environment (prod|staging)
3039
+ token-derby env <prod|staging> Switch environment. Each env has its own
3040
+ identity/stable dir, so switching never
3041
+ touches the other env's account.
3042
+ TOKEN_DERBY_API_BASE Hard-override API base URL (wins over env)
3043
+ TOKEN_DERBY_HOME Hard-override identity/stable directory
2951
3044
  `;
2952
3045
  async function main() {
2953
3046
  const argv = process.argv.slice(2);
@@ -2965,6 +3058,7 @@ async function main() {
2965
3058
  return initCommand(reset);
2966
3059
  }
2967
3060
  if (cmd === "update") return updateCommand();
3061
+ if (cmd === "env") return envCommand(argv[1]);
2968
3062
  const identity = await loadIdentity();
2969
3063
  if (!identity) {
2970
3064
  console.error("Run `token-derby init` to set up your identity before using any other command.");