@mauricode/token-derby 2.10.0 → 2.11.0

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
@@ -45,7 +45,6 @@ var CLI_VERSION_HEADER = "x-cli-version";
45
45
  var USER_ID_HEADER = "x-user-id";
46
46
  var USER_TOKEN_HEADER = "x-user-token";
47
47
  var USER_NAME_MAX_LENGTH = 40;
48
- var ORG_NAME_MAX_LENGTH = 12;
49
48
  var ORG_NAME_PATTERN = /^[A-Za-z0-9]{1,12}$/;
50
49
 
51
50
  // ../shared/dist/version-match.js
@@ -206,34 +205,8 @@ var HATS = [
206
205
  { id: "aurora_helm", name: "Aurora Helm", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...QQQQQ...", "...QQQQQ...", "..AAAAAAA..", "..AAAAAAA..", "..AQAAAQA..", "..AQAAAQA.."], colors: { A: "#00CED1", Q: "#00FF7F" }, animation: { type: "cycle", frames: ["#0000FF", "#0066FF", "#00BFFF", "#00CED1", "#00FF7F", "#7CFC00", "#00FF7F", "#00CED1"], fps: 4 } }
207
206
  ];
208
207
 
209
- // ../shared/dist/schedule.js
210
- var ORDER = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
211
- function parseWeekdays(spec) {
212
- const tokens = spec.toLowerCase().split(",").map((s) => s.trim()).filter(Boolean);
213
- if (tokens.length === 0)
214
- return null;
215
- const set = /* @__PURE__ */ new Set();
216
- for (const tok of tokens) {
217
- if (tok.includes("-")) {
218
- const parts = tok.split("-");
219
- if (parts.length !== 2)
220
- return null;
221
- const [a, b] = parts;
222
- const ai = ORDER.indexOf(a);
223
- const bi = ORDER.indexOf(b);
224
- if (ai < 0 || bi < 0 || ai > bi)
225
- return null;
226
- for (let i = ai; i <= bi; i++)
227
- set.add(i + 1);
228
- } else {
229
- const i = ORDER.indexOf(tok);
230
- if (i < 0)
231
- return null;
232
- set.add(i + 1);
233
- }
234
- }
235
- return [...set].sort((x, y) => x - y);
236
- }
208
+ // ../shared/dist/series-transform.js
209
+ var PACE_WINDOW_MS = 15 * 6e4;
237
210
 
238
211
  // src/ui/HorseSprite.tsx
239
212
  import { Box, Text } from "ink";
@@ -740,8 +713,8 @@ var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
740
713
  // src/version.ts
741
714
  import { createRequire } from "module";
742
715
  function readVersion() {
743
- if ("2.10.0".length > 0) {
744
- return "2.10.0";
716
+ if ("2.11.0".length > 0) {
717
+ return "2.11.0";
745
718
  }
746
719
  try {
747
720
  const req = createRequire(import.meta.url);
@@ -897,18 +870,12 @@ function heartbeat(joinCode, horseId, token, body) {
897
870
  function endRace(adminCode) {
898
871
  return request("DELETE", `/races/admin/${encodeURIComponent(adminCode)}`, void 0, void 0);
899
872
  }
900
- function createOrganisation(body) {
901
- return request("POST", "/organisations", body, void 0);
902
- }
903
873
  function joinOrganisation(body) {
904
874
  return request("POST", "/organisations/join", body, void 0);
905
875
  }
906
876
  function listOrganisations() {
907
877
  return request("GET", "/organisations", void 0, void 0);
908
878
  }
909
- function getOrganisation(name) {
910
- return request("GET", `/organisations/${encodeURIComponent(name)}`, void 0, void 0);
911
- }
912
879
  function initJockey(body) {
913
880
  return request("POST", "/jockey/init", body, void 0);
914
881
  }
@@ -937,60 +904,15 @@ function deleteStableHorse(stableHorseId) {
937
904
  void 0
938
905
  );
939
906
  }
940
- function setOrgWebhook(orgName, body) {
941
- return request(
942
- "PUT",
943
- `/organisations/${encodeURIComponent(orgName)}/webhook`,
944
- body,
945
- void 0
946
- );
947
- }
948
- function getOrgWebhook(orgName) {
949
- return request(
950
- "GET",
951
- `/organisations/${encodeURIComponent(orgName)}/webhook`,
952
- void 0,
953
- void 0
954
- );
955
- }
956
- function deleteOrgWebhook(orgName) {
957
- return request(
958
- "DELETE",
959
- `/organisations/${encodeURIComponent(orgName)}/webhook`,
960
- void 0,
961
- void 0
962
- );
963
- }
964
- function setOrgSchedule(orgName, body) {
965
- return request(
966
- "PUT",
967
- `/organisations/${encodeURIComponent(orgName)}/schedule`,
968
- body,
969
- void 0
970
- );
971
- }
972
- function getOrgSchedule(orgName) {
973
- return request(
974
- "GET",
975
- `/organisations/${encodeURIComponent(orgName)}/schedule`,
976
- void 0,
977
- void 0
978
- );
979
- }
980
- function clearOrgSchedule(orgName) {
981
- return request(
982
- "DELETE",
983
- `/organisations/${encodeURIComponent(orgName)}/schedule`,
984
- void 0,
985
- void 0
986
- );
987
- }
988
907
  function rollHat(stableHorseId) {
989
908
  return request("POST", `/jockey/me/horses/${encodeURIComponent(stableHorseId)}/roll`, void 0, void 0);
990
909
  }
991
910
  function equipHat(stableHorseId, body) {
992
911
  return request("POST", `/jockey/me/horses/${encodeURIComponent(stableHorseId)}/equip`, body, void 0);
993
912
  }
913
+ function createWebSession() {
914
+ return request("POST", "/web-sessions", void 0, void 0);
915
+ }
994
916
 
995
917
  // src/commands/stable-create.ts
996
918
  async function stableCreateCommand() {
@@ -1430,7 +1352,7 @@ function TokenBreakdown(props) {
1430
1352
  ] });
1431
1353
  }
1432
1354
  function StatusScreen(props) {
1433
- const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, primaryModel, perSource, primaryCapped } = props;
1355
+ const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason, primaryModel, perSource, primaryCapped } = props;
1434
1356
  if (!race) {
1435
1357
  return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx7(Text7, { children: "Joining race\u2026" }) });
