token-larper 1.6.1 → 1.7.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/README.md CHANGED
@@ -99,8 +99,15 @@ You can run Token Larper instantly without cloning:
99
99
  ```bash
100
100
  npx token-larper
101
101
  # or
102
- bunx tokenlarper
102
+ bunx token-larper
103
103
  ```
104
+ It starts in the background, opens the dashboard, and keeps running after you close the terminal. On Windows, quit it from the tray icon. On any platform:
105
+ ```bash
106
+ bunx token-larper stop # stop it
107
+ bunx token-larper --foreground # run in the terminal with logs instead
108
+ bunx token-larper --help # all options
109
+ ```
110
+ The background server writes its output to `server.log` in the data folder.
104
111
 
105
112
  ---
106
113
 
@@ -132,7 +139,7 @@ bun run tray
132
139
  ```
133
140
  > **Shortcut**: You can also simply **double-click** `Start-TokenLarper.vbs` in File Explorer!
134
141
 
135
- * A green **`t.`** monogram icon will appear in your notification tray.
142
+ * The **Burning t.** icon will appear in your notification tray.
136
143
  * Hover over the icon to see your real-time token count and cost estimate.
137
144
  * Left-click the icon for a compact usage popup with an **Open dashboard** button. Right-click it to sync or configure boot auto-start.
138
145
 
package/bin/cli.js CHANGED
@@ -1,36 +1,155 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { existsSync } from "node:fs";
4
- import { dirname, join } from "node:path";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
  const rootDir = join(__dirname, "..");
9
10
  const serverScript = join(rootDir, "src", "server.ts");
11
+ const port = Number(process.env.PORT || 4269);
12
+ const args = process.argv.slice(2);
10
13
 
11
- // server.ts handles a copy that is already running (and replaces it if older).
14
+ const HELP = `Token Larper: a local dashboard for AI coding token usage.
12
15
 
