@emesoft/tokenhub 0.7.1 → 0.8.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
@@ -1,10 +1,16 @@
1
- # @emesoft/tokenhub
1
+ <p align="center">
2
+ <img src="https://tokenhub.aimpact.io.vn/readme/banner.jpg" alt="TokenHub" width="520">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@emesoft/tokenhub"><img src="https://img.shields.io/npm/v/@emesoft/tokenhub" alt="npm version"></a>
7
+ <a href="https://www.npmjs.com/package/@emesoft/tokenhub?activeTab=code"><img src="https://img.shields.io/npm/l/@emesoft/tokenhub" alt="MIT license"></a>
8
+ <img src="https://img.shields.io/node/v/@emesoft/tokenhub" alt="Node.js 18+">
9
+ </p>
2
10
 
3
- [![npm](https://img.shields.io/npm/v/@emesoft/tokenhub)](https://www.npmjs.com/package/@emesoft/tokenhub)
4
- [![license](https://img.shields.io/npm/l/@emesoft/tokenhub)](https://www.npmjs.com/package/@emesoft/tokenhub?activeTab=code)
5
- ![node](https://img.shields.io/node/v/@emesoft/tokenhub)
11
+ # @emesoft/tokenhub
6
12
 
7
- Connect [Claude Code](https://docs.anthropic.com/en/docs/claude-code) to a [TokenHub](https://tokenhub.aimpact.io.vn) pool in one command, and keep an eye on your spend from the terminal.
13
+ Connect [Claude Code](https://docs.anthropic.com/en/docs/claude-code) to your company's [TokenHub](https://tokenhub.aimpact.io.vn) in one command, and keep an eye on your spend from the terminal.
8
14
 
9
15
  - **One-step setup:** pair in the browser, no keys to copy.
10
16
  - **Spend at a glance:** a status line under the Claude Code prompt, plus `usage` and a live `dashboard`.
@@ -14,22 +20,30 @@ Connect [Claude Code](https://docs.anthropic.com/en/docs/claude-code) to a [Toke
14
20
 
15
21
  ```sh
16
22
  npm install -g @emesoft/tokenhub
17
- tokenhub connect
23
+ tokenhub connect <your tenant name>
18
24
  claude
19
25
  ```
20
26
 
27
+ Your tenant name is the first part of your TokenHub address: for `https://acme.aimpact.io.vn` it is `acme` (`connect` asks if you leave it out; a full URL works too). Ask your admin if you don't know it.
28
+
21
29
  `connect` shows a short code and opens your browser. Sign in to the pool, check the code matches, and confirm. That's it: Claude Code now runs through the pool.
22
30
 
31
+ <img src="https://tokenhub.aimpact.io.vn/readme/connect.png" alt="tokenhub connect in a terminal, next to the pool's page confirming the same code" width="100%">
32
+
33
+ From then on, today's spend and your credit sit under the Claude Code prompt:
34
+
35
+ <img src="https://tokenhub.aimpact.io.vn/readme/statusline.png" alt="The TokenHub status line under the Claude Code prompt" width="560">
36
+
23
37
  Prefer not to install? Use `npx @emesoft/tokenhub connect` instead.
24
38
 
25
39
  ## Commands
26
40
 
27
41
  | Command | What it does |
28
42
  | --- | --- |
29
- | `tokenhub connect [poolUrl]` | Pair this machine with the pool (default `https://tokenhub.aimpact.io.vn`). |
43
+ | `tokenhub connect [tenant]` | Pair this machine with your tenant (`acme` = `https://acme.aimpact.io.vn`, or a full URL). Asks for it when left out. |
30
44
  | `tokenhub usage [--json]` | Today's and this week's spend against your limits, your credit and pool health. |
31
45
  | `tokenhub dashboard` | Live full-screen view of spend and your recent requests. |
32
- | `tokenhub overlay [poolUrl]` | Install the TokenHub desktop overlay (Windows). |
46
+ | `tokenhub overlay <tenant>` | Install the TokenHub desktop overlay (Windows). |
33
47
  | `tokenhub disconnect` | Undo `connect`. |
34
48
  | `tokenhub --help` / `--version` | Help and version. |
35
49
 
@@ -61,10 +75,16 @@ Each machine gets its own key, which you can revoke from the pool's **Keys** pag
61
75
 
62
76
  `tokenhub dashboard` shows your daily and weekly spend as bars against your limits (green below 70%, orange to 90%, red above), your credit, pool health and your latest requests. Press `r` to refresh now and `q` to quit. If the connection drops, it keeps the last screen and reconnects on its own. With `--once`, `NO_COLOR` or no terminal, it prints a single plain frame.
63
77
 
78
+ <img src="https://tokenhub.aimpact.io.vn/readme/dashboard.png" alt="tokenhub dashboard: daily and weekly spend bars, credit, pool health and recent requests" width="100%">
79
+
64
80
  ## Desktop overlay
65
81
 
66
82
  `tokenhub overlay` downloads the TokenHub overlay installer from the pool and starts it (Windows). The overlay is a small always-on-top widget that shows your own usage after `connect`, or the whole pool after `connect --admin`.
67
83
 
84
+ <img src="https://tokenhub.aimpact.io.vn/readme/overlay.jpg" alt="The TokenHub desktop overlay, collapsed and expanded, on a desktop" width="100%">
85
+
86
+ <sub>Screenshots use sample data.</sub>
87
+
68
88
  ## For admins
69
89
 
70
90
  `tokenhub connect --admin` pairs this machine as an **admin device** for the desktop overlay's pool view:
@@ -85,12 +105,13 @@ The CLI talks only to the pool you connect to. Requests carry your key and one h
85
105
  | --- | --- |
86
106
  | `tokenhub: command not found` after `npm install` | Install globally with `-g`. A plain `npm install` puts it in the current folder, which isn't on your `PATH` (only `npx` finds it there). |
87
107
  | Command missing after switching Node with nvm | Global packages belong to one Node version. Run `npm install -g @emesoft/tokenhub` again. |
88
- | `The pool rejected this key` | The key was revoked. Run `tokenhub connect` again. |
108
+ | `The pool rejected this key` | The key was revoked. Run `tokenhub connect <tenant>` again. |
89
109
  | Claude Code ignores the pool | A shell variable is overriding `settings.json` (see the note under [What `connect` changes](#what-connect-changes)). |
90
110
 
91
111
  ## Links
92
112
 
93
- - Pool: sign in, manage keys and top up at [tokenhub.aimpact.io.vn](https://tokenhub.aimpact.io.vn)
113
+ - Your tenant: sign in, manage keys and top up at `https://<tenant>.aimpact.io.vn`; find yours at [tokenhub.aimpact.io.vn](https://tokenhub.aimpact.io.vn/enter)
114
+ - Upgrading from 0.7: the default pool is gone; run `tokenhub connect <tenant>` once
94
115
  - Changelog: [npm versions](https://www.npmjs.com/package/@emesoft/tokenhub?activeTab=versions)
95
116
 
96
117
  ## License
package/bin/tokenhub.js CHANGED
@@ -3,21 +3,22 @@ import { readFileSync } from "node:fs";
3
3
  import { adminConfigPath, removeAdminConfig } from "../lib/admin.js";
4
4
  import { parseDashboardArgs, runDashboard } from "../lib/dashboard.js";
5
5
  import { runOverlayInstall } from "../lib/overlay.js";
6
- import { DEFAULT_POOL_URL, runConnect } from "../lib/pair.js";
6
+ import { promptTenant, runConnect, TENANT_DOMAIN } from "../lib/pair.js";
7
7
  import { disconnectSettings, readSettings, settingsPath } from "../lib/settings.js";
8
8
  import { fetchUsage, formatUsage, resolveCredentials } from "../lib/usage.js";
9
9
 
10
- const HELP = `tokenhub - pair Claude Code with a TokenHub pool
10
+ const HELP = `tokenhub - pair Claude Code with your TokenHub tenant
11
11
 
12
12
  Usage:
13
- tokenhub connect [poolUrl] [--no-statusline]
13
+ tokenhub connect [tenant] [--no-statusline]
14
14
  Pair this machine and write ~/.claude/settings.json, including the status line
15
15
  (today's spend under the prompt) unless --no-statusline.
16
- poolUrl defaults to ${DEFAULT_POOL_URL}
17
- tokenhub connect [poolUrl] --admin
16
+ tenant is your tenant name (acme = https://acme.${TENANT_DOMAIN}) or a full URL;
17
+ it is asked for when left out
18
+ tokenhub connect [tenant] --admin
18
19
  Pair this machine as an admin device (for the desktop overlay's pool view): the admin
19
20
  confirms in the console, the token is saved to ~/.tokenhub/admin.json
20
- tokenhub overlay [poolUrl] Download and install the TokenHub desktop overlay (Windows)
21
+ tokenhub overlay <tenant> Download and install the TokenHub desktop overlay (Windows)
21
22
  tokenhub usage [--json] Show today's and this week's spend, credit and pool health
22
23
  tokenhub dashboard [--interval <sec>] [--once]
23
24
  Live full-screen view of spend, pool health and recent requests (q quits)
@@ -48,8 +49,17 @@ async function main(argv) {
48
49
  if (command === "connect") {
49
50
  const controller = new AbortController();
50
51
  process.once("SIGINT", () => controller.abort());
52
+ let poolUrl = arg;
53
+ if (!poolUrl) {
54
+ try {
55
+ poolUrl = await promptTenant();
56
+ } catch (error) {
57
+ console.error(`Error: ${error.message}`);
58
+ return 1;
59
+ }
60
+ }
51
61
  return runConnect({
52
- poolUrl: arg,
62
+ poolUrl,
53
63
  settingsFile: file,
54
64
  signal: controller.signal,
55
65
  statusLine: !flags.includes("--no-statusline"),
@@ -58,7 +68,7 @@ async function main(argv) {
58
68
  });
59
69
  }
60
70
  if (command === "overlay") {
61
- return runOverlayInstall(arg ? { poolUrl: arg } : {});
71
+ return runOverlayInstall({ poolUrl: arg });
62
72
  }
63
73
  if (command === "usage") {
64
74
  try {
package/lib/overlay.js CHANGED
@@ -2,8 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { mkdtempSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { normalizeUrl } from "./settings.js";
6
- import { DEFAULT_POOL_URL } from "./pair.js";
5
+ import { resolvePoolUrl } from "./pair.js";
7
6
 
8
7
  /** Path of the overlay installer on the pool. */
9
8
  export const OVERLAY_PATH = "/downloads/overlay.exe";
@@ -22,7 +21,7 @@ function launch(file) {
22
21
  /**
23
22
  * Download the TokenHub desktop overlay's Windows installer from the pool and start it.
24
23
  * @param {object} options
25
- * @param {string} [options.poolUrl] Pool address (default `DEFAULT_POOL_URL`).
24
+ * @param {string} options.poolUrl Tenant slug or pool URL (see `resolvePoolUrl`).
26
25
  * @param {(line: string) => void} [options.log] Output sink.
27
26
  * @param {string} [options.platform] Node platform name; the overlay is Windows only.
28
27
  * @param {(file: string) => void} [options.run] Starts the downloaded installer (tests replace it).
@@ -31,7 +30,7 @@ function launch(file) {
31
30
  * @returns {Promise<number>} Process exit code.
32
31
  */
33
32
  export async function runOverlayInstall({
34
- poolUrl = DEFAULT_POOL_URL,
33
+ poolUrl,
35
34
  log = console.log,
36
35
  platform = process.platform,
37
36
  run = launch,
@@ -44,7 +43,7 @@ export async function runOverlayInstall({
44
43
  }
45
44
  let url;
46
45
  try {
47
- url = `${normalizeUrl(poolUrl)}${OVERLAY_PATH}`;
46
+ url = `${resolvePoolUrl(poolUrl)}${OVERLAY_PATH}`;
48
47
  } catch (error) {
49
48
  log(`Error: ${error.message}`);
50
49
  return 1;
package/lib/pair.d.ts CHANGED
@@ -1,8 +1,10 @@
1
- export const DEFAULT_POOL_URL: string;
1
+ export const TENANT_DOMAIN: string;
2
+ export function resolvePoolUrl(input: string | undefined): string;
3
+ export function promptTenant(streams?: { input?: NodeJS.ReadableStream & { isTTY?: boolean }; output?: NodeJS.WritableStream }): Promise<string>;
2
4
  export function openBrowser(url: string, platform?: string): void;
3
5
  export function runConnect(options: {
4
- /** Pool address; defaults to `DEFAULT_POOL_URL`. */
5
- poolUrl?: string;
6
+ /** Tenant slug (`acme`) or pool URL; required. */
7
+ poolUrl: string | undefined;
6
8
  settingsFile: string;
7
9
  log?: (line: string) => void;
8
10
  open?: (url: string) => void;
package/lib/pair.js CHANGED
@@ -1,12 +1,46 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { hostname } from "node:os";
3
+ import { createInterface } from "node:readline/promises";
3
4
  import { writeAdminConfig } from "./admin.js";
4
5
  import { connectSettings, normalizeUrl, overridingEnvVars, readSettings } from "./settings.js";
5
6
 
6
7
  const MAX_WAIT_MS = 15 * 60 * 1000;
7
8
 
8
- /** The pool `connect` pairs with when no address is given. */
9
- export const DEFAULT_POOL_URL = "https://tokenhub.aimpact.io.vn";
9
+ /** Tenants live at `<slug>.<TENANT_DOMAIN>`; `connect <slug>` targets `https://<slug>.<TENANT_DOMAIN>`. */
10
+ export const TENANT_DOMAIN = "aimpact.io.vn";
11
+
12
+ const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/;
13
+
14
+ /**
15
+ * Turn what the user typed into a pool address: a full URL is kept, a tenant slug becomes its subdomain.
16
+ * @param {string | undefined} input Tenant slug (`acme`) or URL (`https://acme.aimpact.io.vn`).
17
+ * @returns {string} Pool address without a trailing slash.
18
+ * @throws {Error} When the input is empty, or neither a URL nor a valid slug.
19
+ */
20
+ export function resolvePoolUrl(input) {
21
+ const value = String(input ?? "").trim();
22
+ if (!value) throw new Error(`Name your tenant, e.g. \`tokenhub connect acme\` for https://acme.${TENANT_DOMAIN}`);
23
+ if (value.includes("://")) return normalizeUrl(value);
24
+ const slug = value.toLowerCase();
25
+ if (!SLUG_PATTERN.test(slug)) throw new Error(`"${value}" is not a tenant name (3-30 lowercase letters, digits and hyphens) or a URL.`);
26
+ return `https://${slug}.${TENANT_DOMAIN}`;
27
+ }
28
+
29
+ /**
30
+ * Ask for the tenant slug on the terminal.
31
+ * @param {{ input?: NodeJS.ReadableStream & { isTTY?: boolean }, output?: NodeJS.WritableStream }} [streams] Streams to prompt on (tests).
32
+ * @returns {Promise<string>} What was typed.
33
+ * @throws {Error} When stdin is not interactive.
34
+ */
35
+ export async function promptTenant({ input = process.stdin, output = process.stdout } = {}) {
36
+ if (!input.isTTY) throw new Error(`Name your tenant, e.g. \`tokenhub connect acme\` for https://acme.${TENANT_DOMAIN} (no terminal to ask on).`);
37
+ const rl = createInterface({ input, output });
38
+ try {
39
+ return await rl.question(`Tenant name (the part before .${TENANT_DOMAIN}): `);
40
+ } finally {
41
+ rl.close();
42
+ }
43
+ }
10
44
 
11
45
  /**
12
46
  * Open a URL in the default browser; failures are ignored (the URL is printed anyway).
@@ -65,7 +99,7 @@ async function postJson(url, body, signal) {
65
99
  /**
66
100
  * Run the pairing flow and write the issued key into settings.json.
67
101
  * @param {object} options
68
- * @param {string} [options.poolUrl] Pool address as typed by the user (default `DEFAULT_POOL_URL`).
102
+ * @param {string} options.poolUrl Tenant slug or pool URL as typed by the user (see `resolvePoolUrl`).
69
103
  * @param {string} options.settingsFile Path of settings.json to update.
70
104
  * @param {(line: string) => void} [options.log] Output sink.
71
105
  * @param {(url: string) => void} [options.open] Browser opener.
@@ -79,7 +113,7 @@ async function postJson(url, body, signal) {
79
113
  * @returns {Promise<number>} Process exit code.
80
114
  */
81
115
  export async function runConnect({
82
- poolUrl = DEFAULT_POOL_URL,
116
+ poolUrl,
83
117
  settingsFile,
84
118
  log = console.log,
85
119
  open = openBrowser,
@@ -93,7 +127,7 @@ export async function runConnect({
93
127
  }) {
94
128
  let pool;
95
129
  try {
96
- pool = normalizeUrl(poolUrl);
130
+ pool = resolvePoolUrl(poolUrl);
97
131
  if (!admin) readSettings(settingsFile);
98
132
  } catch (error) {
99
133
  log(`Error: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emesoft/tokenhub",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Connect Claude Code to a TokenHub pool in one command, and track your spend from the terminal",
5
5
  "keywords": [
6
6
  "claude",