1436
1358
  }
@@ -1504,7 +1426,11 @@ function StatusScreen(props) {
1504
1426
  " ",
1505
1427
  /* @__PURE__ */ jsx7(Text7, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
1506
1428
  ] }),
1507
- stalled && /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u26A0 Can't read token usage \u2014 try restarting this terminal. Your race continues." })
1429
+ stalled && /* @__PURE__ */ jsxs5(Text7, { color: "yellow", children: [
1430
+ "\u26A0 ",
1431
+ stallReason ?? "Can't read token usage",
1432
+ ". Your race continues."
1433
+ ] })
1508
1434
  ] }),
1509
1435
  primaryModel && perSource && /* @__PURE__ */ jsx7(
1510
1436
  TokenBreakdown,
@@ -1817,6 +1743,9 @@ function parseJsonl(raw) {
1817
1743
  }
1818
1744
 
1819
1745
  // src/tokens/race-tokens.ts
1746
+ function isStall(r) {
1747
+ return "stall" in r;
1748
+ }
1820
1749
  var SCALAR_READERS = {
1821
1750
  claude: sumTokens,
1822
1751
  codex: sumCodexTokens,
@@ -1831,20 +1760,22 @@ function scoreFor(race, t) {
1831
1760
  return race.counts_input ? t.input + t.output : t.output;
1832
1761
  }
1833
1762
  async function readAllSources(race, primary) {
1763
+ const primaryByConv = /* @__PURE__ */ new Map();
1834
1764
  try {
1835
1765
  const primaryMap = await BY_CONVERSATION_READERS[primary]();
1836
- const primaryByConv = /* @__PURE__ */ new Map();
1837
1766
  for (const [id, totals] of primaryMap) primaryByConv.set(id, scoreFor(race, totals));
1838
- const secondary = { claude: 0, codex: 0, gemini: 0 };
1839
- await Promise.all(
1840
- MODEL_KEYS.filter((k) => k !== primary).map(async (k) => {
1841
- secondary[k] = await SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() => 0);
1842
- })
1843
- );
1844
- return { secondary, primaryByConv };
1845
- } catch {
1846
- return null;
1767
+ } catch (e) {
1768
+ if (e?.code !== "ENOENT") {
1769
+ return { stall: `Can't read ${primary} token usage: ${e?.message ?? String(e)}` };
1770
+ }
1847
1771
  }
1772
+ const secondary = { claude: 0, codex: 0, gemini: 0 };
1773
+ await Promise.all(
1774
+ MODEL_KEYS.filter((k) => k !== primary).map(async (k) => {
1775
+ secondary[k] = await SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() => 0);
1776
+ })
1777
+ );
1778
+ return { secondary, primaryByConv };
1848
1779
  }