13
- // If executed directly inside Bun runtime
14
- if (typeof Bun !== "undefined") {
15
- await import(serverScript);
16
- } else {
17
- // If executed via Node / npx, check for Bun and hand over
18
- const bunCheck = spawnSync("bun", ["--version"], { stdio: "ignore" });
19
- if (bunCheck.status === 0) {
20
- const child = spawnSync("bun", [serverScript, ...process.argv.slice(2)], {
21
- stdio: "inherit",
22
- cwd: rootDir,
23
- });
24
- process.exit(child.status ?? 0);
16
+ Usage:
17
+ token-larper Start in the background and open the dashboard
18
+ token-larper --foreground Run in this terminal and show logs (Ctrl+C stops it)
19
+ token-larper --no-open Start without opening the browser
20
+ token-larper stop Stop the running copy
21
+
22
+ Environment: PORT (default 4269), TOKEN_LARPER_DATA_DIR, NO_TRAY=1`;
23
+
24
+ // Same folder as src/paths.ts; the background server's log goes there.
25
+ function dataDir() {
26
+ if (process.env.TOKEN_LARPER_DATA_DIR) return resolve(process.env.TOKEN_LARPER_DATA_DIR);
27
+ const home = homedir();
28
+ if (process.platform === "win32") return join(process.env.LOCALAPPDATA || join(home, "AppData", "Local"), "TokenLarper");
29
+ if (process.platform === "darwin") return join(home, "Library", "Application Support", "TokenLarper");
30
+ return join(process.env.XDG_DATA_HOME || join(home, ".local", "share"), "token-larper");
31
+ }
32
+
33
+ function bunPath() {
34
+ if (typeof Bun !== "undefined") return process.execPath;
35
+ if (spawnSync("bun", ["--version"], { stdio: "ignore" }).status === 0) return "bun";
36
+ console.error("🔥 Token Larper requires Bun (v1.1+) to run.");
37
+ console.error("\nInstall Bun with one command:");
38
+ if (process.platform === "win32") {
39
+ console.error(' powershell -c "irm bun.sh/install.ps1 | iex"');
25
40
  } else {
26
- console.error("🔥 Token Larper requires Bun (v1.1+) to run.");
27
- console.error("\nInstall Bun with one command:");
28
- if (process.platform === "win32") {
29
- console.error(' powershell -c "irm bun.sh/install.ps1 | iex"');
30
- } else {
31
- console.error(" curl -fsSL https://bun.sh/install | bash");
41
+ console.error(" curl -fsSL https://bun.sh/install | bash");
42
+ }
43
+ console.error("\nMore info: https://github.com/YarooqH/token-larper\n");
44
+ process.exit(1);
45
+ }
46
+
47
+ async function isRunning(p) {
48
+ try {
49
+ const res = await fetch(`http://127.0.0.1:${p}/api/tray-status`, { signal: AbortSignal.timeout(800) });
50
+ return res.ok;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function openBrowser(url) {
57
+ const [cmd, cmdArgs] =
58
+ process.platform === "win32" ? ["cmd", ["/c", "start", "", url]]
59
+ : process.platform === "darwin" ? ["open", [url]]
60
+ : ["xdg-open", [url]];
61
+ spawn(cmd, cmdArgs, { stdio: "ignore", detached: true, windowsHide: true }).unref();
62
+ }
63
+
64
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
65
+
66
+ async function stop() {
67
+ try {
68
+ await fetch(`http://127.0.0.1:${port}/api/shutdown`, {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: "{}",
72
+ signal: AbortSignal.timeout(2000),
73
+ });
74
+ } catch {
75
+ console.log(`Token Larper isn't running on port ${port}.`);
76
+ return;
77
+ }
78
+ for (let i = 0; i < 20 && (await isRunning(port)); i++) await sleep(250);
79
+ console.log("Stopped Token Larper.");
80
+ }
81
+
82
+ async function foreground() {
83
+ if (typeof Bun !== "undefined") return import(serverScript);
84
+ const child = spawnSync("bun", [serverScript], { stdio: "inherit", cwd: rootDir });
85
+ process.exit(child.status ?? 0);
86
+ }
87
+
88
+ async function background(open) {
89
+ const bun = bunPath();
90
+ const dir = dataDir();
91
+ mkdirSync(dir, { recursive: true });
92
+ const logFile = join(dir, "server.log");
93
+ const log = openSync(logFile, "w");
94
+
95
+ // The server moves to the next free port if another program has this one, and takes
96
+ // over from an older Token Larper on the same port (see src/server.ts).
97
+ const child = spawn(bun, [serverScript], {
98
+ cwd: rootDir,
99
+ detached: true,
100
+ windowsHide: true,
101
+ stdio: ["ignore", log, log],
102
+ env: { ...process.env, TOKEN_LARPER_NO_BROWSER: "1" },
103
+ });
104
+ let exited = false;
105
+ child.on("exit", () => (exited = true));
106
+ child.unref();
107
+ closeSync(log);
108
+
109
+ const readLog = () => {
110
+ try {
111
+ return readFileSync(logFile, "utf8");
112
+ } catch {
113
+ return "";
32
114
  }
33
- console.error("\nMore info: https://github.com/YarooqH/token-larper\n");
115
+ };
116
+
117
+ // Ready once the server says where it is listening. A copy of the same version that
118
+ // is already running says so too ("already running at ..."), then the new one exits.
119
+ let url = null;
120
+ for (let i = 0; i < 120 && !url; i++) {
121
+ await sleep(250);
122
+ url = readLog().match(/running at (http:\/\/localhost:\d+)/)?.[1] ?? null;
123
+ if (!url && exited) break;
124
+ }
125
+
126
+ if (!url) {
127
+ console.error("🔥 Token Larper didn't start.");
128
+ const tail = readLog().trim().split(/\r?\n/).slice(-15).join("\n");
129
+ if (tail) console.error(`\n${tail}\n`);
130
+ console.error(`Full log: ${logFile}`);
131
+ console.error("Run with --foreground to see the server output live.");
34
132
  process.exit(1);
35
133
  }
134
+
135
+ console.log(`🔥 Token Larper is running at ${url}`);
136
+ console.log(" It keeps running after you close this window.");
137
+ console.log(
138
+ process.platform === "win32"
139
+ ? " Quit it from the tray icon, or run: bunx token-larper stop"
140
+ : " Stop it with: bunx token-larper stop"
141
+ );
142
+ if (open) openBrowser(url);
143
+ process.exit(0);
144
+ }
145
+
146
+ if (args.includes("--help") || args.includes("-h")) {
147
+ console.log(HELP);
148
+ } else if (args[0] === "stop") {
149
+ await stop();
150
+ } else if (args.includes("--foreground") || args.includes("-f")) {
151
+ bunPath();
152
+ await foreground();
153
+ } else {
154
+ await background(!args.includes("--no-open") && process.env.TOKEN_LARPER_NO_BROWSER !== "1");
36
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-larper",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Token Larper — Multi-Harness AI Coding Usage & Cost Telemetry Dashboard powered by ccusage",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/server.ts CHANGED
@@ -64,18 +64,20 @@ function openDashboard(port: number) {
64
64
 
65
65
  /** The version of a Token Larper already on the port, "0.0.0" for one too old to say, or null if none. */
66
66
  async function runningVersion(port: number): Promise<string | null> {
67
+ // /api/version answers instantly; versions before 1.5.0 don't have it, so fall back to
68
+ // /api/tray-status, which can take a few seconds while usage data loads.
67
69
  try {
68
- const status = await fetch(`http://127.0.0.1:${port}/api/tray-status`, { signal: AbortSignal.timeout(600) });
69
- if (!status.ok) return null;
70
+ const res = await fetch(`http://127.0.0.1:${port}/api/version`, { signal: AbortSignal.timeout(1500) });
71
+ const body = (await res.json().catch(() => null)) as { current?: unknown } | null;
72
+ if (res.ok && typeof body?.current === "string") return body.current;
70
73
  } catch {
71
- return null;
74
+ // Nothing on the port, or it is busy; the next check decides.
72
75
  }
73
76
  try {
74
- const res = await fetch(`http://127.0.0.1:${port}/api/version`, { signal: AbortSignal.timeout(600) });
75
- const body = (await res.json()) as { current?: unknown };
76
- return typeof body.current === "string" ? body.current : "0.0.0";
77
+ const status = await fetch(`http://127.0.0.1:${port}/api/tray-status`, { signal: AbortSignal.timeout(5000) });
78
+ return status.ok ? "0.0.0" : null;
77
79
  } catch {
78
- return "0.0.0";
80
+ return null;
79
81
  }
80
82
  }
81
83