@emesoft/tokenhub 0.5.0 → 0.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
@@ -2,16 +2,23 @@
2
2
 
3
3
  Pair Claude Code on this machine with a [TokenHub](https://tokenhub.aimpact.io.vn) pool. No dependencies, Node 18+.
4
4
 
5
+ `connect` pairs with https://tokenhub.aimpact.io.vn by default. For another pool, pass its address: `npx @emesoft/tokenhub connect https://pool.example.com`.
6
+
5
7
  ```
6
- npx @emesoft/tokenhub connect https://tokenhub.aimpact.io.vn
7
- npx @emesoft/tokenhub connect https://tokenhub.aimpact.io.vn --no-statusline
8
+ npx @emesoft/tokenhub connect
9
+ npx @emesoft/tokenhub connect --no-statusline
8
10
  npx @emesoft/tokenhub usage
9
11
  npx @emesoft/tokenhub dashboard
10
12
  npx @emesoft/tokenhub disconnect
11
- npx @emesoft/tokenhub connect https://tokenhub.aimpact.io.vn --admin
13
+ npx @emesoft/tokenhub connect --admin
12
14
  npx @emesoft/tokenhub disconnect --admin
15
+ npx @emesoft/tokenhub overlay
13
16
  ```
14
17
 
18
+ To type `tokenhub` without `npx`, install it **globally**: `npm install -g @emesoft/tokenhub`, then `tokenhub connect`. A plain `npm install @emesoft/tokenhub` puts it in the current folder's `node_modules`, which is not on your `PATH` (only `npx` finds it there). With nvm, global packages belong to one Node version: install again after `nvm use`.
19
+
20
+ `overlay` (Windows) downloads the TokenHub desktop overlay installer from the pool (`/downloads/overlay.exe`) and starts it. The overlay then shows your own usage after `connect`, or the whole pool after `connect --admin`.
21
+
15
22
  `connect` prints a short code and opens your browser. Confirm the code on the pool's member site and the CLI writes `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` into `~/.claude/settings.json` (or `$CLAUDE_CONFIG_DIR/settings.json`). It also adds a `statusLine` that shows today's spend and credit under the Claude Code prompt (pass `--no-statusline` to skip it; a `statusLine` you already had is left alone). Other settings are kept, and the previous file is saved as `settings.json.bak`. Each paired machine gets its own key, which you can revoke on the Keys page.
16
23
 
17
24
  `usage` shows today's and this week's spend against your limits, your credit and the pool's health (`--json` for the raw report). It exits 2 when the pool would currently refuse your requests (out of credit or budget reached).
@@ -22,8 +29,8 @@ Requests to the pool carry your key and one extra header, `x-tokenhub-cli: <vers
22
29
 
23
30
  `disconnect` removes only those two entries and the TokenHub `statusLine`.
24
31
 
25
- `connect --admin` is for pool admins. It pairs this machine as an *admin device* for the TokenHub desktop overlay: you confirm the code on the pool's **admin** console (so only the signed-in admin can approve it), and the CLI saves a read-only device token to `~/.tokenhub/admin.json` (or `$TOKENHUB_CONFIG_DIR/admin.json`). It never touches Claude Code's settings. The token can read account names and quota levels, nothing else. Revoke a device on the admin Devices page (`/admin/pair`); `disconnect --admin` just forgets the token locally.
32
+ `connect --admin` is for pool admins. It pairs this machine as an *admin device* for the TokenHub desktop overlay: you confirm the code on the pool's **admin** console (so only the signed-in admin can approve it), and the CLI saves a device token to `~/.tokenhub/admin.json` (or `$TOKENHUB_CONFIG_DIR/admin.json`). It never touches Claude Code's settings. The token can read account names and quota levels and switch accounts on or off (the overlay's switch), nothing else: no emails, tokens, members or settings. Revoke a device on the admin Devices page (`/admin/pair`); `disconnect --admin` just forgets the token locally.
26
33
 
27
34
  If `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN` or `ANTHROPIC_API_KEY` is set in your shell, it overrides `settings.json`.
28
35
 
29
- Sign in, manage your keys and top up at [tokenhub.aimpact.io.vn](https://tokenhub.aimpact.io.vn). The setup guide there has this command with your pool address filled in.
36
+ Sign in, manage your keys and top up at [tokenhub.aimpact.io.vn](https://tokenhub.aimpact.io.vn).
package/bin/tokenhub.js CHANGED
@@ -2,19 +2,22 @@
2
2
  import { readFileSync } from "node:fs";
3
3
  import { adminConfigPath, removeAdminConfig } from "../lib/admin.js";
4
4
  import { parseDashboardArgs, runDashboard } from "../lib/dashboard.js";
5
- import { runConnect } from "../lib/pair.js";
5
+ import { runOverlayInstall } from "../lib/overlay.js";
6
+ import { DEFAULT_POOL_URL, runConnect } from "../lib/pair.js";
6
7
  import { disconnectSettings, readSettings, settingsPath } from "../lib/settings.js";
7
8
  import { fetchUsage, formatUsage, resolveCredentials } from "../lib/usage.js";
8
9
 
9
10
  const HELP = `tokenhub - pair Claude Code with a TokenHub pool
10
11
 
11
12
  Usage:
12
- tokenhub connect <poolUrl> [--no-statusline]
13
+ tokenhub connect [poolUrl] [--no-statusline]
13
14
  Pair this machine and write ~/.claude/settings.json, including the status line
14
- (today's spend under the prompt) unless --no-statusline
15
- tokenhub connect <poolUrl> --admin
15
+ (today's spend under the prompt) unless --no-statusline.
16
+ poolUrl defaults to ${DEFAULT_POOL_URL}
17
+ tokenhub connect [poolUrl] --admin
16
18
  Pair this machine as an admin device (for the desktop overlay's pool view): the admin
17
19
  confirms in the console, the token is saved to ~/.tokenhub/admin.json
20
+ tokenhub overlay [poolUrl] Download and install the TokenHub desktop overlay (Windows)
18
21
  tokenhub usage [--json] Show today's and this week's spend, credit and pool health
19
22
  tokenhub dashboard [--interval <sec>] [--once]
20
23
  Live full-screen view of spend, pool health and recent requests (q quits)
@@ -43,10 +46,6 @@ async function main(argv) {
43
46
  }
44
47
  const file = settingsPath();
45
48
  if (command === "connect") {
46
- if (!arg) {
47
- console.error(`Missing pool URL.\n\n${HELP}`);
48
- return 1;
49
- }
50
49
  const controller = new AbortController();
51
50
  process.once("SIGINT", () => controller.abort());
52
51
  return runConnect({
@@ -58,11 +57,14 @@ async function main(argv) {
58
57
  adminFile: adminConfigPath(),
59
58
  });
60
59
  }
60
+ if (command === "overlay") {
61
+ return runOverlayInstall(arg ? { poolUrl: arg } : {});
62
+ }
61
63
  if (command === "usage") {
62
64
  try {
63
65
  const credentials = resolveCredentials(readSettings(file).settings);
64
66
  if (!credentials) {
65
- console.error("Not connected. Run `tokenhub connect <poolUrl>` first.");
67
+ console.error("Not connected. Run `tokenhub connect` first.");
66
68
  return 1;
67
69
  }
68
70
  const report = await fetchUsage(credentials.baseUrl, credentials.key);
@@ -78,7 +80,7 @@ async function main(argv) {
78
80
  const options = parseDashboardArgs(argv.slice(1));
79
81
  const credentials = resolveCredentials(readSettings(file).settings);
80
82
  if (!credentials) {
81
- console.error("Not connected. Run `tokenhub connect <poolUrl>` first.");
83
+ console.error("Not connected. Run `tokenhub connect` first.");
82
84
  return 1;
83
85
  }
84
86
  return await runDashboard(credentials, options);
@@ -0,0 +1,9 @@
1
+ export const OVERLAY_PATH: string;
2
+ export function runOverlayInstall(options?: {
3
+ poolUrl?: string;
4
+ log?: (line: string) => void;
5
+ platform?: string;
6
+ run?: (file: string) => void;
7
+ dir?: string;
8
+ fetchImpl?: typeof fetch;
9
+ }): Promise<number>;
package/lib/overlay.js ADDED
@@ -0,0 +1,81 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { normalizeUrl } from "./settings.js";
6
+ import { DEFAULT_POOL_URL } from "./pair.js";
7
+
8
+ /** Path of the overlay installer on the pool. */
9
+ export const OVERLAY_PATH = "/downloads/overlay.exe";
10
+
11
+ /**
12
+ * Start the installer and let it run on its own (it shows its own window; this process does not wait for it).
13
+ * @param {string} file Installer path.
14
+ * @returns {void}
15
+ */
16
+ function launch(file) {
17
+ const child = spawn(file, [], { detached: true, stdio: "ignore", windowsHide: false });
18
+ child.on("error", () => {});
19
+ child.unref();
20
+ }
21
+
22
+ /**
23
+ * Download the TokenHub desktop overlay's Windows installer from the pool and start it.
24
+ * @param {object} options
25
+ * @param {string} [options.poolUrl] Pool address (default `DEFAULT_POOL_URL`).
26
+ * @param {(line: string) => void} [options.log] Output sink.
27
+ * @param {string} [options.platform] Node platform name; the overlay is Windows only.
28
+ * @param {(file: string) => void} [options.run] Starts the downloaded installer (tests replace it).
29
+ * @param {string} [options.dir] Folder to save the installer in (default a new temp folder).
30
+ * @param {typeof fetch} [options.fetchImpl] `fetch` override for tests.
31
+ * @returns {Promise<number>} Process exit code.
32
+ */
33
+ export async function runOverlayInstall({
34
+ poolUrl = DEFAULT_POOL_URL,
35
+ log = console.log,
36
+ platform = process.platform,
37
+ run = launch,
38
+ dir,
39
+ fetchImpl = fetch,
40
+ } = {}) {
41
+ if (platform !== "win32") {
42
+ log("The TokenHub overlay is a Windows app; there is no build for this system yet.");
43
+ return 1;
44
+ }
45
+ let url;
46
+ try {
47
+ url = `${normalizeUrl(poolUrl)}${OVERLAY_PATH}`;
48
+ } catch (error) {
49
+ log(`Error: ${error.message}`);
50
+ return 1;
51
+ }
52
+ log(`Downloading the overlay from ${url} ...`);
53
+ let res;
54
+ try {
55
+ res = await fetchImpl(url, { signal: AbortSignal.timeout(120_000) });
56
+ } catch (error) {
57
+ log(`Error: could not reach the pool (${error.message}).`);
58
+ return 1;
59
+ }
60
+ if (res.status === 404) {
61
+ log("This pool has not published the overlay installer yet. Ask your admin.");
62
+ return 1;
63
+ }
64
+ if (!res.ok) {
65
+ log(`Error: the pool answered HTTP ${res.status}.`);
66
+ return 1;
67
+ }
68
+ const bytes = Buffer.from(await res.arrayBuffer());
69
+ const expected = Number(res.headers.get("content-length"));
70
+ // A Windows executable starts with "MZ"; anything else (an HTML error page, a cut-off download) is not run.
71
+ if ((expected > 0 && bytes.length !== expected) || bytes.length < 2 || bytes[0] !== 0x4d || bytes[1] !== 0x5a) {
72
+ log("Error: the download is incomplete or not an installer. Try again.");
73
+ return 1;
74
+ }
75
+ const file = join(dir ?? mkdtempSync(join(tmpdir(), "tokenhub-overlay-")), "TokenHub-Overlay-setup.exe");
76
+ writeFileSync(file, bytes);
77
+ log(`Downloaded ${(bytes.length / 1024 / 1024).toFixed(1)} MB. Starting the installer...`);
78
+ run(file);
79
+ log("Follow the installer. The overlay picks up `tokenhub connect` (your usage) or `tokenhub connect --admin` (the pool).");
80
+ return 0;
81
+ }
package/lib/pair.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ export const DEFAULT_POOL_URL: string;
1
2
  export function openBrowser(url: string, platform?: string): void;
2
3
  export function runConnect(options: {
3
- poolUrl: string;
4
+ /** Pool address; defaults to `DEFAULT_POOL_URL`. */
5
+ poolUrl?: string;
4
6
  settingsFile: string;
5
7
  log?: (line: string) => void;
6
8
  open?: (url: string) => void;
package/lib/pair.js CHANGED
@@ -5,6 +5,9 @@ import { connectSettings, normalizeUrl, overridingEnvVars, readSettings } from "
5
5
 
6
6
  const MAX_WAIT_MS = 15 * 60 * 1000;
7
7
 
8
+ /** The pool `connect` pairs with when no address is given. */
9
+ export const DEFAULT_POOL_URL = "https://tokenhub.aimpact.io.vn";
10
+
8
11
  /**
9
12
  * Open a URL in the default browser; failures are ignored (the URL is printed anyway).
10
13
  * @param {string} url Address to open.
@@ -62,7 +65,7 @@ async function postJson(url, body, signal) {
62
65
  /**
63
66
  * Run the pairing flow and write the issued key into settings.json.
64
67
  * @param {object} options
65
- * @param {string} options.poolUrl Pool address as typed by the user.
68
+ * @param {string} [options.poolUrl] Pool address as typed by the user (default `DEFAULT_POOL_URL`).
66
69
  * @param {string} options.settingsFile Path of settings.json to update.
67
70
  * @param {(line: string) => void} [options.log] Output sink.
68
71
  * @param {(url: string) => void} [options.open] Browser opener.
@@ -76,7 +79,7 @@ async function postJson(url, body, signal) {
76
79
  * @returns {Promise<number>} Process exit code.
77
80
  */
78
81
  export async function runConnect({
79
- poolUrl,
82
+ poolUrl = DEFAULT_POOL_URL,
80
83
  settingsFile,
81
84
  log = console.log,
82
85
  open = openBrowser,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emesoft/tokenhub",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Pair Claude Code on this machine with a TokenHub pool",
5
5
  "type": "module",
6
6
  "bin": {