token-larper 1.0.0 → 1.0.1

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
@@ -27,9 +27,9 @@ It consolidates all sessions into a real-time, interactive local dashboard runni
27
27
 
28
28
  The screens below use sample data. No local sessions or project names are included.
29
29
 
30
- [![Token Larper usage overview](docs/media/overview.jpg)](docs/media/walkthrough.mp4)
31
-
32
- [Watch the 20-second video walkthrough (MP4)](docs/media/walkthrough.mp4)
30
+ <video src="https://github.com/user-attachments/assets/104c8036-694e-474b-81e1-d33576f2cc30" controls playsinline poster="https://raw.githubusercontent.com/YarooqH/tokenlarper/main/docs/media/overview.jpg" width="820">
31
+ <a href="docs/media/walkthrough.mp4">Watch the 20-second walkthrough</a>
32
+ </video>
33
33
 
34
34
  | Usage breakdown | Projects |
35
35
  | --- | --- |
package/bin/cli.js CHANGED
@@ -8,6 +8,29 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const rootDir = join(__dirname, "..");
9
9
  const serverScript = join(rootDir, "src", "server.ts");
10
10
 
11
+ // Check if Token Larper is already running on the requested port
12
+ const port = process.env.PORT || 4269;
13
+ try {
14
+ const res = await fetch(`http://127.0.0.1:${port}/api/tray-status`, {
15
+ signal: AbortSignal.timeout(600),
16
+ });
17
+ if (res.ok) {
18
+ console.log(`🔥 Token Larper is already running at http://localhost:${port}`);
19
+ console.log(`🚀 Opening dashboard in your default browser...`);
20
+ const cmd =
21
+ process.platform === "win32"
22
+ ? `start http://localhost:${port}`
23
+ : process.platform === "darwin"
24
+ ? `open http://localhost:${port}`
25
+ : `xdg-open http://localhost:${port}`;
26
+ const { exec } = await import("node:child_process");
27
+ exec(cmd);
28
+ process.exit(0);
29
+ }
30
+ } catch {
31
+ // Server is not running yet, proceed with startup
32
+ }
33
+
11
34
  // If executed directly inside Bun runtime
12
35
  if (typeof Bun !== "undefined") {
13
36
  await import(serverScript);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-larper",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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
@@ -48,23 +48,49 @@ function isAllowedMutation(req: Request, port: number): boolean {
48
48
  return (req.headers.get("content-type") || "").toLowerCase().startsWith("application/json");
49
49
  }
50
50
 
51
+ // Check if Token Larper is already running on the target port
52
+ try {
53
+ const existing = await fetch(`http://127.0.0.1:${PORT}/api/tray-status`, {
54
+ signal: AbortSignal.timeout(600),
55
+ });
56
+ if (existing.ok) {
57
+ console.log(`🔥 Token Larper is already running at http://localhost:${PORT}`);
58
+ console.log(`🚀 Opening dashboard in your default browser...`);
59
+ const cmd =
60
+ process.platform === "win32"
61
+ ? `start http://localhost:${PORT}`
62
+ : process.platform === "darwin"
63
+ ? `open http://localhost:${PORT}`
64
+ : `xdg-open http://localhost:${PORT}`;
65
+ const { exec } = await import("node:child_process");
66
+ exec(cmd);
67
+ process.exit(0);
68
+ }
69
+ } catch {
70
+ // Not running, proceed with startup
71
+ }
72
+
51
73
  // Warm cached usage before the first dashboard request.
52
74
  void getDashboardData();
53
75
 
54
- const server = Bun.serve({
55
- port: PORT,
56
- hostname: "127.0.0.1",
57
- idleTimeout: 120,
58
- async fetch(req) {
59
- const url = new URL(req.url);
60
- const port = server.port ?? PORT;
61
-
62
- if (!isAllowedHost(req.headers.get("host"), port)) {
63
- return new Response("Forbidden", { status: 403 });
64
- }
65
- if (req.method !== "GET" && req.method !== "HEAD" && !isAllowedMutation(req, port)) {
66
- return new Response("Forbidden", { status: 403 });
67
- }
76
+ function startServer(preferredPort: number) {
77
+ let port = preferredPort;
78
+ while (port < preferredPort + 10) {
79
+ try {
80
+ return Bun.serve({
81
+ port,
82
+ hostname: "127.0.0.1",
83
+ idleTimeout: 120,
84
+ async fetch(req) {
85
+ const url = new URL(req.url);
86
+ const activePort = server?.port ?? port;
87
+
88
+ if (!isAllowedHost(req.headers.get("host"), activePort)) {
89
+ return new Response("Forbidden", { status: 403 });
90
+ }
91
+ if (req.method !== "GET" && req.method !== "HEAD" && !isAllowedMutation(req, activePort)) {
92
+ return new Response("Forbidden", { status: 403 });
93
+ }
68
94
 
69
95
  if (url.pathname === "/" || url.pathname === "/index.html") {
70
96
  return new Response(Bun.file(join(CLIENT_DIR, "index.html")), {
@@ -155,10 +181,22 @@ const server = Bun.serve({
155
181
  return Response.json({ ok: true, message: "Shutting down Token Larper..." });
156
182
  }
157
183
 
158
- return new Response("Not Found", { status: 404 });
159
- },
160
- });
184
+ return new Response("Not Found", { status: 404 });
185
+ },
186
+ });
187
+ } catch (err: any) {
188
+ if (err?.code === "EADDRINUSE" || String(err).includes("EADDRINUSE")) {
189
+ console.warn(`Port ${port} in use, trying port ${port + 1}...`);
190
+ port++;
191
+ continue;
192
+ }
193
+ throw err;
194
+ }
195
+ }
196
+ throw new Error(`Could not find an open port starting from ${preferredPort}`);
197
+ }
161
198
 
199
+ const server = startServer(PORT);
162
200
  startSystemTray(server.port ?? PORT);
163
201
 
164
202
  process.on("SIGINT", () => {