@emesoft/tokenhub 0.7.2 → 0.8.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
@@ -10,7 +10,7 @@
10
10
 
11
11
  # @emesoft/tokenhub
12
12
 
13
- 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.
14
14
 
15
15
  - **One-step setup:** pair in the browser, no keys to copy.
16
16
  - **Spend at a glance:** a status line under the Claude Code prompt, plus `usage` and a live `dashboard`.
@@ -20,10 +20,12 @@ Connect [Claude Code](https://docs.anthropic.com/en/docs/claude-code) to a [Toke
20
20
 
21
21
  ```sh
22
22
  npm install -g @emesoft/tokenhub
23
- tokenhub connect
23
+ tokenhub connect <your tenant name>
24
24
  claude
25
25
  ```
26
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
+
27
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.
28
30
 
29
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,16 +34,16 @@ From then on, today's spend and your credit sit under the Claude Code prompt:
32
34
 
33
35
  <img src="https://tokenhub.aimpact.io.vn/readme/statusline.png" alt="The TokenHub status line under the Claude Code prompt" width="560">
34
36
 
35
- Prefer not to install? Use `npx @emesoft/tokenhub connect` instead.
37
+ Prefer not to install? Use `npx @emesoft/tokenhub connect <your tenant name>` instead.
36
38
 
37
39
  ## Commands
38
40
 
39
41
  | Command | What it does |
40
42
  | --- | --- |
41
- | `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. |
42
44
  | `tokenhub usage [--json]` | Today's and this week's spend against your limits, your credit and pool health. |
43
45
  | `tokenhub dashboard` | Live full-screen view of spend and your recent requests. |
44
- | `tokenhub overlay [poolUrl]` | Install the TokenHub desktop overlay (Windows). |
46
+ | `tokenhub overlay <tenant>` | Install the TokenHub desktop overlay (Windows). |
45
47
  | `tokenhub disconnect` | Undo `connect`. |
46
48
  | `tokenhub --help` / `--version` | Help and version. |
47
49
 
@@ -103,12 +105,13 @@ The CLI talks only to the pool you connect to. Requests carry your key and one h
103
105
  | --- | --- |
104
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). |
105
107
  | Command missing after switching Node with nvm | Global packages belong to one Node version. Run `npm install -g @emesoft/tokenhub` again. |
106
- | `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. |
107
109
  | Claude Code ignores the pool | A shell variable is overriding `settings.json` (see the note under [What `connect` changes](#what-connect-changes)). |
108
110
 
109
111
  ## Links
110
112
 
111
- - 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
112
115
  - Changelog: [npm versions](https://www.npmjs.com/package/@emesoft/tokenhub?activeTab=versions)
113
116
 
114
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.2",
3
+ "version": "0.8.1",
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",