1849
1780
 
1850
1781
  // src/tokens/primary-cap.ts
@@ -1866,6 +1797,7 @@ var RaceScoreTracker = class {
1866
1797
  counted;
1867
1798
  seq;
1868
1799
  stalls = 0;
1800
+ lastStall = null;
1869
1801
  primary;
1870
1802
  primaryTop5;
1871
1803
  constructor(init, primary, primaryTop5) {
@@ -1880,16 +1812,19 @@ var RaceScoreTracker = class {
1880
1812
  }
1881
1813
  /**
1882
1814
  * Record a scan result.
1883
- * - `null` → stall (warning), anchors untouched.
1815
+ * - `null` or a `{ stall }` reading → stall (warning), anchors untouched. A
1816
+ * stall reading also captures its cause for the UI.
1884
1817
  * - otherwise → secondaries advance scalar lastGood (never down to 0); the
1885
1818
  * primary's per-conversation latest readings are updated (monotonic).
1886
1819
  */
1887
1820
  recordReading(reading) {
1888
- if (reading === null) {
1821
+ if (reading === null || isStall(reading)) {
1889
1822
  this.stalls += 1;
1823
+ this.lastStall = reading?.stall ?? null;
1890
1824
  return;
1891
1825
  }
1892
1826
  this.stalls = 0;
1827
+ this.lastStall = null;
1893
1828
  for (const key of MODEL_KEYS) {
1894
1829
  if (key === this.primary) continue;
1895
1830
  const v = reading.secondary[key];
@@ -1946,6 +1881,10 @@ var RaceScoreTracker = class {
1946
1881
  get stalled() {
1947
1882
  return this.stalls >= STALL_THRESHOLD;
1948
1883
  }
1884
+ /** Human-readable cause of the most recent stall (null once a good read recovers). */
1885
+ get stallReason() {
1886
+ return this.lastStall;
1887
+ }
1949
1888
  /** Cumulative primary tokens credited so far (for the UI's primary "since join" row). */
1950
1889
  primaryCounted() {
1951
1890
  return this.counted;
@@ -1985,6 +1924,7 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
1985
1924
  const pendingRef = useRef(pendingMode);
1986
1925
  const ctrl = useRef(new AbortController());
1987
1926
  const [stalled, setStalled] = useState5(false);
1927
+ const [stallReason, setStallReason] = useState5(null);
1988
1928
  const baselineRef = useRef(initialState.acked);
1989
1929
  const [perSource, setPerSource] = useState5({ claude: 0, codex: 0, gemini: 0 });
1990
1930
  useEffect2(() => {
@@ -2006,15 +1946,16 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
2006
1946
  new Promise((_, reject) => setTimeout(() => reject(new Error("scan timeout")), 1e4))
2007
1947
  ]);
2008
1948
  } catch {
2009
- return null;
1949
+ return { stall: "Token scan timed out" };
2010
1950
  }
2011
1951
  };
2012
1952
  runHeartbeatLoop({
2013
1953
  prepareBeat: async () => {
2014
1954
  const reading = await scanWithTimeout();
2015
1955
  tracker.recordReading(reading);
2016
- if (pendingRef.current) tracker.reprime();
1956
+ if (pendingRef.current && !isStall(reading)) tracker.reprime();
2017
1957
  setStalled(tracker.stalled);
1958
+ setStallReason(tracker.stalled ? tracker.stallReason : null);
2018
1959
  const since = tracker.secondarySinceJoin(baselineRef.current);
2019
1960
  const ps = { claude: 0, codex: 0, gemini: 0 };
2020
1961
  for (const k of MODEL_KEYS) {
@@ -2087,6 +2028,7 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
2087
2028
  lastHeartbeatAgoSec,
2088
2029
  lastHeartbeatOk: lastHbOk,
2089
2030
  stalled,
2031
+ stallReason,
2090
2032
  primaryModel: active.primary_model,
2091
2033
  perSource,
2092
2034
  primaryCapped: primaryConversationCap(active.primary_top5 ?? false) !== Infinity
@@ -2134,14 +2076,16 @@ function raceViewFrom(resp) {
2134
2076
  };
2135
2077
  }
2136
2078
  async function buildInitialState(args) {
2137
- let now = null;
2079
+ let secondary = { claude: 0, codex: 0, gemini: 0 };
2080
+ const primaryConvAcked = {};
2138
2081
  try {
2139
- now = await readAllSources(args.active, args.active.primary_model);
2082
+ const now = await readAllSources(args.active, args.active.primary_model);
2083
+ if (!isStall(now)) {
2084
+ secondary = now.secondary;
2085
+ for (const [id, v] of now.primaryByConv) primaryConvAcked[id] = v;
2086
+ }
2140
2087
  } catch {
2141
2088
  }
2142
- const secondary = now?.secondary ?? { claude: 0, codex: 0, gemini: 0 };
2143
- const primaryConvAcked = {};
2144
- if (now) for (const [id, v] of now.primaryByConv) primaryConvAcked[id] = v;
2145
2089
  return {
2146
2090
  initialState: {
2147
2091
  acked: { ...secondary },
@@ -2780,8 +2724,8 @@ function pendingFor(horse) {
2780
2724
  }
2781
2725
  async function promptYesNo(question) {
2782
2726
  resetStdinAfterInk();
2783
- const readline7 = await import("readline/promises");
2784
- const rl = readline7.createInterface({ input: process.stdin, output: process.stdout });
2727
+ const readline6 = await import("readline/promises");
2728
+ const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
2785
2729
  const a = (await rl.question(question)).trim().toLowerCase();
2786
2730
  rl.close();
2787
2731
  return a === "" || a === "y" || a === "yes";
@@ -2905,39 +2849,6 @@ No hat this time. +${result.xp_awarded} XP toward your next level.
2905
2849
  }
2906
2850
  }
2907
2851
 
2908
- // src/commands/org-create.ts
2909
- import * as readline6 from "readline/promises";
2910
- import { stdin as stdin6, stdout as stdout6 } from "process";
2911
- async function orgCreateCommand() {
2912
- const rl = readline6.createInterface({ input: stdin6, output: stdout6 });
2913
- try {
2914
- const name = (await rl.question(`Organisation name (1\u2013${ORG_NAME_MAX_LENGTH} alphanumeric chars): `)).trim();
2915
- if (!ORG_NAME_PATTERN.test(name)) {
2916
- console.error(`Name must be 1\u2013${ORG_NAME_MAX_LENGTH} alphanumeric characters (no spaces or symbols).`);
2917
- return 1;
2918
- }
2919
- const resp = await createOrganisation({ name });
2920
- console.log("");
2921
- console.log(` Organisation created: ${resp.org_name}`);
2922
- console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
2923
- console.log(` \u2551 JOIN TOKEN: ${resp.org_join_token.padEnd(43)}\u2551`);
2924
- console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
2925
- console.log(" \u26A0 Share this token to invite members. Treat it as a secret.");
2926
- console.log("");
2927
- console.log(` Members join with: token-derby organisation join ${resp.org_join_token}`);
2928
- console.log(` Create org races: token-derby create --organisation ${resp.org_name}`);
2929
- return 0;
2930
- } catch (e) {
2931
- if (e instanceof ApiError) {
2932
- console.error(`Error: ${e.code} ${e.message}`);
2933
- return 1;
2934
- }
2935
- throw e;
2936
- } finally {
2937
- rl.close();
2938
- }
2939
- }
2940
-
2941
2852
  // src/commands/org-join.ts
2942
2853
  async function orgJoinCommand(token) {
2943
2854
  if (!token) {
@@ -2957,209 +2868,22 @@ async function orgJoinCommand(token) {
2957
2868
  }
2958
2869
  }
2959
2870
 
2960
- // src/commands/org-list.ts
2961
- async function orgListCommand() {
2962
- try {
2963
- const resp = await listOrganisations();
2964
- if (resp.organisations.length === 0) {
2965
- console.log("You are not in any organisations.");
2966
- console.log("Create one with: token-derby organisation create");
2967
- console.log("Or join one with: token-derby organisation join <token>");
2968
- return 0;
2969
- }
2970
- console.log(`Your organisations (${resp.organisations.length}):`);
2971
- for (const o of resp.organisations) {
2972
- console.log(` \u2022 ${o.org_name}`);
2973
- }
2974
- return 0;
2975
- } catch (e) {
2976
- if (e instanceof ApiError) {
2977
- console.error(`Error: ${e.code} ${e.message}`);
2978
- return 1;
2979
- }
2980
- throw e;
2981
- }
2982
- }
2983
-
2984
- // src/commands/org-info.ts
2985
- async function orgInfoCommand(name) {
2986
- if (!name) {
2987
- console.error("Usage: token-derby organisation info <name>");
2988
- return 2;
2989
- }
2990
- if (!ORG_NAME_PATTERN.test(name)) {
2991
- console.error("Organisation name must be 1\u201312 alphanumeric characters.");
2992
- return 2;
2993
- }
2994
- try {
2995
- const resp = await getOrganisation(name);
2996
- console.log(`Organisation: ${resp.org_name}`);
2997
- console.log(`Created: ${resp.created_at} by ${resp.creator_user_name}`);
2998
- console.log("");
2999
- console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
3000
- console.log(` \u2551 JOIN TOKEN: ${resp.org_join_token.padEnd(43)}\u2551`);
3001
- console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
3002
- console.log(" \u26A0 Treat the token as a secret \u2014 anyone with it can join.");
3003
- console.log("");
3004
- console.log(` Members join with: token-derby organisation join ${resp.org_join_token}`);
3005
- return 0;
3006
- } catch (e) {
3007
- if (e instanceof ApiError) {
3008
- console.error(`Error: ${e.code} ${e.message}`);
3009
- return 1;
3010
- }
3011
- throw e;
3012
- }
3013
- }
3014
-
3015
- // src/commands/org-webhook-set.ts
3016
- async function orgWebhookSetCommand(orgName, url) {
3017
- if (!orgName || !url) {
3018
- console.error("Usage: token-derby organisation webhook set <org-name> <https-url>");
3019
- return 2;
3020
- }
3021
- try {
3022
- const resp = await setOrgWebhook(orgName, { url });
3023
- console.log("");
3024
- console.log(` Webhook set for ${orgName}: ${resp.webhook_url}`);
3025
- console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
3026
- console.log(` \u2551 SECRET: ${resp.webhook_secret.padEnd(47)}\u2551`);
3027
- console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
3028
- console.log(" \u26A0 Save this secret now \u2014 it will not be shown again.");
3029
- console.log(" Your receiver verifies requests with: X-Token-Derby-Signature: sha256=<hmac(secret, raw body)>.");
3030
- return 0;
3031
- } catch (e) {
3032
- if (e instanceof ApiError) {
3033
- console.error(`Error: ${e.code} ${e.message}`);
3034
- return 1;
3035
- }
3036
- throw e;
3037
- }
2871
+ // src/commands/web.ts
2872
+ import { spawn as spawn2 } from "child_process";
2873
+ function webOrigin() {
2874
+ return apiBase().replace(/\/api\/?$/, "");
3038
2875
  }
3039
-
3040
- // src/commands/org-webhook-get.ts
3041
- async function orgWebhookGetCommand(orgName) {
3042
- if (!orgName) {
3043
- console.error("Usage: token-derby organisation webhook get <org-name>");
3044
- return 2;
3045
- }
3046
- try {
3047
- const resp = await getOrgWebhook(orgName);
3048
- if (resp.webhook_url) {
3049
- console.log(`Webhook for ${orgName}: ${resp.webhook_url}`);
3050
- } else {
3051
- console.log(`No webhook configured for ${orgName}.`);
3052
- }
3053
- return 0;
3054
- } catch (e) {
3055
- if (e instanceof ApiError) {
3056
- console.error(`Error: ${e.code} ${e.message}`);
3057
- return 1;
3058
- }
3059
- throw e;
3060
- }
3061
- }
3062
-
3063
- // src/commands/org-webhook-clear.ts
3064
- async function orgWebhookClearCommand(orgName) {
3065
- if (!orgName) {
3066
- console.error("Usage: token-derby organisation webhook clear <org-name>");
3067
- return 2;
3068
- }
3069
- try {
3070
- await deleteOrgWebhook(orgName);
3071
- console.log(`Webhook removed for ${orgName}.`);
3072
- return 0;
3073
- } catch (e) {
3074
- if (e instanceof ApiError) {
3075
- console.error(`Error: ${e.code} ${e.message}`);
3076
- return 1;
3077
- }
3078
- throw e;
3079
- }
3080
- }
3081
-
3082
- // src/commands/org-schedule-set.ts
3083
- function flag(args, name) {
3084
- for (let i = 0; i < args.length; i++) {
3085
- if (args[i] === name) return args[i + 1];
3086
- const eq = `${name}=`;
3087
- if (args[i]?.startsWith(eq)) return args[i].slice(eq.length);
3088
- }
3089
- return void 0;
3090
- }
3091
- var USAGE = 'Usage: token-derby organisation schedule set <org-name> --days mon-fri --start 09:00 --end 17:30 --tz Europe/London [--name "Daily"] [--max 30] [--counts-input] [--primary-top5]';
3092
- async function orgScheduleSetCommand(orgName, rest) {
3093
- if (!orgName) {
3094
- console.error(USAGE);
3095
- return 2;
3096
- }
3097
- const daysSpec = flag(rest, "--days");
3098
- const start = flag(rest, "--start");
3099
- const end = flag(rest, "--end");
3100
- const tz = flag(rest, "--tz");
3101
- const name = flag(rest, "--name");
3102
- const maxStr = flag(rest, "--max");
3103
- const counts_input = rest.includes("--counts-input");
3104
- const primary_top5 = rest.includes("--primary-top5");
3105
- if (!daysSpec || !start || !end || !tz) {
3106
- console.error("Required flags: --days, --start, --end, --tz");
3107
- console.error(USAGE);
3108
- return 2;
3109
- }
3110
- const weekdays = parseWeekdays(daysSpec);
3111
- if (!weekdays) {
3112
- console.error('Invalid --days. Use day names like "mon-fri" or "mon,wed,fri".');
3113
- return 2;
3114
- }
3115
- let max_participants;
3116
- if (maxStr !== void 0) {
3117
- max_participants = Number(maxStr);
3118
- if (!Number.isInteger(max_participants) || max_participants < 1) {
3119
- console.error("--max must be a positive integer");
3120
- return 2;
3121
- }
3122
- }
3123
- try {
3124
- const resp = await setOrgSchedule(orgName, {
3125
- weekdays,
3126
- start_local: start,
3127
- end_local: end,
3128
- tz,
3129
- ...name ? { race_name: name } : {},
3130
- ...max_participants !== void 0 ? { max_participants } : {},
3131
- ...counts_input ? { counts_input: true } : {},
3132
- ...primary_top5 ? { primary_top5: true } : {}
3133
- });
3134
- const s = resp.schedule;
3135
- console.log(`Schedule set for ${orgName}: days [${s.weekdays.join(",")}] ${s.start_local}\u2013${s.end_local} ${s.tz}`);
3136
- if (s.primary_top5) console.log(" Primary score counts only each racer's top 5 conversations per beat.");
3137
- return 0;
3138
- } catch (e) {
3139
- if (e instanceof ApiError) {
3140
- console.error(`Error: ${e.code} ${e.message}`);
3141
- return 1;
3142
- }
3143
- throw e;
3144
- }
2876
+ function opener() {
2877
+ if (process.platform === "darwin") return "open";
2878
+ if (process.platform === "win32") return "start";
2879
+ if (process.platform === "linux") return "xdg-open";
2880
+ return null;
3145
2881
  }
3146
-
3147
- // src/commands/org-schedule-get.ts
3148
- async function orgScheduleGetCommand(orgName) {
3149
- if (!orgName) {
3150
- console.error("Usage: token-derby organisation schedule get <org-name>");
3151
- return 2;
3152
- }
2882
+ async function webCommand(deps = {}) {
2883
+ const spawnImpl = deps.spawnImpl ?? spawn2;
2884
+ let code;
3153
2885
  try {
3154
- const resp = await getOrgSchedule(orgName);
3155
- if (resp.schedule) {
3156
- const s = resp.schedule;
3157
- console.log(`Schedule for ${orgName}: days [${s.weekdays.join(",")}] ${s.start_local}\u2013${s.end_local} ${s.tz}`);
3158
- if (s.primary_top5) console.log(" Primary top-5 conversations cap: ON.");
3159
- } else {
3160
- console.log(`No schedule configured for ${orgName}.`);
3161
- }
3162
- return 0;
2886
+ ({ code } = await createWebSession());
3163
2887
  } catch (e) {
3164
2888
  if (e instanceof ApiError) {
3165
2889
  console.error(`Error: ${e.code} ${e.message}`);
@@ -3167,25 +2891,23 @@ async function orgScheduleGetCommand(orgName) {
3167
2891
  }
3168
2892
  throw e;
3169
2893
  }
3170
- }
3171
-
3172
- // src/commands/org-schedule-clear.ts
3173
- async function orgScheduleClearCommand(orgName) {
3174
- if (!orgName) {
3175
- console.error("Usage: token-derby organisation schedule clear <org-name>");
3176
- return 2;
3177
- }
3178
- try {
3179
- await clearOrgSchedule(orgName);
3180
- console.log(`Schedule removed for ${orgName}.`);
3181
- return 0;
3182
- } catch (e) {
3183
- if (e instanceof ApiError) {
3184
- console.error(`Error: ${e.code} ${e.message}`);
3185
- return 1;
2894
+ const url = `${webOrigin()}/org-manager#code=${code}`;
2895
+ console.log("");
2896
+ console.log(" Opening the Token Derby org manager in your browser...");
2897
+ console.log(` ${url}`);
2898
+ console.log("");
2899
+ console.log(" If it doesn't open, copy the link above. It expires in 60 seconds.");
2900
+ const cmd = opener();
2901
+ if (cmd) {
2902
+ try {
2903
+ const child = spawnImpl(cmd, [url], { stdio: "ignore", detached: true });
2904
+ child.on("error", () => {
2905
+ });
2906
+ child.unref();
2907
+ } catch {
3186
2908
  }
3187
- throw e;
3188
2909
  }
2910
+ return 0;
3189
2911
  }
3190
2912
 
3191
2913
  // src/bin.ts
@@ -3207,25 +2929,9 @@ Stable management:
3207
2929
  token-derby stable delete <name> Remove a horse from your stable
3208
2930
 
3209
2931
  Organisations:
3210
- token-derby organisation create Create a new organisation (interactive)
3211
2932
  token-derby organisation join <token> Join an organisation with a join token
3212
- token-derby organisation info <name> Show an org's join token (members only)
3213
- token-derby organisation list Show organisations you're a member of
3214
- token-derby organisation webhook set <name> <url>
3215
- Configure an https webhook for race events.
3216
- Prints a secret used to sign each request.
3217
- Only the org creator can run this.
3218
- token-derby organisation webhook get <name>
3219
- Show the org's configured webhook URL (or "no webhook").
3220
- token-derby organisation webhook clear <name>
3221
- Remove the webhook for this org.
3222
- token-derby organisation schedule set <name> --days mon-fri --start 09:00 --end 17:30 --tz Europe/London
3223
- Configure a repeating race schedule. Races auto-start
3224
- at the daily start time. Only the org creator can run this.
3225
- token-derby organisation schedule get <name>
3226
- Show the org's configured schedule (or "no schedule").
3227
- token-derby organisation schedule clear <name>
3228
- Remove the repeating schedule for this org.
2933
+ token-derby web Open the web org manager (create orgs,
2934
+ manage schedules, webhooks, members)
3229
2935
 
3230
2936
  Races:
3231
2937
  token-derby create [--organisation <name>]
@@ -3276,30 +2982,9 @@ async function main() {
3276
2982
  }
3277
2983
  if (cmd === "organisation" || cmd === "org") {
3278
2984
  const sub = argv[1];
3279
- if (sub === "create") return orgCreateCommand();
3280
2985
  if (sub === "join") return orgJoinCommand(argv[2]);
3281
- if (sub === "info") return orgInfoCommand(argv[2]);
3282
- if (sub === "list") return orgListCommand();
3283
- if (sub === "webhook") {
3284
- const action = argv[2];
3285
- if (action === "set") return orgWebhookSetCommand(argv[3], argv[4]);
3286
- if (action === "get") return orgWebhookGetCommand(argv[3]);
3287
- if (action === "clear") return orgWebhookClearCommand(argv[3]);
3288
- console.error(`Unknown webhook action: ${action ?? "(none)"}`);
3289
- console.error("Try: organisation webhook set <name> <url> | organisation webhook get <name> | organisation webhook clear <name>");
3290
- return 2;
3291
- }
3292
- if (sub === "schedule") {
3293
- const action = argv[2];
3294
- if (action === "set") return orgScheduleSetCommand(argv[3], argv.slice(4));
3295
- if (action === "get") return orgScheduleGetCommand(argv[3]);
3296
- if (action === "clear") return orgScheduleClearCommand(argv[3]);
3297
- console.error(`Unknown schedule action: ${action ?? "(none)"}`);
3298
- console.error("Try: organisation schedule set <name> ... | organisation schedule get <name> | organisation schedule clear <name>");
3299
- return 2;
3300
- }
3301
- console.error(`Unknown organisation subcommand: ${sub ?? "(none)"}`);
3302
- console.error("Try: organisation create | organisation join <token> | organisation info <name> | organisation list | organisation webhook <set|get|clear> ... | organisation schedule <set|get|clear> ...");
2986
+ console.error(`Organisation management has moved to the web: token-derby web`);
2987
+ console.error(`The only CLI organisation command is: organisation join <token>`);
3303
2988
  return 2;
3304
2989
  }
3305
2990
  if (cmd === "create") {
@@ -3309,14 +2994,15 @@ async function main() {
3309
2994
  if (cmd === "join") return joinCommand(argv[1], argv.slice(2));
3310
2995
  if (cmd === "end") return endCommand(argv[1]);
3311
2996
  if (cmd === "roll") return rollCommand();
2997
+ if (cmd === "web") return webCommand();
3312
2998
  console.error(`Unknown command: ${cmd}`);
3313
2999
  console.error(HELP);
3314
3000
  return 2;
3315
3001
  }
3316
- function parseFlag(args, flag2) {
3002
+ function parseFlag(args, flag) {
3317
3003
  for (let i = 0; i < args.length; i++) {
3318
- if (args[i] === flag2) return args[i + 1];
3319
- const eq = `${flag2}=`;
3004
+ if (args[i] === flag) return args[i + 1];
3005
+ const eq = `${flag}=`;
3320
3006
  if (args[i]?.startsWith(eq)) return args[i].slice(eq.length);
3321
3007
  }
3322
3008
  return void 0;