@emesoft/tokenhub 0.7.2 → 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 +9 -6
- package/bin/tokenhub.js +18 -8
- package/lib/overlay.js +4 -5
- package/lib/pair.d.ts +5 -3
- package/lib/pair.js +39 -5
- package/package.json +1 -1
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
|
|
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%">
|
|
@@ -38,10 +40,10 @@ Prefer not to install? Use `npx @emesoft/tokenhub connect` instead.
|
|
|
38
40
|
|
|
39
41
|
| Command | What it does |
|
|
40
42
|
| --- | --- |
|
|
41
|
-
| `tokenhub connect [
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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 {
|
|
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
|
|
10
|
+
const HELP = `tokenhub - pair Claude Code with your TokenHub tenant
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
|
-
tokenhub connect [
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 {
|
|
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}
|
|
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
|
|
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 = `${
|
|
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
|
|
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
|
-
/**
|
|
5
|
-
poolUrl
|
|
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
|
-
/**
|
|
9
|
-
export const
|
|
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}
|
|
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
|
|
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 =
|
|
130
|
+
pool = resolvePoolUrl(poolUrl);
|
|
97
131
|
if (!admin) readSettings(settingsFile);
|
|
98
132
|
} catch (error) {
|
|
99
133
|
log(`Error: ${error.message}`);
|